在 View Model 中管理多选 (.NET Metro Style Apps)
本文提供了一种基于附加行为的方法,用于从 View Model 中管理基于集合的 UI 控件中的多选。 本文中的所有代码严格适用于 Win 8 metro style apps。虽然这些行为可以轻松地适应 WPF/Silverlight。
介绍
本文展示了如何使用附加行为在 View Model 中管理多选。
为什么?
我正在将一些 WPF 代码(一个 Metro 应用程序)重构为 MVVM 设计模式,该模式大量使用了代码隐藏。 我偶然发现了一个需要绑定到 GridView
的 SelectedItems
属性的需求。 在捕捉模式下有一个列表视图,而在其他模式(完整/填充)下有网格视图。
要求
- 在 ViewModel 中定义
SelectedItems
- 将
GridView.ItemsSource
、ListView.ItemsSource
绑定到Items
- 以某种方式将
GridView
和ListView
的SelectedItems
绑定到SelectedItems
- 在步骤 3 之后,希望
GridView
中的任何选择更改都应反映到ListView
,反之亦然。
挑战
SelectedItems
属性是只读的,不能直接设置。SelectedItems
不能用于绑定表达式,因此无法在 View Model 中检索。- WinRT 不支持行为。 *(出于未知原因,我希望使用附加行为。)* 值得庆幸的是,CodePlex 上存在 WinRTBehaviors。
注意 - WinRT 本身不支持行为。 WinRTBehaviors 是一个用于提供行为支持的开源项目。 这个库很棒,并提供了一个行为扩展,与 WPF 框架完全类似。
附加行为概述
- 行为名称 -> MultiSelectBehavior。 它将针对
ListViewBase
(原因 -> 提供多选机制的基类)。 - 将
SelectedItems
依赖属性添加到行为。 此属性将跟踪关联 UI 元素 (以后将ListView
派生类称为 UI 元素) 的所选项目。 - 在行为的
OnAttached
、OnDetached
事件中挂钩 UI 元素的SelectionChanged
事件。 在OnSelectionChanged
事件中,将更改同步到SelectedItems
(行为)。 它会将 UI 选择更改传播到 MultiBehavior 中的SelectedItems
。 - 在
SelectedItems
(在行为中)的PropertyChanged
回调中,侦听绑定对象的CollectionChanged
。 将CollectionChanged
事件中的更改传播到 UI 元素。 - 将行为添加到 XAML 中的 UI 元素。
- 定义从
SelectedItems
(在行为中) 到 View Model 中SelectedItems
的数据绑定。
代码演练
ListView
、GridView
继承自 ListViewBase
。 ListViewBase
提供了多选机制 (SelectedItems
、SelectionMode
属性)。
定义了 MultiSelectBehavior
类,目标是 ListViewBase
。
public class MultiSelectBehavior : Behavior<ListViewBase>
在 MultiSelectBehavior
类中创建了 SelectedItems
依赖属性。 它内部保存了 ListViewBase
派生类中的所有选定项目。
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
"SelectedItems",
typeof(ObservableCollection<object>),
typeof(MultiSelectBehavior),
new PropertyMetadata(new ObservableCollection<object>(), PropertyChangedCallback));
public ObservableCollection<object> SelectedItems
{
get { return (ObservableCollection<object>)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
在行为中挂钩了 SelectionChanged
事件。
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectionChanged += OnSelectionChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.SelectionChanged -= OnSelectionChanged;
}
当在元素上触发 SelectionChanged
时,将填充 SelectedItems
。 _selectionChangedInProgress
标志指示选择更改正在进行中。 如果设置了此标志,则不执行进一步的处理(因为它将触发无限循环和堆栈溢出异常)。
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_selectionChangedInProgress) return;
_selectionChangedInProgress = true;
foreach (var item in e.RemovedItems)
{
if (SelectedItems.Contains(item))
{
SelectedItems.Remove(item);
}
}
foreach (var item in e.AddedItems)
{
if (!SelectedItems.Contains(item))
{
SelectedItems.Add(item);
}
}
_selectionChangedInProgress = false;
}
挂钩绑定 ObservableCollection
的集合更改事件。 将任何更改传播到 ListView base 派生 UI 元素。 这在 PropertyChangedCallback
处理程序中完成。
private static void PropertyChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
NotifyCollectionChangedEventHandler handler = (s, e) => SelectedItemsChanged(sender, e);
if (args.OldValue is ObservableCollection<object>)
{
(args.OldValue as ObservableCollection<object>).CollectionChanged -= handler;
}
if (args.NewValue is ObservableCollection<object>)
{
(args.NewValue as ObservableCollection<object>).CollectionChanged += handler;
}
}
private static void SelectedItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (sender is MultiSelectBehavior)
{
var listViewBase = (sender as MultiSelectBehavior).AssociatedObject;
var listSelectedItems = listViewBase.SelectedItems;
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
if (listSelectedItems.Contains(item))
{
listSelectedItems.Remove(item);
}
}
}
if (e.NewItems != null)
{
foreach (var item in e.NewItems)
{
if (!listSelectedItems.Contains(item))
{
listSelectedItems.Add(item);
}
}
}
}
}
现在 MultiSelectBehavior
已完成。
将行为应用于 UI 元素。
在 XAML 中导入命名空间 ( i -> 行为框架库, custom -> MultiSelectBehavior
类)
xmlns:i ="using:WinRtBehaviors"
xmlns:custom="using:WinRtExt.Behavior"
添加行为,并将行为的绑定 SelectedItems
附加到 ViewModel 的 SelectedItems
<i:Interaction.Behaviors>
<custom:MultiSelectBehavior SelectedItems="{Binding SelectedItems, Mode=TwoWay}">
</custom:MultiSelectBehavior>
</i:Interaction.Behaviors>
以下 XAML 适用于两个控件 (一个 ListView
,另一个 GridView
)
<GridView SelectionMode="Multiple" ItemsSource="{Binding Items}"
BorderBrush="White" BorderThickness="2"
ItemTemplate="{StaticResource textBlockDataTemplate}">
<i:Interaction.Behaviors>
<custom:MultiSelectBehavior SelectedItems="{Binding SelectedItems, Mode=TwoWay}">
</custom:MultiSelectBehavior>
</i:Interaction.Behaviors>
</GridView>
<Rectangle Width="20"></Rectangle>
<ListView SelectionMode="Multiple" ItemsSource="{Binding Items}"
BorderBrush="White" BorderThickness="2"
ItemTemplate="{StaticResource textBlockDataTemplate}">
<i:Interaction.Behaviors>
<custom:MultiSelectBehavior SelectedItems="{Binding SelectedItems, Mode=TwoWay}">
</custom:MultiSelectBehavior>
</i:Interaction.Behaviors>
</ListView>
列表视图和网格视图都启用了多选并保持同步。 任何控件中的任何选择更改都会传播到另一个控件。
代码是用 VS2012 RC 编写的,在 Win 8 Release preview 中。(它与旧版本、Win 8 Consumer preview、Win 8 developer preview 不兼容,并且可能会在未来的版本中损坏。)
要使用代码,请复制 MultiSelectBehavior.cs。
要执行该应用程序,请在 Win 8 release preview 的 VS2012RC 中打开。构建应用程序,部署应用程序,然后启动该应用程序。
兴趣点
感谢 WinRTBehaviors ,它让我从 View Model 管理多选,并以文章的形式分享信息。
历史
这是该文章/源代码的第一个版本。