Как поддерживать привязку ListBox SelectedItems к MVVM в навигационном приложении
Я создаю приложение WPF, которое можно использовать с помощью пользовательских кнопок "Далее" и "Назад" (например, не используя NavigationWindow
). На одном экране у меня есть ListBox
, который должен поддерживать множественный выбор (используя режим Extended
). У меня есть модель представления для этого экрана и сохранение выбранных элементов в качестве свойства, поскольку их необходимо поддерживать.
Однако я знаю, что свойство SelectedItems
для ListBox
доступно только для чтения. Я пытался решить проблему, используя это решение здесь, но я не смог ее перенести в мою реализацию. Я обнаружил, что я не могу различать, когда один или несколько элементов отменены, и когда я перемещаюсь между экранами (NotifyCollectionChangedAction.Remove
поднимается в обоих случаях, поскольку технически все выбранные элементы не выбираются при навигации по экрану). Мои навигационные команды расположены в отдельной модели представлений, которая управляет моделями просмотра для каждого экрана, поэтому я не могу помещать какую-либо реализацию, связанную с моделью просмотра, с ListBox
там.
Я нашел несколько других менее элегантных решений, но ни один из них, похоже, не обеспечивает двухстороннюю привязку между моделью просмотра и представлением.
Любая помощь будет принята с благодарностью. Я могу предоставить часть своего исходного кода, если это поможет понять мою проблему.
Ответы
Ответ 1
Попробуйте создать свойство IsSelected
для каждого из ваших элементов данных и привязать ListBoxItem.IsSelected
к этому свойству
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
Ответ 2
Решения Rachel отлично работают! Но есть одна проблема, с которой я столкнулся - если вы переопределите стиль ListBoxItem
, вы потеряете оригинальный стиль, примененный к нему (в моем случае ответственный за выделение выделенного элемента и т.д.). Вы можете избежать этого, наследуя от оригинального стиля:
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
Установка примечаний BasedOn
(см. этот ответ)
.
Ответ 3
Я не мог заставить решение Rachel работать так, как я этого хотел, но нашел ответ Sandesh о создании пользовательского свойство зависимостей отлично работает для меня. Мне просто пришлось написать аналогичный код для ListBox:
public class ListBoxCustom : ListBox
{
public ListBoxCustom()
{
SelectionChanged += ListBoxCustom_SelectionChanged;
}
void ListBoxCustom_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectedItemsList = SelectedItems;
}
public IList SelectedItemsList
{
get { return (IList)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register("SelectedItemsList", typeof(IList), typeof(ListBoxCustom), new PropertyMetadata(null));
}
В моей модели просмотра я просто ссылался на это свойство, чтобы получить выбранный вами список.
Ответ 4
Я продолжал искать легкое решение для этого, но не повезло.
Решение Rachel хорошо, если у вас уже есть свойство Selected на объекте в вашем ItemsSource. Если вы этого не сделаете, вам нужно создать модель для этой бизнес-модели.
Я пошел другим путем. Быстрый, но не идеальный.
В вашем ListBox создайте событие для SelectionChanged.
<ListBox ItemsSource="{Binding SomeItemsSource}"
SelectionMode="Multiple"
SelectionChanged="lstBox_OnSelectionChanged" />
Теперь реализуйте событие на коде, расположенном за вашей страницей XAML.
private void lstBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listSelectedItems = ((ListBox) sender).SelectedItems;
ViewModel.YourListThatNeedsBinding = listSelectedItems.Cast<ObjectType>().ToList();
}
Тада. Готово.
Это было сделано с помощью преобразования SelectedItemCollection в список.
Ответ 5
Не удовлетворенными ответами, я пытался найти один сам...
Ну, похоже, это скорее хак, а решение, но для меня это прекрасно работает. Это решение использует MultiBindings особым образом.
Сначала это может показаться тонкой кода, но вы можете использовать его с минимальными усилиями.
Сначала я реализовал "IMultiValueConverter"
public class SelectedItemsMerger : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
SelectedItemsContainer sic = values[1] as SelectedItemsContainer;
if (sic != null)
sic.SelectedItems = values[0];
return values[0];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return new[] { value };
}
}
И контейнер/обертка SelectedItems:
public class SelectedItemsContainer
{
/// Nothing special here...
public object SelectedItems { get; set; }
}
Теперь мы создаем привязку для нашего ListBox.SelectedItem(Singular). Примечание. Вам необходимо создать статический ресурс для "Конвертера". Это можно сделать один раз для каждого приложения и повторно использовать для всех списков, которые нуждаются в конвертере.
<ListBox.SelectedItem>
<MultiBinding Converter="{StaticResource SelectedItemsMerger}">
<Binding Mode="OneWay" RelativeSource="{RelativeSource Self}" Path="SelectedItems"/>
<Binding Path="SelectionContainer"/>
</MultiBinding>
</ListBox.SelectedItem>
В ViewModel я создал контейнер, с которым я могу привязываться. Важно инициализировать его с помощью new(), чтобы заполнить его значениями.
SelectedItemsContainer selectionContainer = new SelectedItemsContainer();
public SelectedItemsContainer SelectionContainer
{
get { return this.selectionContainer; }
set
{
if (this.selectionContainer != value)
{
this.selectionContainer = value;
this.OnPropertyChanged("SelectionContainer");
}
}
}
И что это. Может быть, кто-то видит некоторые улучшения?
Что вы думаете об этом?
Ответ 6
Вот еще одно решение. Это похоже на ответ Бена, но привязка работает двумя способами. Хитрость заключается в обновлении выбранных элементов ListBox
при изменении связанных элементов данных.
public class MultipleSelectionListBox : ListBox
{
public static readonly DependencyProperty BindableSelectedItemsProperty =
DependencyProperty.Register("BindableSelectedItems",
typeof(IEnumerable<string>), typeof(MultipleSelectionListBox),
new FrameworkPropertyMetadata(default(IEnumerable<string>),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));
public IEnumerable<string> BindableSelectedItems
{
get => (IEnumerable<string>)GetValue(BindableSelectedItemsProperty);
set => SetValue(BindableSelectedItemsProperty, value);
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
BindableSelectedItems = SelectedItems.Cast<string>();
}
private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MultipleSelectionListBox listBox)
listBox.SetSelectedItems(listBox.BindableSelectedItems);
}
}
К сожалению, я не смог использовать IList
как тип BindableSelectedItems. Это отправил null
в мое свойство модели просмотра, тип которого IEnumerable<string>
.
Здесь XAML:
<v:MultipleSelectionListBox
ItemsSource="{Binding AllMyItems}"
BindableSelectedItems="{Binding MySelectedItems}"
SelectionMode="Multiple"
/>
Есть одна вещь, на которую нужно следить. В моем случае ListBox
может быть удален из представления. По какой-то причине это приводит к тому, что свойство SelectedItems
изменяется на пустой список. Это, в свою очередь, приводит к изменению свойства модели вида на пустой список. В зависимости от вашего варианта использования это может быть нежелательно.
Ответ 7
Для меня это было серьезной проблемой, некоторые из ответов, которые я видел, были слишком хакерскими или требовали сброса значения свойства SelectedItems
нарушающего любой код, прикрепленный к объекту OnCollectionChanged. Но мне удалось получить работоспособное решение, изменив коллекцию напрямую, и в качестве бонуса он даже поддерживает SelectedValuePath
для коллекций объектов.
public class MultipleSelectionListBox : ListBox
{
internal bool processSelectionChanges = false;
public static readonly DependencyProperty BindableSelectedItemsProperty =
DependencyProperty.Register("BindableSelectedItems",
typeof(object), typeof(MultipleSelectionListBox),
new FrameworkPropertyMetadata(default(ICollection<object>),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));
public dynamic BindableSelectedItems
{
get => GetValue(BindableSelectedItemsProperty);
set => SetValue(BindableSelectedItemsProperty, value);
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
if (BindableSelectedItems == null || !this.IsInitialized) return; //Handle pre initilized calls
if (e.AddedItems.Count > 0)
if (!string.IsNullOrWhiteSpace(SelectedValuePath))
{
foreach (var item in e.AddedItems)
if (!BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
BindableSelectedItems.Add((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
}
else
{
foreach (var item in e.AddedItems)
if (!BindableSelectedItems.Contains((dynamic)item))
BindableSelectedItems.Add((dynamic)item);
}
if (e.RemovedItems.Count > 0)
if (!string.IsNullOrWhiteSpace(SelectedValuePath))
{
foreach (var item in e.RemovedItems)
if (BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
BindableSelectedItems.Remove((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
}
else
{
foreach (var item in e.RemovedItems)
if (BindableSelectedItems.Contains((dynamic)item))
BindableSelectedItems.Remove((dynamic)item);
}
}
private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MultipleSelectionListBox listBox)
{
List<dynamic> newSelection = new List<dynamic>();
if (!string.IsNullOrWhiteSpace(listBox.SelectedValuePath))
foreach (var item in listBox.BindableSelectedItems)
{
foreach (var lbItem in listBox.Items)
{
var lbItemValue = lbItem.GetType().GetProperty(listBox.SelectedValuePath).GetValue(lbItem, null);
if ((dynamic)lbItemValue == (dynamic)item)
newSelection.Add(lbItem);
}
}
else
newSelection = listBox.BindableSelectedItems as List<dynamic>;
listBox.SetSelectedItems(newSelection);
}
}
}
Связывание работает так же, как вы ожидали, что MS сделает сами:
<uc:MultipleSelectionListBox
ItemsSource="{Binding Items}"
SelectionMode="Extended"
SelectedValuePath="id"
BindableSelectedItems="{Binding mySelection}"
/>
Он не был тщательно протестирован, но прошел проверку первого взгляда. Я пытался использовать его повторно, используя динамические типы в коллекциях.
Ответ 8
Выключает привязку флажка к свойству IsSelected, и размещение текстового блока и флажка в панели стека делает трюк!