将多选列表框中的 SelectedItems 与 ViewModel 中的集合同步

Posted

技术标签:

【中文标题】将多选列表框中的 SelectedItems 与 ViewModel 中的集合同步【英文标题】:Sync SelectedItems in a muliselect listbox with a collection in ViewModel 【发布时间】:2010-11-20 19:46:27 【问题描述】:

我在使用 prism 的 SL3 应用程序中有一个多选列表框,我需要在我的视图模型中包含一个包含列表框中当前选定项目的集合。

视图模型对视图一无所知,因此它无权访问列表框控件。此外,我需要能够从视图模型中清除列表框中的选定项目。

不知道如何解决这个问题

谢谢 迈克尔

【问题讨论】:

【参考方案1】:

因此,假设您有一个具有以下属性的 ViewModel:

public ObservableCollection<string> AllItems  get; private set; 
public ObservableCollection<string> SelectedItems  get; private set; 

首先将 AllItems 集合绑定到 ListBox:

<ListBox x:Name="MyListBox" ItemsSource="Binding AllItems" SelectionMode="Multiple" />

问题在于 ListBox 上的 SelectedItems 属性不是 DependencyProperty。这很糟糕,因为您无法将其绑定到 ViewModel 中的某些内容。

第一种方法是将这个逻辑放在代码隐藏中,以调整 ViewModel:

public MainPage()

    InitializeComponent();

    MyListBox.SelectionChanged += ListBoxSelectionChanged;


private static void ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)

    var listBox = sender as ListBox;
    if(listBox == null) return;

    var viewModel = listBox.DataContext as MainVM;
    if(viewModel == null) return;

    viewModel.SelectedItems.Clear();

    foreach (string item in listBox.SelectedItems)
    
        viewModel.SelectedItems.Add(item);
    

这种方法行得通,但它真的很难看。我首选的方法是将这种行为提取到“附加行为”中。如果您这样做,您可以完全消除您的代码隐藏并在 XAML 中进行设置。好处是这个“附加行为”现在可以在任何 ListBox 中重复使用:

<ListBox ItemsSource="Binding AllItems" Demo:SelectedItems.Items="Binding SelectedItems" SelectionMode="Multiple" />

这里是附加行为的代码:

public static class SelectedItems

    private static readonly DependencyProperty SelectedItemsBehaviorProperty =
        DependencyProperty.RegisterAttached(
            "SelectedItemsBehavior",
            typeof(SelectedItemsBehavior),
            typeof(ListBox),
            null);

    public static readonly DependencyProperty ItemsProperty = DependencyProperty.RegisterAttached(
            "Items",
            typeof(IList),
            typeof(SelectedItems),
            new PropertyMetadata(null, ItemsPropertyChanged));

    public static void SetItems(ListBox listBox, IList list)  listBox.SetValue(ItemsProperty, list); 
    public static IList GetItems(ListBox listBox)  return listBox.GetValue(ItemsProperty) as IList; 

    private static void ItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    
        var target = d as ListBox;
        if (target != null)
        
            GetOrCreateBehavior(target, e.NewValue as IList);
        
    

    private static SelectedItemsBehavior GetOrCreateBehavior(ListBox target, IList list)
    
        var behavior = target.GetValue(SelectedItemsBehaviorProperty) as SelectedItemsBehavior;
        if (behavior == null)
        
            behavior = new SelectedItemsBehavior(target, list);
            target.SetValue(SelectedItemsBehaviorProperty, behavior);
        

        return behavior;
    


public class SelectedItemsBehavior

    private readonly ListBox _listBox;
    private readonly IList _boundList;

    public SelectedItemsBehavior(ListBox listBox, IList boundList)
    
        _boundList = boundList;
        _listBox = listBox;
        _listBox.SelectionChanged += OnSelectionChanged;
    

    private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    
        _boundList.Clear();

        foreach (var item in _listBox.SelectedItems)
        
            _boundList.Add(item);
        
    

【讨论】:

这根本不起作用,不仅因为该类应该是静态的,因此所选项目始终为空。 这适用于 SL3、SL4 和 WPF。我一直使用这种方法。是的,持有附加行为的类应该是静态的。它是 SL 和 WPF 中“附加行为”模式的一部分。 这绝对有效,我已经为此努力了半个小时。我总是忘记附加属性是 Silverlight/WPF 中的“修复任何东西”解决方案 这个解决方案几乎对我有用,除非 SelectedItems 属性不为空,它根本不会更新 UI。我该如何存档? 另一个方向的变化呢?假设我想从我的 ViewModel 中清除绑定列表?更新:另一个答案解决了这个问题。【参考方案2】:

我想要真正的双向绑定,以便 ListBox 选择反映底层 ViewModel 的 SelectedItems 集合中包含的项目。这让我可以通过 ViewModel 层中的逻辑来控制选择。

这是我对 SelectedItemsBehavior 类的修改。如果 ViewModel 属性实现 INotifyCollectionChanged(例如,由 ObservableCollection 类型实现),它们会将 ListBox.SelectedItems 集合与基础 ViewModel 属性同步。

  public static class SelectedItems
  
    private static readonly DependencyProperty SelectedItemsBehaviorProperty =
        DependencyProperty.RegisterAttached(
            "SelectedItemsBehavior",
            typeof(SelectedItemsBehavior),
            typeof(ListBox),
            null);

    public static readonly DependencyProperty ItemsProperty = DependencyProperty.RegisterAttached(
            "Items",
            typeof(IList),
            typeof(SelectedItems),
            new PropertyMetadata(null, ItemsPropertyChanged));

    public static void SetItems(ListBox listBox, IList list)  listBox.SetValue(ItemsProperty, list); 
    public static IList GetItems(ListBox listBox)  return listBox.GetValue(ItemsProperty) as IList; 

    private static void ItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    
      var target = d as ListBox;
      if (target != null)
      
        AttachBehavior(target, e.NewValue as IList);
      
    

    private static void AttachBehavior(ListBox target, IList list)
    
      var behavior = target.GetValue(SelectedItemsBehaviorProperty) as SelectedItemsBehavior;
      if (behavior == null)
      
        behavior = new SelectedItemsBehavior(target, list);
        target.SetValue(SelectedItemsBehaviorProperty, behavior);
      
    
  

  public class SelectedItemsBehavior
  
    private readonly ListBox _listBox;
    private readonly IList _boundList;

    public SelectedItemsBehavior(ListBox listBox, IList boundList)
    
      _boundList = boundList;
      _listBox = listBox;
      _listBox.Loaded += OnLoaded;
      _listBox.DataContextChanged += OnDataContextChanged;
      _listBox.SelectionChanged += OnSelectionChanged;

      // Try to attach to INotifyCollectionChanged.CollectionChanged event.
      var notifyCollectionChanged = boundList as INotifyCollectionChanged;
      if (notifyCollectionChanged != null)
      
        notifyCollectionChanged.CollectionChanged += OnCollectionChanged;
      
    

    void UpdateListBoxSelection()
    
      // Temporarily detach from ListBox.SelectionChanged event
      _listBox.SelectionChanged -= OnSelectionChanged;

      // Synchronize selected ListBox items with bound list
      _listBox.SelectedItems.Clear();
      foreach (var item in _boundList)
      
        // References in _boundList might not be the same as in _listBox.Items
        var i = _listBox.Items.IndexOf(item);
        if (i >= 0)
        
          _listBox.SelectedItems.Add(_listBox.Items[i]);
        
      

      // Re-attach to ListBox.SelectionChanged event
      _listBox.SelectionChanged += OnSelectionChanged;
    

    void OnLoaded(object sender, RoutedEventArgs e)
    
      // Init ListBox selection
      UpdateListBoxSelection();
    

    void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    
      // Update ListBox selection
      UpdateListBoxSelection();
    

    void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    
      // Update ListBox selection
      UpdateListBoxSelection();
    

    void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    
      // Temporarily deattach from INotifyCollectionChanged.CollectionChanged event.
      var notifyCollectionChanged = _boundList as INotifyCollectionChanged;
      if (notifyCollectionChanged != null)
      
        notifyCollectionChanged.CollectionChanged -= OnCollectionChanged;
      

      // Synchronize bound list with selected ListBox items
      _boundList.Clear();
      foreach (var item in _listBox.SelectedItems)
      
        _boundList.Add(item);
      

      // Re-attach to INotifyCollectionChanged.CollectionChanged event.
      if (notifyCollectionChanged != null)
      
        notifyCollectionChanged.CollectionChanged += OnCollectionChanged;
      
    
  

【讨论】:

我似乎无法正常工作:我在列表框中选择一项,然后执行命令将该项目在列表中上移一位。这是通过将它与绑定到 ItemsSource 的集合中的下一个项目交换来完成的。然后我使用您发布的代码选择下一个项目。最终结果是列表框中的第一项和第二项都被选中,即使在 UpdateListBoxSelection() 中放置断点时 _listBox.SelectedItems 仅包含单个元素。 好的:我首先必须清除 SelectedItems,然后修改 ItemsSource,然后重新选择。由于某种原因,首先修改然后更改选择不能正常工作.. 对于那些仍然无法使其工作的人,请确保您没有像我一样修改您的 Windows 主题颜色。事实证明,当 ListBox 失焦时,我的 ListBox 背景颜色与选择颜色匹配,因此看起来好像没有选择任何内容。将您的 ListBox 背景画笔更改为红色,以检查您是否遇到了这种情况。这让我花了 2 个小时才意识到......【参考方案3】:

谢谢!我添加了一个小更新来支持初始加载和 DataContext 更改。

干杯,

亚历山德罗·皮洛蒂 [MVP/IIS]

public class SelectedItemsBehavior

    private readonly ListBox _listBox;
    private readonly IList _boundList;

    public ListBoxSelectedItemsBehavior(ListBox listBox, IList boundList)
    
        _boundList = boundList;
        _listBox = listBox;

        SetSelectedItems();

        _listBox.SelectionChanged += OnSelectionChanged;
        _listBox.DataContextChanged += ODataContextChanged;
    

    private void SetSelectedItems()
    
        _listBox.SelectedItems.Clear();

        foreach (object item in _boundList)
        
            // References in _boundList might not be the same as in _listBox.Items
            int i = _listBox.Items.IndexOf(item);
            if (i >= 0)
                _listBox.SelectedItems.Add(_listBox.Items[i]);
        
    

    private void ODataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    
        SetSelectedItems();
    

    private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    
        _boundList.Clear();

        foreach (var item in _listBox.SelectedItems)
        
            _boundList.Add(item);
        
    

【讨论】:

只是我还是 ListBox 上不存在事件 DataContextChanged 它在 WPF 中确实如此,但在 Silverlight 5 之前不是 Silverlight。【参考方案4】:

通过选择 Collection Changed & Rebinded 上的项目更新了现有行为

http://rnragu.blogspot.com/2011/04/multiselect-listbox-in-silverlight-use.html

【讨论】:

【参考方案5】:

如果您记得先创建一个可观察集合的实例,上述原始解决方案就可以工作!此外,您需要确保 Observable 集合内容类型与您的 ListBox ItemSource 的内容类型匹配(如果您与上面提到的确切示例不同)。

【讨论】:

【参考方案6】:

这是一个包含此问题解决方案的博客,其中包括一个示例应用程序,以便您可以准确了解如何使其工作: http://alexshed.spaces.live.com/blog/cns!71C72270309CE838!149.entry

我刚刚在我的应用程序中实现了这个,它很好地解决了问题

【讨论】:

【参考方案7】:

我的解决方案是将 Alessandro Pilotti 更新与 Brian Genisio 附加行为结合起来。但是删除 DataContext 更改的代码 Silverlight 4 不支持这个。

如果您将列表框绑定到 ObservableCollection&lt;string&gt;,上面的工作正常,但如果您通过 DataTemplate 绑定到复杂的对象,例如 ObservableCollection&lt;Person&gt; SelectedItems get; private set; ,它似乎不起作用。这是由于集合使用的 Equals 方法的默认实现。您可以通过告诉您的 Person 对象在确定对象是否相等时比较哪些字段来解决此问题,这是通过在您的对象上实现接口 IEquatable&lt;T&gt; 来完成的。

之后 IndexOf(item) 代码将起作用,并且能够比较对象是否相等并选择列表中的项目

// References in _boundList might not be the same as in _listBox.Items
int i = _listBox.Items.IndexOf(item);
if (i >= 0)
  _listBox.SelectedItems.Add(_listBox.Items[i]);

查看链接:http://msdn.microsoft.com/en-us/library/ms131190(VS.95).aspx

【讨论】:

【参考方案8】:

我在 XAML 中的选择更改事件上使用 EventToCommand 对象,并将 ListBox 作为参数传递给那里。比 MMVM 中的命令管理所选项目的 ObservableCollection。它简单快捷;)

【讨论】:

【参考方案9】:

Brian Genisio 和 Samuel Jack here 的解决方案很棒。我已经成功实施了。但是我也遇到过这样的情况,因为我不是 WPF 或 .Net 方面的专家,所以我无法调试它。我仍然不确定问题是什么,但在适当的时候,我找到了多选绑定的解决方法。在这个解决方案中,我不必访问 DataContext。

此解决方案适用于无法使上述 2 个解决方案发挥作用的人。我猜这个解决方案不会被视为 MVVM。它是这样的。假设您在 ViewModel 中有 2 个集合:

public ObservableCollection<string> AllItems  get; private set; 
public ObservableCollection<string> SelectedItems  get; private set; 

你需要一个列表框:

<ListBox x:Name="MyListBox" ItemsSource="Binding AllItems" SelectionMode="Multiple" />

现在添加另一个 ListBox 并将其绑定到 SelectedItems 并设置 Visibility:

<ListBox x:Name="MySelectedItemsListBox" ItemsSource="Binding SelectedItems, Mode=OneWayToSource" SelectionMode="Multiple" Visibility="Collapsed" />

现在,在 WPF 页面后面的代码中,在 InitializeComponent() 方法之后添加到构造函数:

MyListBox.SelectionChanged += MyListBox_SelectionChanged;

并添加一个方法:

private void MyListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)

    MySelectedItemsListBox.ItemsSource = MyListBox.SelectedItems;

你就完成了。这肯定会奏效。如果上述解决方案不起作用,我想这也可以在 Silverlight 中使用。

【讨论】:

【参考方案10】:

对于那些仍然无法使 candritzky 回答工作的人,请确保您没有像我一样修改您的 Windows 主题颜色。事实证明,当 ListBox 失焦时,我的 ListBox 背景颜色与选择颜色匹配,因此看起来好像没有选择任何内容。

将您的 ListBox 背景画笔更改为红色,以检查您是否遇到了这种情况。这让我花了 2 个小时才意识到......

【讨论】:

以上是关于将多选列表框中的 SelectedItems 与 ViewModel 中的集合同步的主要内容,如果未能解决你的问题,请参考以下文章

将多选列表变成一个漂亮且易于使用的带有复选框的列表。

Access VBA 如何根据多选列表框中的选择过滤记录集?

UITableView 多选,预选项

如何将多选框的值发送到 django 后端

php将多选标签保存到文本文件

JSON将多选行编码为数组[重复]