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

20

我在使用Prism编写的SL3应用程序中有一个多选列表框,需要在视图模型中添加一个集合属性,保存当前被选中项的信息,但是视图模型无法直接访问列表框控件。同时,我需要能够在视图模型中清除列表框中的选中项。

目前还不确定该如何处理这个问题。

谢谢, Michael

10个回答

40

假设您有一个具有以下属性的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);
        }
    }
}

1
这根本不起作用,不仅因为类应该是静态的,而且所选项目始终为空。 - msfanboy
除了一个问题,这个解决方案对我来说几乎完美。如果SelectedItems属性不为空,它根本不会更新UI。我该怎么实现呢? - Anonymous
另一方向的更改呢?假设我想要从我的ViewModel中清除绑定列表怎么办?更新:另一个答案解决了这个问题。 - ultravelocity
如果这仍然相关,请将 ListBox 更改为 MultiSelector,以便允许更广泛的控件(例如 DataGrid)。 - Bas
@MattWolf 不是的。这个已经6年了。Silverlight已经死了。我们都已经超越它了。 - Brian Genisio
显示剩余4条评论

7
我希望实现真正的双向绑定,使 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只包含一个元素。 - stijn
好的,我明白了:我首先需要清除SelectedItems,然后修改ItemsSource,最后重新选择。由于某种原因,先修改再更改选择不起作用。 - stijn
对于那些仍然无法让它正常工作的人,请确保您没有像我一样修改了Windows主题颜色。结果发现,当ListBox失去焦点时,我的ListBox背景颜色与选择颜色匹配,从而使它看起来好像没有选择任何内容。将您的ListBox背景刷成红色,以检查是否也是这种情况。我就因此花了2个小时才意识到问题所在… - CGodo

3

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

祝好!

Alessandro Pilotti [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事件? - Neil
它在WPF中可以实现,但在Silverlight中直到Silverlight 5才能实现。 - Joe McBride

3

1

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


0
我的解决方案是将Alessandro Pilotti的更新与Brian Genisio的附加行为结合起来。但是要删除更改DataContext的代码,因为Silverlight 4不支持它。
如果您将listbox绑定到ObservableCollection<string>,则以上方法可以正常工作,但是如果您通过DataTemplate将其绑定到像ObservableCollection<Person> SelectedItems { get; private set; }这样的复杂对象,则似乎无法正常工作。这是由于集合使用的Equals方法的默认实现。您可以通过告诉您的Person对象在确定对象是否相等时比较哪些字段来解决此问题,这是通过在对象上实现接口IEquatable<T>来完成的。
之后,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


0
Brian Genisio和Samuel Jack的解决方案在这里非常好。我已经成功实现了它。但是我也遇到了一种情况,这种情况下它没有起作用,因为我不是WPF或.Net的专家,所以我无法调试它。我仍然不确定问题是什么,但是在适当的时间内,我找到了一个多选绑定的解决方法。在这个解决方案中,我不需要获取DataContext。
这个解决方案适用于那些无法使上述两个解决方案工作的人。我想这个解决方案不会被视为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,然后设置可见性:

<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。


0

对于那些仍然无法使candritzky的答案起作用的人,请确保您没有像我一样修改Windows主题颜色。结果发现,当ListBox失去焦点时,我的ListBox背景颜色与选择颜色匹配,从而使它看起来好像没有选中任何内容。

将您的ListBox背景刷成红色以检查是否也发生了这种情况。我花了2个小时才意识到这一点...


0

0

我在XAML中使用EventToCommand对象,在选择更改事件中将ListBox作为参数传递。然后,MMVM中的命令管理所选项目的ObservableCollection。这很容易和快速 ;)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接