如何在可导航的应用程序中支持与MVVM绑定的ListBox SelectedItems

我正在制作一个可通过自定义“下一步”和“后退”按钮和命令(即不使用NavigationWindowNavigationWindow的WPF应用程序。 在一个屏幕上,我有一个ListBox ,必须支持多个选择(使用Extended模式)。 我有一个这个屏幕的视图模型,并将所选项目存储为属性,因为它们需要维护。

但是,我知道ListBoxSelectedItems属性是只读的。 我一直在尝试使用此解决方案解决此问题 ,但我无法将其应用到我的实现中。 我发现无法区分何时取消选择一个或多个元素以及何时在屏幕之间导航(在两种情况下都会引发NotifyCollectionChangedAction.Remove ,因为从技术上讲,在离开屏幕时会取消选择所有选定的项目)。 我的导航命令位于一个单独的视图模型中,该模型管理每个屏幕的视图模型,因此我不能将任何与视图模型相关的实现与ListBox放在那里。

我找到了其他一些不太优雅的解决方案,但这些解决方案似乎都没有强制实现视图模型和视图之间的双向绑定。

任何帮助将不胜感激。 我可以提供一些我的源代码,如果它有助于理解我的问题。

尝试在每个数据项上创建一个IsSelected属性,并将ListBoxItem.IsSelected绑定到该属性

  

雷切尔的解决方案效果很好! 但是我遇到了一个问题 – 如果你覆盖了ListBoxItem的样式,你就会失去应用它的原始样式(在我的情况下负责突出显示所选项目等)。 您可以通过inheritance原始样式来避免这种情况:

  

注意设置BasedOn (参见本答案 )。

我无法让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)); } 

在我的视图模型中,我刚引用该属性来获取我选择的列表。

我一直在寻找一个简单的解决方案,但没有运气。

如果您已经在ItemsSource中的对象上拥有Selected属性,那么Rachel的解决方案很好。 如果不这样做,则必须为该业务模型创建模型。

我走了另一条路。 一个快速的,但不完美。

在ListBox上为SelectionChanged创建一个事件。

  

现在在XAML页面后面的代码上实现该事件。

 private void lstBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) { var listSelectedItems = ((ListBox) sender).SelectedItems; ViewModel.YourListThatNeedsBinding = listSelectedItems.Cast().ToList(); } 

田田。 完成。

这是在将SelectedItemCollection转换为List的帮助下完成的。

不满意给定的答案我试图自己找到一个……好吧,事实certificate它更像是一个黑客然后解决方案但对我来说工作正常。 此解决方案以特殊方式使用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)创建Binding。 注意:您必须为“转换器”创建静态资源。 这可以在每个应用程序中完成一次,并且可以重用于需要转换器的所有ListBox。

       

在ViewModel中,我创建了可以绑定到的Container。 使用new()初始化它是很重要的,以便用值填充它。

  SelectedItemsContainer selectionContainer = new SelectedItemsContainer(); public SelectedItemsContainer SelectionContainer { get { return this.selectionContainer; } set { if (this.selectionContainer != value) { this.selectionContainer = value; this.OnPropertyChanged("SelectionContainer"); } } } 

就是这样。 也许有人看到了一些改进? 你怎么看待这件事?

这是另一种解决方案。 它与Ben的答案类似,但绑定有两种方式。 诀窍是在绑定数据项更改时更新ListBox的选定项。

 public class MultipleSelectionListBox : ListBox { public static readonly DependencyProperty BindableSelectedItemsProperty = DependencyProperty.Register("BindableSelectedItems", typeof(IEnumerable), typeof(MultipleSelectionListBox), new FrameworkPropertyMetadata(default(IEnumerable), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged)); public IEnumerable BindableSelectedItems { get => (IEnumerable)GetValue(BindableSelectedItemsProperty); set => SetValue(BindableSelectedItemsProperty, value); } protected override void OnSelectionChanged(SelectionChangedEventArgs e) { base.OnSelectionChanged(e); BindableSelectedItems = SelectedItems.Cast(); } private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is MultipleSelectionListBox listBox) listBox.SetSelectedItems(listBox.BindableSelectedItems); } } 

不幸的是,我无法使用IList作为BindableSelectedItems类型。 这样做会将null发送到我的视图模型的属性,其类型为IEnumerable

这是XAML:

  

有一点需要注意。 在我的例子中,可以从视图中删除ListBox 。 由于某种原因,这会导致SelectedItems属性更改为空列表。 反过来,这会导致视图模型的属性更改为空列表。 根据您的使用情况,这可能是不可取的。

这对我来说是一个主要问题,我看到的一些答案要么过于hackish,要么重置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), 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 newSelection = new List(); 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; listBox.SetSelectedItems(newSelection); } } } 

绑定的工作方式与您预期的MS完成一样:

  

它尚未经过全面测试,但已通过首次检查。 我试图通过在集合上使用动态类型来保持可重用性。

结果将复选框绑定到IsSelected属性,并将文本块和复选框放在堆栈面板中就可以了!