Вопрос по listbox, wpf, c#, mvvm – Как поддерживать привязку ListBox SelectedItems с MVVM в навигационном приложении
Я делаю приложение WPF, в котором можно перемещаться с помощью пользовательского & quot; Далее & quot; и & quot; Назад & quot; кнопки и команды (т.е. не используяNavigationWindow
). На одном экране у меня естьListBox
который должен поддерживать несколько выборов (используяExtended
Режим). У меня есть модель представления для этого экрана и я храню выбранные элементы как свойство, так как они должны поддерживаться.
Тем не менее, я знаю, чтоSelectedItems
свойствоListBox
только для чтения. Я пытался обойти проблему, используяэто решение здесь, но я не смог принять его в свою реализацию. Я обнаружил, что не могу различить, когда один или несколько элементов отменены, и когда я перемещаюсь между экранами (NotifyCollectionChangedAction.Remove
поднимается в обоих случаях, поскольку технически все выбранные элементы отменяются при удалении от экрана). Мои навигационные команды расположены в отдельной модели представления, которая управляет моделями представления для каждого экрана, поэтому я не могу поставить любую реализацию, связанную с моделью представления, с помощьюListBox
там.
Я нашел несколько других менее изящных решений, но ни одно из них, по-видимому, не обеспечивает двустороннюю связь между моделью представления и представлением.
Любая помощь будет принята с благодарностью. Я могу предоставить часть моего исходного кода, если это поможет понять мою проблему.
SelectedItems
(мой называлсяSelectedLanguages
). Теперь я получаюInvalidOperationException
бросили вBindableCollection
конструктор, когда я нажимаю кнопку & quot; Назад & quot; Кнопка в строке диспетчер вызывается сRaisePropertyChangedEventHandler
, Я попытался просто вставить блок try / catch сDispatcher.BeginInvoke
в блоке перехвата, но затем элементы списка не переизбираются при возврате к странице.
Попробуйте создатьIsSelected
собственность на каждый из ваших элементов данных и привязкиListBoxItem.IsSelected
к этой собственности
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
IsSelected
значение хранится наDataContext
IsSelected
моделировать объекты? Я сделал это и немного запутался, особенно когда один и тот же экземпляр модельного объекта появляется в нескольких местах (в дереве). Когда один из них выбран, все они отображаются выбранными. Это не всегда желательно. Также мне кажется, что модель "загрязнена". По материалам просмотра. Какой у вас опыт?
Вот еще одно решение. Он подобен бенуответ, но привязка работает двумя способами. Хитрость в том, чтобы обновить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
свойство изменить на пустой список. Это, в свою очередь, приводит к тому, что свойство модели представлени измен етс на пустой список. В зависимости от вашего варианта использования это может быть нежелательно.
Это было серьезной проблемой для меня, некоторые из ответов, которые я видел, были либо слишком хакерскими, либо требовали перезагрузки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}"
/>
Он не был тщательно протестирован, но прошел первые проверки. Я пытался сохранить его многократное использование, используя динамические типы в коллекциях.
Не удовлетворенный данными ответами, я пытался найти один сам ... Ну, это больше похоже на взлом, чем решение, но для меня это работает отлично. Это решение использует 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");
}
}
}
И это все. Может быть, кто-то видит некоторые улучшения? Что ты думаешь об этом?
Решения Рэйчел прекрасно работают! Но есть одна проблема, с которой я столкнулся - если вы переопределяете стильListBoxItem
Вы теряете оригинальную стилизацию, примененную к нему (в моем случае, ответственную за выделение выбранного элемента и т. д.). Вы можете избежать этого, наследуя от оригинального стиля:
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
Примечание настройкиBasedOn
(увидетьэтот ответ)
.
Я не мог заставить Рэйчел работать так, как я хотел, но я нашелСандеш & APOS; s ответ на создание кастомасвойство зависимости работать идеально для меня. Мне просто нужно было написать похожий код для 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));
}
В моей модели представления я просто ссылался на это свойство, чтобы получить выбранный список.
Я продолжал искать простое решение для этого, но безуспешно.
Решение, которое есть у Рэйчел, хорошо, если у вас уже есть свойство Selected объекта в вашем ItemsSource. Если вы этого не сделаете, вы должны создать модель для этой бизнес-модели.
Я пошел другим путем. Быстрый, но не идеальный.
В вашем ListBox создайте событие для SelectionChanged.
<ListBox ItemsSource="{Binding SomeItemsSource}"
SelectionMode="Multiple"
SelectionChanged="lstBox_OnSelectionChanged" />
Теперь реализуйте событие в коде вашей страницы XAML.
private void lstBox_OnSelectionChanged(object sender, Sel,ectionChangedEventArgs e)
{
var listSelectedItems = ((ListBox) sender).SelectedItems;
ViewModel.YourListThatNeedsBinding = listSelectedItems.Cast<ObjectType>().ToList();
}
Тад. Готово.
Это было сделано с помощьюпреобразование SelectedItemCollection в список.
Это было довольно легко сделать с помощью Command и Interactivities EventTrigger. ItemsCount - это просто привязанное свойство для использования в вашем XAML, если вы хотите отобразить обновленное количество.
XAML:
<ListBox ItemsSource="{Binding SomeItemsSource}"
SelectionMode="Multiple">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"
CommandParameter="{Binding ElementName=MyView, Path=SelectedItems.Count}" />
</i:EventTrigger>
</Interaction.Triggers>
</ListView>
<Label Content="{Binding ItemsCount}" />
ViewModel:
private int _itemsCount;
private RelayCommand<int> _selectionChangedCommand;
public ICommand SelectionChangedCommand
{
get {
return _selectionChangedCommand ?? (_selectionChangedCommand =
new RelayCommand<int>((itemsCount) => { ItemsCount = itemsCount; }));
}
}
public int ItemsCount
{
get { return _itemsCount; }
set {
_itemsCount = value;
OnPropertyChanged("ItemsCount");
}
}