关于silverlight 3.0:使用ViewModel中的集合在muliselect列表框中同步SelectedItems

Sync SelectedItems in a muliselect listbox with a collection in ViewModel

我在使用Prism的SL3应用程序中有一个多选列表框,我需要在我的ViewModel中有一个包含列表框中当前所选项目的集合。

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

不知道如何处理这个问题

谢谢迈克尔


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

1
2
public ObservableCollection<string> AllItems { get; private set; }
public ObservableCollection<string> SelectedItems { get; private set; }

首先将Allitems集合绑定到列表框:

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

问题是列表框上的SelectedItems属性不是DependencyProperty。这很糟糕,因为您不能将它绑定到您的ViewModel中的某个对象上。

第一种方法是将这个逻辑放在代码后面,调整视图模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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中进行设置。额外的好处是,这种"附加行为"现在可以在任何列表框中重用:

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

下面是附加行为的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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);
        }
    }
}


我希望有真正的双向绑定,以便列表框选择反映底层ViewModel的SelectedItems集合中包含的项。这允许我在ViewModel层中通过逻辑控制选择。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
  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;
      }
    }
  }


通过选择集合中的项已更改并重新绑定,更新了现有行为

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


谢谢你!我添加了一个小的更新来支持初始加载和数据上下文更改。

干杯,

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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);
        }
    }
}


如果您记得首先创建一个可观察集合的实例,那么上面的原始解决方案是有效的!此外,还需要确保可观察集合内容类型与列表框项源的内容类型匹配(如果您偏离了上面提到的确切示例)。


对于那些仍然不能让Candritzky回答工作的人,确保你没有像我一样修改你的Windows主题颜色。当列表框失去焦点时,我的列表框背景颜色与选择颜色匹配,从而使其看起来像没有选择任何内容。

将您的列表框背景画笔更改为红色,以检查这是否是发生在您身上的事情。我花了2个小时才意识到…


布莱恩·吉尼西奥和塞缪尔·杰克的解决方案很好。我已经成功地实现了它。但我也遇到过这样的情况:这不起作用,因为我对WPF或.NET不是专家,所以我未能调试它。我仍然不确定这个问题是什么,但在适当的时候,我找到了一个多选择绑定的解决方案。在这个解决方案中,我不需要访问DataContext。

此解决方案适用于无法使上述两个解决方案发挥作用的人员。我想这个解决方案不会被认为是MVVM。就像这样。假设在ViewModel中有两个集合:

1
2
public ObservableCollection<string> AllItems { get; private set; }
public ObservableCollection<string> SelectedItems { get; private set; }

您需要一个列表框:

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

现在添加另一个列表框并将其绑定到selecteditems并设置可见性:

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

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

1
MyListBox.SelectionChanged += MyListBox_SelectionChanged;

并添加一个方法:

1
2
3
4
private void MyListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    MySelectedItemsListBox.ItemsSource = MyListBox.SelectedItems;
}

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


我在XAML中使用EventToCommand对象处理Selection Changed事件,并将ListBox作为参数传递到该事件。mmvm中的than命令正在管理所选项目的ObservableCollection。它既简单又快速;)


我的解决方案是将亚历山德罗·皮洛蒂的最新消息与布莱恩·金尼西奥的行为联系起来。但是删除DataContext的代码更改Silverlight4不支持此功能。

如果要将列表框绑定到ObservableCollection上,上述方法可以正常工作,但如果要通过数据模板绑定到像ObservableCollection SelectedItems { get; private set; }这样的复杂对象,则似乎无法工作。这是由于集合使用的equals方法的默认实现所致。您可以通过告诉您的Person对象在确定对象是否相等时要比较哪些字段来解决这个问题,这可以通过在对象上实现接口IEquatable来实现。

之后,(项)代码的索引将工作,并且能够比较对象是否相等,并选择列表中的项。

1
2
3
4
// 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


这里有一个关于这个问题的解决方案的博客,其中包括一个示例应用程序,这样您就可以确切地看到如何使其工作:http://alexshed.spaces.live.com/blog/cns!71C72270309CE838!149条目

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