基于 View-View Model 的 WPF 和 XAML 实现模式。(WPF 和 XAML 代码复用模式简易示例。第 2 部分)






4.94/5 (26投票s)
基于 View-View Model 的实现模式。
引言
正如在WPF 控件模式。(WPF 和 XAML 代码复用模式简易示例。第 1 部分)中提到的,WPF 框架催生了全新的编程概念,这些概念也可以用于 WPF 之外的纯非可视化编程。在我看来,这些概念至少与面向对象编程(OOP)相对于过程式编程的进步幅度相同,是 OOP 之上的一步。
围绕这些新概念,可以构建各种 WPF/XAML 模式。这些模式共同作用,可以在应用程序功能内部实现巨大的代码复用和关注点分离。
在本系列文章中,我将尝试通过简单的 WPF 示例来介绍和阐明这些模式。
在本系列的第一篇文章——WPF 控件模式。(WPF 和 XAML 代码复用模式简易示例。第 1 部分)——中,我介绍了一些处理 WPF Control
和 ControlTemplate
的模式。该文章中描述的模式完全不依赖于 WPF Control
的一个非常特殊的 DataContext
属性,也没有处理 ContentControl
或 ItemControl
将非可视化对象转换为可视化对象的能力。本文将填补这一空白。
我将本文中描述的模式称为“实现模式”,因为它们用于实现某些可视化行为。更复杂的模式(我称之为“架构模式”)留待本系列的第三部分,我希望很快能发表一篇描述它们的文章。
通过使用本文和上一篇文章中描述的 XAML/WPF 范式,我成功地以极高的速度进行编程,同时生成了高质量和高密度的代码(具有高复用率和单位代码大量功能的代码)。
通常,随着代码库的扩展,开发人员的生产力会下降——开发人员对自己的代码有点困惑,他们需要更长时间才能找出复用旧功能的最佳方法。事实上,应用本系列文章中描述的原则,随着代码库的扩展,生产力和代码复用会上升,因为开发人员实际上是在用相同的模式和思想构建一个可复用的框架。
本文主要讨论基于视图和视图模型之间关系的模式——关于使可视化对象模仿完全非可视化对象。
本文不适合初学者——我假设读者熟悉基本的 WPF 概念——依赖属性和附加属性、绑定、样式、模板、数据模板、触发器。
可视化对象和非可视化对象
可视化对象和非可视化对象
在WPF 控件模式。(WPF 和 XAML 代码复用模式简易示例。第 1 部分)中,我们讨论了骨架-肉体范式。无外观控件扮演着骨架的角色,而ControlTemplate
则是肉体。请注意,无外观控件仍然必须派生自 WPF 的Control
类型(或其子类)的对象。
在本文中,我们将主要讨论提供完全非可视化甚至非 WPF 骨架的肉体的可视化对象。这是 MVVM 范式的 View-View Model 部分。这里我们不关心 MVVM 的 Model 部分——它可以很容易地合并到 View Model 中或由 View Model 复制。
本节主要是一个复习——但仍然强烈建议您仔细阅读,因为它从大多数 WPF 教科书不同的角度讨论了已知的 WPF 控件。
DataContext 属性
每个 FrameworkElement
对象都有 DataContext
依赖属性。除非被重写,否则此属性会从逻辑树中的对象父级继承。DataContext
对象是 WPF Binding
的默认源对象,其含义是,如果绑定中未指定 RelativeSource
、Source
或 ElementName
,则绑定假定其 Source
对象由绑定目标的 DataContext
提供。这是由实现 WPF 的微软人员特意这样做的,以便让可视化对象更容易将其属性绑定到由其 DataContext
属性提供的非可视化对象。
ContentControl
一些 WPF 教科书指出,您可以在 WPF ContentControl
的 Content
属性中放置任何 XAML 代码。例如,您可以有一个 Button
(它是 ContentControl
的后代),其中包含一个 StackPanel
对象,而 StackPanel
又包含多个 Image
对象。
<Button>
<StackPanel>
<Image Source="Image1">
<Image Source="Image2">
/<StackPanel>
/<Button>
虽然这是事实,但这并不是使用ContentControl
的Content
属性的最佳方式,应尽可能避免。
ContentControl
更实用的定义应该是——一个允许将其Content
属性指向的非可视化对象与其ContentTemplate
属性指向的DataTemplate
“结合”成一个可视化对象的控件。
项目 SimpleContentControlSample 演示了 ContentControl
的这种用法。
非可视化对象由TextContainer
类表示。它只有一个属性TheText
和一个方法ChangeText()
,用于在“Hello World”和“Bye World”之间切换TheText
属性。
public class TextContainer : INotifyPropertyChanged { #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; #endregion protected void OnPropertyChanged(string propertyName) { if (this.PropertyChanged != null) { this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } #region TheText Property private string _text = "Hello World"; public string TheText { get { return this._text; } set { if (this._text == value) { return; } this._text = value; this.OnPropertyChanged("TheText"); } } #endregion TheText Property bool _showOriginalText = true; public void ChangeText() { _showOriginalText = !_showOriginalText; if (_showOriginalText) { TheText = "Hello World"; } else { TheText = "Bye World"; } } }
请注意,OnPropertyChanged
函数会触发在 INotifyPropertyChanged
接口中声明的 PropertyChanged
事件,以通知 WPF Binding
属性已更改。
在 MainWindow.xaml 文件的 Window
的 Resources
部分,我们定义了非可视化对象和 DataTemplate
。
<!-- non-visual object -->
<this:TextContainer x:Key="TheTextContainer" />
<!-- data template built around the TextContainer -->
<DataTemplate x:Key="TextContainerDataTemplate">
<StackPanel x:Name="TopLevelDataTemplatePanel"
Orientation="Horizontal">
<TextBlock Text="{Binding Path=TheText}"
VerticalAlignment="Center"/>
<Button x:Name="ChangeTextButton"
Width="120"
Height="25"
Content="Change Text"
Margin="20,5"
VerticalAlignment="Center">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<ei:CallMethodAction MethodName="ChangeText"
TargetObject="{Binding }"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</StackPanel>
</DataTemplate>
TextBlock
的Text
属性绑定到TextContainer
对象的TheText
属性。单击ChangeTextButton
(通过 MS Expression Blend SDK 管道)将导致调用ChangeText()
方法。
注意:正如在WPF 控件模式。(WPF 和 XAML 代码复用模式简易示例。第 1 部分)中所述,您无需安装 MS Expression Blend 即可使用其 SDK。它可以从MS Expression Blend SDK免费下载和使用。此外,我们只需要 SDK 中的两个 dll 文件:Microsoft.Expression.Interactions.dll 和 System.Windows.Interactivity.dll,并且我们将这些文件与我们的代码一起提供在 MSExpressionBlendSDKDlls 文件夹下。
由于您是从互联网下载这些文件,您可能需要取消阻止它们。要做到这一点,右键单击每个文件,选择“属性”菜单项,然后单击“取消阻止”按钮。
最后,在Window
中,我们创建了ContentControl
,其Content
属性设置为TextContainer
对象,其ContentTemplate
属性设置为TextContainerDataTemplate
。
<ContentControl HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{StaticResource TheTextContainer}"
ContentTemplate="{StaticResource TextContainerDataTemplate}"/>
这是我们运行项目后得到的结果
按下“更改文本”按钮将在“Hello World”和“Bye World”之间切换文本。
请注意,所有用于显示文本以及处理“Hello World”和“Bye World”字符串之间切换的有意义功能都定义在 TextContainer
对象中,该对象充当我们简单应用程序的视图模型。视图在 MainWindow.xaml 文件中由 TextContainerDataTemplate
定义,仅负责应用程序的可视化方面,并为 TextContainer
类中定义的逻辑提供了一个包装器或外壳。
重要提示
除了Content
属性之外,ContentControl
还具有通常的DataContext
属性,其值如上所述在逻辑树中从父级传播到子级。Content
属性不会自动设置为DataContext
属性——如果您希望将其设置为DataContext
属性——您需要提供一个绑定:Content={Binding}
。请注意,您不必指定绑定路径——默认情况下,{Binding}
将绑定到同一对象的DataContext
属性。
ContentControl
的Content
对象成为数据模板中顶层对象的DataContext
——在我们的例子中,“TopLevelDataTemplatePanel”StackPanel
的DataContext
属性设置为TextContainer
视图模型对象。相应地,相同的对象在DataTemplate
中沿着逻辑树向下传递到TextBlock
和Button
元素,作为DataContext
。
ItemsControl
虽然 ContentControl
非常适合显示单个非可视化对象,但 ItemsControl
用于显示非可视化对象的集合。ItemsControl
的 ItemsSource
属性被设置为(通常通过绑定)非可视化对象的集合。其 ItemTemplate
属性被设置为 DataTemplate
,为集合中的每个项目提供可视化表示。
请注意,ItemsSource
可以设置为任何集合,但只有当 ItemsSource
集合实现 INotifyCollectionChanged
接口(或实现 ObservableCollection<T>
)时,当项目从 ItemsSource
集合中添加或删除时,可视化效果才会更新。否则,可视化效果只会在 ItemsSource
本身被重置时改变。
项目SimpleItemsControlSample
演示了如何使用ItemsControl
。
它的TextContainer
类代表一个非可视化(视图模型)对象,而TextContainerTestCollection
类代表一个此类非可视化对象的集合。
TextContainer
类与上一个示例中的不同。我们不再硬编码按钮点击前后的显示字符串,而是允许在TextContainer
的构造函数中传递这些字符串。
string _originalText = null; string _alternateText = null; public TextContainer(string originalText, string alternateText) { _originalText = originalText; _alternateText = alternateText; }
TheText
属性的实现也略有改变,仅仅是为了演示一种更优雅的实现方式。它不再有 Setter,并且根据 _showOriginalString
标志的值返回 _originalText
或 _alternativeText
字符串。
#region TheText Property public string TheText { get { if (_showOriginalText) { return _originalText; } else { return _alternateText; } } } #endregion TheText Property
ChangeText()
方法切换_showOriginalString
标志并调用OnPropertyChanged("TheText")
,以便通知UITheText
属性已更改。
bool _showOriginalText = true; public void ChangeText() { _showOriginalText = !_showOriginalText; OnPropertyChanged("TheText"); }
TextContainerTestCollection
类派生自ObservableCollection<TextContainer>
。它的构造函数向自身添加了3个TextContainer
项。
public TextContainerTestCollection() { Add(new TextContainer("Hi World", "Bye World")); Add(new TextContainer("Hi Friend", "Bye Friend")); Add(new TextContainer("Hi WPF", "Bye WPF")); }
一个TextContainerTestCollection
对象在XAML的Window.Resources
部分中定义。我们还在那里以与上一个示例完全相同的方式定义了TextContainerDataTemplate
。
<Window.Resources>
<!-- non-visual object -->
<this:TextContainerTestCollection x:Key="TheTextContainerTestCollection" />
<!-- data template built around the TextContainer -->
<DataTemplate x:Key="TextContainerDataTemplate">
<StackPanel x:Name="TopLevelDataTemplatePanel"
Orientation="Horizontal">
<TextBlock Text="{Binding Path=TheText}"
VerticalAlignment="Center" />
<Button Width="120"
Height="25"
Content="Change Text"
Margin="20,5"
VerticalAlignment="Center">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<ei:CallMethodAction MethodName="ChangeText"
TargetObject="{Binding }" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</StackPanel>
</DataTemplate>
</Window.Resources>
最后,我们定义了ItemsControl
,将其ItemsSource
设置为非可视化集合,将其ItemTemplate
设置为数据模板。
<!-- Resulting ItemsControl -->
<ItemsControl HorizontalAlignment="Center"
VerticalAlignment="Center"
ItemsSource="{StaticResource TheTextContainerTestCollection}"
ItemTemplate="{StaticResource TextContainerDataTemplate}"/>
这是我们运行应用程序时的样子
按下每个项目上的按钮,都会将其文本更改为备用文本。再次按下则会改回。
将 ControlTemplate 用于 ContentControl
本节我要讨论的最后一个主题是如何使用ControlTemplate
来改变内容控件的外观。
此小节的解决方案名为CustomToggleControl
。它由主项目(也同名)组成,并引用了一个项目CustomControls
。
该项目的目的是将ToggleButton
定义为ContentControl
,并展示如何为其提供几种不同的ControlTemplate
。
MyToggleButton
类代表Lookless
骨架的切换按钮。它派生自ContentControl
类。它定义了一个布尔依赖属性IsChecked
。
#region IsChecked Dependency Property public bool IsChecked { get { return (bool)GetValue(IsCheckedProperty); } set { SetValue(IsCheckedProperty, value); } } public static readonly DependencyProperty IsCheckedProperty = DependencyProperty.Register ( "IsChecked", typeof(bool), typeof(MyToggleButton), new PropertyMetadata(false) ); #endregion IsChecked Dependency Property
它还为MouseUp
事件设置了一个事件处理程序,以便在MouseUp
时切换IsChecked
属性。
public MyToggleButton() { this.MouseUp += MyToggleButton_MouseUp; } void MyToggleButton_MouseUp(object sendr, System.Windows.Input.MouseButtonEventArgs e) { this.IsChecked = !this.IsChecked; }
在 CustomControls.xaml 文件中,我们为相同的 MyToggleButton
控件在两种不同的样式中定义了两个控件模板——“MyToggleButtonCheckBoxStyle”将切换按钮显示为复选框,而“ToggleButton_ButtonStyle”将切换按钮显示为按钮。
在描述模板之前,让我们先看看应用程序的主文件。它在垂直的StackPanel
中包含MyToggleButton
控件的两个实例。
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center">
<customControls:MyToggleButton Content="My Toggle Button - Check Box Style:"
Style="{StaticResource MyToggleButtonCheckBoxStyle}"
Foreground="White"
Background="Black"
Margin="10,10" />
<customControls:MyToggleButton Content="My Toggle Button - Button Style:"
Foreground="White"
Background="Black"
Style="{StaticResource ToggleButton_ButtonStyle}"
Margin="10,10" />
</StackPanel>
当我们运行应用程序时,我们会得到以下结果
复选框样式按钮在其 IsChecked
属性为 true 时,在其右侧的方框中显示一个对勾标记,而按钮样式则将背景更改为更暗。
现在让我们看看相应的ControlTemplate
。
复选框模板有一个水平的StackPanel
,其中包含一个ContentPresenter
和一个Border
。边框表示带有复选框的方块。ContentPresenter
是一个特殊对象,它控制ContentControl
的DataTemplate
将在何处渲染。它采用由ContentControl
的DataTemplate
定义的形状和形式。如果未定义DataTemplate
,它将简单地将ContentPresenter
渲染为一个TextBlock
,显示ContentControl
的Content
属性的字符串表示。
<ControlTemplate TargetType="this:MyToggleButton">
<Grid Background="Transparent">
<StackPanel Orientation="Horizontal"
Background="{TemplateBinding Background}">
<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
<!-- the square on the right hand side-->
<Border Width="20"
Height="20"
BorderThickness="1"
BorderBrush="{TemplateBinding Foreground}"
Background="{TemplateBinding Background}"
Margin="10, 1, 0, 1">
<!-- this the the check mark. It is visible when
IsChecked property of the control is true and
invisible otherwise -->
<TextBlock Text="V"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{TemplateBinding Foreground}"
Visibility="{Binding Path=IsChecked,
Converter={StaticResource TheBoolToVisConverter},
RelativeSource={RelativeSource AncestorType=this:MyToggleButton}}" />
</Border>
</StackPanel>
</Grid>
</ControlTemplate>
按钮样式切换按钮有一个包含边框的Grid
面板,边框内有一个ContentPresenter
。它还有一个不透明的Border
对象,覆盖在原始边框上。默认情况下,不透明边框的透明度为0.5。通过触发器,我们在鼠标悬停时将其透明度降低到0.3,当控件被选中时,透明度进一步降低到0。
<ControlTemplate TargetType="this:MyToggleButton">
<Grid Background="Transparent"
x:Name="ItemPanel">
<Border x:Name="TheItemBorder"
Background="Black"
BorderBrush="White"
BorderThickness="1">
<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="10, 5" />
</Border>
<!-- provide some opacity for the control -->
<Border x:Name="TheOpacityBorder"
Background="White"
Opacity="0.5" />
</Grid>
<ControlTemplate.Triggers>
<!-- reduce the opacity of the opacity border
to 0.3 on Mouse Over -->
<Trigger Property="IsMouseOver"
Value="True">
<Setter TargetName="TheOpacityBorder"
Property="Opacity"
Value="0.3" />
</Trigger>
<!-- reduce the opacity of the opacity border
to 0 when the button is checked -->
<Trigger Property="IsChecked"
Value="True">
<Setter TargetName="TheOpacityBorder"
Property="Opacity"
Value="0" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
本小节我想要表达的主要思想是,ContentControl
的ControlTemplate
可以定义一个ContentPresenter
对象,其视觉效果将由ContentControl
的DataTemplate
决定。
基于 MVVM 的模式
现在我们终于要讨论基于MVVM的模式了。
单选视图模型模式
我喜欢基本的 WPF 控件,例如 ContentControl
和 ItemsControl
。我对派生的 WPF 控件(例如 RadioButton
或 ListView
和 ListBox
)则不那么喜欢——在我看来,它们是在 WPF 最佳实践尚未为人所知之前创建的,并未充分利用它们。
在本小节中,我们将展示如何使用视图模型概念来创建一个具有单选功能的ItemsControl
,模拟RadioButton
的功能——即一次最多只能选择一个项目:如果一个项目已经选中,用户选择另一个项目,则第一个选中项目将变为未选中。
稍后我们将展示如何创建更通用的功能,以保持 2 个或更多最后一个项目被选中。
请注意,ListView
和ListBox
已经具备单项选择功能,但它们无法轻易地推广到保持两个或更多项目被选中。
另请注意,由于项目选择功能是在非可视化视图模型中定义的,因此我们可以轻松地对其进行单元测试,如下所示。
包含代码的解决方案名为“UniqueSelectionPatternSample”。其主项目同名,并引用了同一解决方案中的另外两个项目:ViewModels
、TestViewModels
。该解决方案还包含 MS 单元测试项目SelectionTests
。
ViewModels
项目定义了本示例和几个其他示例的非可视化对象。在本示例中,我们将查看在ViewModels
项目中定义的接口ISelectable
和类SelectableItemBase
、SelectableItemWithDisplayName
以及CollectionWithUniqueItemSelection
。
TestViewModels
项目包含许多类,这些类表示在 ViewModels
项目中定义的集合,并填充了一些测试数据。在本示例中,我们将使用其中的 MyTestButtonsWithSelectionVM
类。
SelectionTests
项目为 ViewModels
项目下定义的非可视化类提供了单元测试。
让我们从展示示例功能开始。将UniqueSelectionPatternSample
项目设为解决方案的启动项目。然后运行解决方案。您将看到一行四个按钮。尝试单击这些按钮——您会发现当一个按钮被选中时,之前被选中的按钮会取消选中。
控制选择/取消选择的功能位于纯非可视化类CollectionWithUniqueItemSelection<T>
中,该类定义在ViewModels
项目下。此集合扩展了ObservableCollection<T>
,其泛型参数T
必须实现ISelectable
接口。
ISelectable
声明了IsItemSelected
属性和IsItemSelectedChangedEvent
事件。
public interface ISelectable { bool IsItemSelected { get; set; } event ActionItemSelectedChangedEvent; }
此接口的实现必须在IsItemSelected
属性更改后触发ItemSelectedChangedEvent
。
SelectableItemBase
是此接口的基本实现。当IsItemSelected
属性更改时,它会触发ItemSelectionChangedEvent
;它还会触发PropertyChanged
事件,以便UI可以检测到属性更改。
public class SelectableItemBase : ISelectable, INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropChanged(string propName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propName)); } public event Action<ISelectable> ItemSelectedChangedEvent; bool _isItemSelected = false; public bool IsItemSelected { get { return _isItemSelected; } set { if (_isItemSelected == value) return; _isItemSelected = value; if (ItemSelectedChangedEvent != null) ItemSelectedChangedEvent(this); OnPropChanged("IsItemSelected"); } } }
类SelectableItemWithDisplayName
通过添加DisplayName
属性和ToggleSelection()
方法来扩展SelectableItemBase
。此方法简单地切换项目的选择状态。
public class SelectableItemWithDisplayName : SelectableItemBase { public SelectableItemWithDisplayName(string displayName) { DisplayName = displayName; } public string DisplayName { get; private set; } public void ToggleSelection() { this.IsItemSelected = !this.IsItemSelected; } }
现在让我们仔细看看CollectionWithUniqueItemSelection<T>
类。它定义了SelectedItem
属性,该属性应包含当前选定的项目。如果集合中没有选定的项目,则此属性应为null。当SelectedItem
设置为集合中的某个项目时,该项目的IsItemSelected
属性将设置为true
,而先前选定的项目的IsItemSelected
属性将设置为false
。
T _selectedItem = null; public T SelectedItem { get { return _selectedItem; } set { if (_selectedItem == value) return; if (_selectedItem != null) // unselect old item { _selectedItem.IsItemSelected = false; } _selectedItem = value; if (_selectedItem != null) // select the new item { _selectedItem.IsItemSelected = true; } OnPropertyChanged(new PropertyChangedEventArgs("SelectedItem")); } }
每当向集合中添加项目时,我们都会将方法item_ItemSelectedChagnedEvent(ISelectable item)
添加为该项目的ItemSelectedChangedEvent
的处理程序。每当从集合中删除项目时,我们都会删除相应的处理程序。添加处理程序通过ConnectItems
方法实现。
void ConnectItems(IEnumerable items) { if (items == null) return; foreach (T item in items) { item.ItemSelectedChangedEvent += item_ItemSelectedChangedEvent; } }
此方法由Init()
方法(用于集合中的原始项目)调用。
void Init() { ConnectItems(this); this.CollectionChanged += CollectionWithUniqueItemSelection_CollectionChanged; }
它也会在CollectionChanged
事件的处理程序中为集合中的新项目调用。
void CollectionWithUniqueItemSelection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { DisconnectItems(e.OldItems); ConnectItems(e.NewItems); }
DisconnectItems(IEnumerable items)
方法用于断开从集合中移除的项目的处理程序。
void DisconnectItems(IEnumerable items) { if (items == null) return; foreach (T item in items) { item.ItemSelectedChangedEvent -= item_ItemSelectedChangedEvent; } }
如上所示,它由CollectionWithUniqueItemSelection_CollectionChanged
方法(它是CollectionChanged
事件的处理程序)调用,以断开从集合中移除的项目的连接。
方法item_ItemSelectedChangedEvent(ISelectable item)
(被指定为集合中每个项目的IsItemSelectedChangedEvent
处理程序)负责为当前选中的项目设置SelectedItem
属性。
void item_ItemSelectedChangedEvent(ISelectable item) { if (item.IsItemSelected) { this.SelectedItem = (T)item; } else { this.SelectedItem = null; } }
因此,在CollectionWithUniqueItemSelection
类中选择选中项有两种等效方式。
- 通过设置相应项目的
IsItemSelected
属性。 - 通过将集合的
SelectedItem
属性设置为相应的项目。
此外,这两种方法是完全等效的——如果您使用上述任一方法选择(或取消选择)一个项目,则另一种方法的条件也将满足。为了验证其真实性,我在SelectionTests
项目中创建了一个单元测试UniqueSelectionTests
类。
在类的构造函数中,我们创建了一个CollectionWithUniqueItemSelection<SelectableItemWithDisplayName>
的_testCollection
,并用三个DisplayName
为“1”、“2”和“3”的项填充了它。
public UniqueSelectionTests() { _testCollection = new CollectionWithUniqueItemSelection<SelectableItemWithDisplayName>(); _testCollection.Add(new SelectableItemWithDisplayName("1")); _testCollection.Add(new SelectableItemWithDisplayName("2")); _testCollection.Add(new SelectableItemWithDisplayName("3")); }
方法TestAllUnselected
验证集合中没有选定项目——它检查SelectedItem
属性是否设置为null,并且集合中所有单独项目的IsItemSelected
属性是否设置为false。
// test that no item is selected void TestAllUnselected() { // testing for no items selected Assert.IsNull(_testCollection.SelectedItem); // test that all items is the collected have IsItemSelected property // set to false. foreach (SelectableItemWithDisplayName selectableItem in _testCollection) { Assert.IsFalse(selectableItem.IsItemSelected); } return; }
方法TestSelected(string selectedItemDisplayName)
测试具有相应显示名称的项是否被选中,并且只有该项被选中——它检查SelecteItem
是否具有所需的显示名称,并且只有具有该显示名称的项的IsItemSelected
属性设置为true
,而其余项的该属性设置为false
。
// test that item with the specified display name is selected void TestSelected(string selectedItemDisplayName) { Assert.IsNotNull(_testCollection.SelectedItem); Assert.AreEqual(_testCollection.SelectedItem.DisplayName, selectedItemDisplayName); Assert.IsTrue(_testCollection.SelectedItem.IsItemSelected); foreach (SelectableItemWithDisplayName selectableItem in _testCollection) { if (selectedItemDisplayName.Equals(selectableItem.DisplayName)) { Assert.IsTrue(selectableItem.IsItemSelected); } else { Assert.IsFalse(selectableItem.IsItemSelected); } } }
负责选择项目的方法有两个,负责取消选择项目的方法也有两个——每对中的一个对应于通过SelectedItem
属性进行选择(或取消选择),另一个则通过IsItemSelected
标志进行。
// select the specified item by setting the testCollection's SelectedItem // property to that item. void SelectItemViaSettingSelectedItem(string itemDisplayName) { SelectableItemWithDisplayName itemToSelect = FindItemByDisplayName(itemDisplayName); _testCollection.SelectedItem = itemToSelect; } // unselect the currently selected item by // setting the test collection's selected item to null void UnselectItemViaUnsettingSelectedItem() { _testCollection.SelectedItem = null; } // select the specified item by setting its IsItemSelected property to true. void SelectItemViaFlag(string itemDisplayName) { SelectOrUnselectItemViaFlag(itemDisplayName, true); } // unselect the specified item by setting its IsItemSelected property to false. void UnselectItemViaFlag(string itemDisplayName) { SelectOrUnselectItemViaFlag(itemDisplayName, false); }
IsItemSelected
标志更改函数调用SelectOrUnselectItemViaFlag
实用方法,该方法又调用FindItemByDisplayName
实用方法。
SelectableItemWithDisplayName FindItemByDisplayName(string itemDisplayName) { SelectableItemWithDisplayName itemToSelect = _testCollection. Where((item) => (item as SelectableItemWithDisplayName).DisplayName.Equals(itemDisplayName)).First(); return itemToSelect; } // select or unselect the specified item by setting its IsItemSelected // property to true or false. void SelectOrUnselectItemViaFlag(string itemDisplayName, bool selectOrUnselect) { SelectableItemWithDisplayName itemToSelect = FindItemByDisplayName(itemDisplayName); itemToSelect.IsItemSelected = selectOrUnselect; }
“主要”测试方法是TestSelectingItem()
,它调用各种选择/取消选择方法,然后调用测试相应项目确实已选择(或未选择)的方法。
[TestMethod] public void TestSelectingItem() { TestAllUnselected(); SelectItemViaFlag("2"); TestSelected("2"); SelectItemViaFlag("1"); TestSelected("1"); UnselectItemViaFlag("1"); TestAllUnselected(); SelectItemViaSettingSelectedItem("2"); TestSelected("2"); SelectItemViaSettingSelectedItem("3"); TestSelected("3"); UnselectItemViaUnsettingSelectedItem(); TestAllUnselected(); }
您可以通过右键单击其声明并选择“运行测试”或“调试测试”选项来运行此方法。
最后,让我们谈谈“主要”的UniqueSelectionPatternSample
项目。其所有代码都位于MainWindow.xaml文件中。它使用MyTestButtonsWithSelectionVM
对象(在TestViewModels
项目定义)作为其视图模型。
<testVMs:MyTestButtonsWithSelectionVM x:Key="TheTestButtonsWithSelectionVM" />
MyTestButtonsWithSelectionVM
只是一个CollectionWithUniqueItemSelection<SelectableItemWithDisplayName>
,它用四个项填充自身。
public class MyTestButtonsWithSelectionVM : CollectionWithUniqueItemSelection<SelectableItemWithDisplayName> { public MyTestButtonsWithSelectionVM() { Add(new SelectableItemWithDisplayName("Button 1")); Add(new SelectableItemWithDisplayName("Button 2")); Add(new SelectableItemWithDisplayName("Button 3")); Add(new SelectableItemWithDisplayName("Button 4")); } }
回到 MainWindow.xaml 文件——让我们看看代表可选按钮的 ItemsControl
。
<ItemsControl x:Name="SingleButtonSelectionControl"
ItemsSource="{StaticResource TheTestButtonsWithSelectionVM}"
ItemTemplate="{StaticResource ItemButtonDataTemplate}"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<!-- arrange the items horizontally -->
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<!-- make sure that each item has margin 5 pixels to the left and right of it-->
<Style TargetType="FrameworkElement">
<Setter Property="Margin"
Value="5,0" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
ItemControl
的ItemsSource
属性被设置为TheTestButtonsWithSelectionVM
资源对象(包含TestButtonsWithSelectionVM
集合),其ItemTemplate
属性指向ItemButtonDataTemplate
资源(稍后将讨论)。
这是ItemsControl
中单个项目的DataTemplate
<DataTemplate x:Key="ItemButtonDataTemplate">
<Grid Background="Transparent"
x:Name="ItemPanel">
<Border x:Name="TheItemBorder"
Background="Black"
BorderBrush="White"
BorderThickness="1">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="White"
Text="{Binding Path=DisplayName}"
Margin="10, 5" />
</Border>
<Border x:Name="TheOpacityBorder"
Background="White"
Opacity="0.5" />
<i:Interaction.Triggers>
<!-- Call ToggleSelection method when when MouseDown event
is fired on the item -->
<i:EventTrigger EventName="MouseDown">
<ei:CallMethodAction MethodName="ToggleSelection"
TargetObject="{Binding Path=DataContext, ElementName=ItemPanel}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Grid>
<DataTemplate.Triggers>
<!-- make TheOpacityBorder less opaque when the mouse is over-->
<Trigger Property="IsMouseOver"
Value="True">
<Setter TargetName="TheOpacityBorder"
Property="Opacity"
Value="0.3" />
</Trigger>
<DataTrigger Binding="{Binding Path=IsItemSelected}"
Value="True">
<!-- make TheOpacityBorder completely transparent when the
item is selected-->
<Setter TargetName="TheOpacityBorder"
Property="Opacity"
Value="0" />
<!-- make the ItemPanel non-responsive to the event when
the item is selected (this is to prevent unselection
when clicking on the same item again) -->
<Setter TargetName="ItemPanel"
Property="IsHitTestVisible"
Value="False" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
可视化项本身由一个Border
(名为TheItemBorder
)对象组成,其中包含一个TextBlock
对象。TextBlock
的Text
属性绑定到其自己的DataContext
的DisplayName
属性(其中包含集合中的相应项)。还有一个TheOpacityBorder
Border
覆盖在TheItemBorder
对象上。默认情况下,其不透明度为0.5。DataTemplate
的触发器确保当鼠标悬停在可视化项上时,其不透明度降低到0.3,然后当项被选中时,其不透明度进一步降低到0。使用MS Expression Blend SDK管道,我们确保每次单击项时,都会在相应项的视图模型对象上调用ToggleSelection()
方法。
还有一个触发器,用于在选中项时将 IsHitTestVisible
值设置为 False
——这样做是为了防止单击时取消选中项——以模拟 RadioButton
的功能。
使用自定义控件实现单选
上面的示例从零开始创建ItemTemplate
来实现单选。这可能会给代码复用带来一些问题。例如,如果我们想将上面的DataTemplate
复用于不同的视图模型,新的视图模型也必须具有相同的属性和方法名称,即我们想要在项中显示的任何文本都应由DisplayName
属性提供,选择状态应由IsItemSelected
属性提供,并且它需要一个ToggleSelection()
方法来更改选择状态。
与其从头开始创建此类模板,不如使用具有反映要显示的文本和选择状态的属性的自定义控件。即使我们使用具有不同名称的类似属性的视图模型,我们所需要做的也只是更改此类控件上的绑定以供属性使用。
我们上面展示了如何创建MyToggleButton
自定义控件。该控件可以直接用于我们的单选实现。它的Content
属性可以绑定到视图模型的DisplayName
属性以显示其中包含的文本,而它的IsChecked
属性可以绑定到视图模型的IsItemSelected
属性。
展示如何使用MyToggleButton
自定义控件实现单选的解决方案名为“UniqueSelectionWithCustomControl”。其主项目(同名)与上述项目唯一的区别在于MainWindow.xaml
文件的Window.Resources
部分中TheItemButtonDataTemplate
的构建方式。
<DataTemplate x:Key="ItemButtonDataTemplate">
<customControls:MyToggleButton x:Name="TheToggleButton"
Content="{Binding Path=DisplayName}"
Foreground="White"
Background="Black"
IsChecked="{Binding Path=IsItemSelected, Mode=TwoWay}"
Style="{StaticResource ToggleButton_ButtonStyle}"/>
<DataTemplate.Triggers>
<Trigger SourceName="TheToggleButton"
Property="IsChecked"
Value="True">
<Setter Property="IsHitTestVisible"
Value="False" />
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
MyToggleButton
类为其Content
属性绑定到DisplayName
,其IsChecked
属性绑定到视图模型对象的IsItemSelected
属性,从而为可选按钮提供了充分的实现。
运行此项目时,您将获得与上一个示例完全相同的视觉效果和行为。
现在将MyToggleButton
的样式更改为MyToggleButtonCheckBoxStyle
。您将看到复选框而不是按钮,具有相同的单选原则——当一个复选框被选中时,之前选中的复选框将被取消选中。
请注意,将可选按钮替换为复选框是通过更改 XAML 文件中的一个单词来实现的。
非可视化行为模式
在WPF 控件模式。(WPF 和 XAML 代码复用模式简易示例。第 1 部分)中,我们举了一个 WPF 控件行为模式的例子,作为一种非侵入性修改控件行为的方式。在这里,我们将介绍非可视化行为,作为一种(几乎)非侵入性修改非可视化对象行为的方式。
请注意,我们在前一个示例中用于创建非可视化单选的CollectionWithUniqueItemSelection<T>
类,有大量代码专门用于单选功能。事实上,几乎所有代码(约90行)都在处理单选。行为将允许将所有这些代码从类中移出到行为类中。这样我们就可以使用相同的集合类,但具有不同的行为,从而产生完全不同的选择算法。
单选行为
NonVisualBehaviorsSample 解决方案演示了如何创建用于单选的行为。我们不再使用 CollectionWithUniqueSelection<T>
作为我们的视图模型集合,而是使用 SelectableItemCollection<T>
。它比 CollectionWithUniqueSelection<T>
简单得多。与 CollectionWithUniqueSelection<T>
一样,它扩展了 ObservableCollection<T>
,但其功能仅包含一个类型为 IBehavior
的属性 TheBehavior
。此属性用于将行为附加到集合。
public class SelectableItemCollection<T> : ObservableCollection<T> where T : class, ISelectable { IBehavior _behavior = null; public IBehavior TheBehavior { get { return _behavior; } set { if (_behavior == value) return; if (_behavior != null) // if not null detach old selection behavior _behavior.OnDetach(); _behavior = value; if (_behavior != null) // if new selection behavior is not null, attach it _behavior.OnAttach(this); } } }
IBehavior
是一个非常简单的接口,在同一个项目ViewModels
中定义。
public interface IBehavior { void OnAttach(IEnumerable collectionToAttachTo); void OnDetach(); }
大部分选择行为功能都位于SelectionBehaviorBase<T>
类中。本文中介绍的两种选择行为都派生自该类。
此类别确保所附加行为的集合中每个ISelectable
项目的ItemSelectedChangedEvent
都由OnItemSelectionChanged(ISelectable item)
函数处理。如果相应的项目从集合中移除,它还会负责移除事件处理程序。
与CollectionWithUniqueSelection<T>
类似,移除和添加事件处理程序的功能由方法DisconnectItems(IEnumerable items)
和ConnectItems(IEnumerable items)
提供。
void DisconnectItems(IEnumerable items) { if (items == null) return; foreach (ISelectable item in items) { item.ItemSelectedChangedEvent -= OnItemSelectionChanged; } } void ConnectItems(IEnumerable items) { if (items == null) return; foreach (ISelectable item in items) { item.ItemSelectedChangedEvent += OnItemSelectionChanged; } }
当行为的TheCollection
属性被重置时,这些方法会被调用。
ObservableCollection_collection = null; ObservableCollection TheCollection { get { return _collection; } set { if (_collection == value) return; if (_collection != null) { _collection.CollectionChanged -= _collection_CollectionChanged; } // disconnect event handlers from the items in the old collection DisconnectItems(_collection); _collection = value; // connect the event handlers to the items in the new collection ConnectItems(_collection); if (_collection != null) { _collection.CollectionChanged += _collection_CollectionChanged; } } }
它们也会为从集合中移除或添加到集合中的项目调用。
void _collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { // disconnect the event handlers from the items removed from the collection DisconnectItems(e.OldItems); // connect the event handlers to the items added to the collection ConnectItems(e.NewItems); }
IBehavior
方法OnAttach(IEnumerable selectableItemCollectionToAttachTo)
和OnDetach()
简单地设置行为的TheCollection
属性。
public void OnAttach(IEnumerable selectableItemCollectionToAttachTo) { if (TheCollection != null) throw new Exception("Programming Error: Selection Behavior cannot attach to more than one collection"); TheCollection = selectableItemCollectionToAttachTo as ObservableCollection; } public void OnDetach() { TheCollection = null; }
OnItemSelectionChanged(ISelectableItem item)
函数提供的选择事件处理程序未在SelectionBehaviorBase<T>
类中实现——它被声明为一个抽象方法,将在子类中重写。
UniqueSelectionBehavior<T>
类扩展了SelectionBehaviorBase<T>
,提供了单项选择功能。它的SelectedItem
属性允许通过选择一个项目进行选择(就像CollectionWithUniqueSelection<T>
一样),而其OnItemSelectionChanged(ISelectable item)
事件处理程序的实现允许通过更改其IsItemSelected
属性来选择或取消选择一个项目。
public class UniqueSelectionBehavior<T> : SelectionBehaviorBase<T> where T : class, ISelectable { T _selectedItem = null; public T SelectedItem { get { return _selectedItem; } set { if (_selectedItem == value) return; if (_selectedItem != null) { _selectedItem.IsItemSelected = false; } _selectedItem = value; if (_selectedItem != null) { _selectedItem.IsItemSelected = true; } OnPropChanged("SelectedItem"); } } protected override void OnItemSelectionChanged(ISelectable item) { if (item.IsItemSelected) { this.SelectedItem = (T)item; } else { this.SelectedItem = null; } } }
作为项目集合的视图模型,我们使用在TestViewModels
项目中定义的MyTestButtonsWithSelectionAndBehavior
类。它扩展了SelectableItemCollection<SelectableItemWithDisplayName>
集合,并向其中添加了四个项目。
public class MyTestButtonsWithSelectionAndBehavior : SelectableItemCollection<SelectableItemWithDisplayName> { public MyTestButtonsWithSelectionAndBehavior() { Add(new SelectableItemWithDisplayName("Button 1")); Add(new SelectableItemWithDisplayName("Button 2")); Add(new SelectableItemWithDisplayName("Button 3")); Add(new SelectableItemWithDisplayName("Button 4")); } }
以下是在主项目的MainWindow.xaml
文件中定义行为和集合的方式:
<!-- define the selection behavior -->
<viewModels:UniqueSelectionBehaviorOnSelectableWithDisplayName x:Key="TheUniqueSelectionBehavior" />
<!-- define the collection and assign the behavior to it -->
<testViewModels:MyTestButtonsWithSelectionAndBehavior x:Key="TheButtonsWithSelectionBehaviorVM"
TheBehavior="{StaticResource TheUniqueSelectionBehavior}"/>
MainWindow.xaml
文件的其余部分与上一个示例几乎相同,除了我们使用“TheButtonsWithSelectionBehaviorVM”集合作为ItemsControl
的ItemsSource
。
结果,我们获得了与上一个示例相同的单选按钮行为。
最后两个项目选择行为
此示例对同一集合实现了不同的行为。根据此行为,我们允许集合中最多同时选中两个项目。当用户选择第三个项目时,选中时间最长的项目将取消选中。
此示例位于“TwoLastItemsSelectionBehaviorSample”解决方案中。除了行为之外,其代码与上一个示例几乎相同。我们使用TwoLastItemSelectionBehaviorOnSelectableWithDisplayName
,而不是UniqueSelectionBehaviorOnSelectableWithDisplayName
,后者派生自TwoLastItemsSelectionBehavior<SelectableItemWithDisplayName>
类。
所有行为代码都位于超类TwoLastItemsSelectionBehavior<T>
中。就像UniqueSelectionBehavior<T>
一样,它派生自SelectionBehaviorBase<T>
并重写了OnItemSelectionChanged(ISelectable item)
方法(提醒一下——每当集合中任何项目的IsItemSelected
属性发生变化时,都会调用此方法)。
TwoLastItemsSelectionBehavior<T>
有一个List<T>
字段_selectedItems
,它控制选择和取消选择。它包含当前选定的项目,其中选择时间越长的项目越靠近集合的末尾。控制向_selectedItems
集合添加新项目的方法是AddSelectedItem(T itemToAdd)
。
// collection of selected items List<T> _selectedItems = new List<T>(); // returns the number of selected items int NumberSelectedItems { get { return _selectedItems.Count; } } // if the number of selected items within _selectedItems // collection is 2, unselect the item that has been selected the longest // (last item within the collection) // add a newly selected item to beginning of _selectedItems collection. void AddSelectedItem(T itemToAdd) { if (NumberSelectedItems >= 2) { // if the current number of selected item is 2 (or greater - which cannot happen) // remove the last item within the _selecteItems collection T itemToRemove = _selectedItems.Last(); _selectedItems.Remove(itemToRemove); // remove last item // set the IsItemSelected property on the // removed item to false itemToRemove.IsItemSelected = false; } // insert the newly selected item // at the beginning of the collection _selectedItems.Insert(0, itemToAdd); }
AddSelectedItem(T itemToAdd)
由OnItemSelectionChanged(ISelectable item)
方法调用。
protected override void OnItemSelectionChanged(ISelectable item) { if (!item.IsItemSelected) { // if item is no longer selected, remove it from _selected items collection _selectedItems.Remove((T)item); } else { // if the item is selected, add it to the _selectedItems collection AddSelectedItem((T)item); } OnPropChanged("SelectedItems"); OnPropChanged("NumberSelectedItems"); }
现在,MainWindow.xaml 文件(与上一个示例相比)的唯一微小更改是定义 TwoLastItemSelectionBehaviorOnSelectableWithDisplayName
对象,并将其用作 MyTestButtonsWithSelectionAndBehavior
对象上的行为,而不是 UniqueSelectionBehaviorOnSelectableWithDisplayName
。
<viewModels:TwoLastItemSelectionBehaviorOnSelectableWithDisplayName x:Key="TheTwoLastItemsSelectionBehavior" />
<testViewModels:MyTestButtonsWithSelectionAndBehavior x:Key="TheButtonsWithSelectionBehaviorVM"
TheBehavior="{StaticResource TheTwoLastItemsSelectionBehavior}" />
当我们运行示例时,我们将获得所需的行为——如果选中两个按钮,然后单击另一个按钮,则选中时间最长的按钮将取消选中。
请注意,我们更改了行为,而没有更改视图或视图模型,只更改了附加到视图模型的行为。因此,我们展示了如何以最小的侵入性修改项目集合的行为。
另请注意,只需更改切换按钮的样式/模板,我们就可以使用复选框生成相同的行为。
上述是通过将MainWindow.xaml文件中定义的ItemButtonDataTemplate
中对ToggleButton_ButtonStyle
的引用替换为对MyToggleButtonCheckBoxStyle
的引用来实现的。
我们可以很容易地将2个最后项目的选择泛化为N个最后项目的选择,只需将行为中的数字2替换为一个整数参数。此外,我们可以很容易地基于相同的原理创建更复杂的选择行为。
我使用了“最后两个项目选择行为”来在同一个 Dev Express 的 ChartControl
对象中显示多个图表,对应于两个不同的 Y 轴——一个在左边,一个在右边。如果您有超过 2 个图表,您可以使用“最后两个项目选择原则”,允许用户选择其中任意两个进行显示。
递归数据模板
许多概念,无论是在现实世界还是在软件开发中,都具有递归树结构,其中项目可以用树节点表示——每个节点可能具有多个子节点和一个父节点。没有子节点的节点称为树的叶子。只有一个没有父节点的节点,它被称为树的根。
此类树的示例包括文件系统、组织内部的报告结构、WPF 的逻辑树和可视树。
事实证明,视图中的此类树可以很容易地由引用自身的 WPF DataTemplates 表示——即递归数据模板。
递归数据模板示例位于 RecursiveDatateTemplatesSample 解决方案中,主项目同名。如果您运行解决方案并展开其所有节点,您将看到以下内容:
您可以看到,这是一个模拟文件系统(与您机器上的文件系统无关)的树形表示。它有一个名为 Root 的文件夹,其中包含 Documents 和 Pictures 两个文件夹以及文件 MySystemFile。Documents 文件夹包含 Document1 和 Document2 两个文件,而 Picture 文件夹包含 Picture1 和 Picture2。
如您所见,我们并没有使用 WPF 的 TreeView
控件来显示此文件系统。
首先,让我们解释代表此树结构的视图模型。
ViewModels
项目下的TreeNodeBase
类是树节点的基本类。它具有Parent
和Children
属性,分别表示树节点的父节点和子节点。它还有一个HasChildren
属性,如果Children
集合为null或为空,则为false
,否则为true
。
public class TreeNodeBase : INotifyPropertyChanged { #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; #endregion protected void OnPropertyChanged(string propertyName) { if (this.PropertyChanged != null) { this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } #region Parent Property private TreeNodeBase _parent; public TreeNodeBase Parent { get { return this._parent; } set { if (this._parent == value) { return; } this._parent = value; this.OnPropertyChanged("Parent"); } } #endregion Parent Property #region Children Property private ObservableCollection_children; public ObservableCollection Children { get { return this._children; } set { if (this._children == value) { return; } if (this._children != null) { this._children.CollectionChanged -= _children_CollectionChanged; } this._children = value; if (this._children != null) { this._children.CollectionChanged += _children_CollectionChanged; } this.OnPropertyChanged("Children"); this.OnPropertyChanged("HasChildren"); } } void _children_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { // collection changed, so fire the PropertyChanged event for HasChildren property. this.OnPropertyChanged("HasChildren"); } #endregion Children Property public bool HasChildren { get { return (Children != null) && (Children.Count > 0); } } }
类ViewModels.FileTreeNode
派生自TreeNodeBase
。
public class FileTreeNode : TreeNodeBase { // true for a file, false for a folder public bool FileOrFolder { get; set; } // name of the file or folder public string Name { get; set; } // adds a new child and returns it public FileTreeNode AddChild(bool fileOrFolder, string name) { if (this.FileOrFolder) throw new Exception("Cannot add a child to a file"); if (this.Children == null) { this.Children = new ObservableCollection(); } FileTreeNode newNode = new FileTreeNode { FileOrFolder = fileOrFolder, Name = name, Parent = this }; Children.Add(newNode); return newNode; } #region IsExpanded Property private bool _isExpanded = false; public bool IsExpanded { get { return this._isExpanded; } set { if (this._isExpanded == value) { return; } this._isExpanded = value; this.OnPropertyChanged("IsExpanded"); } } #endregion IsExpanded Property }
它有一个布尔属性FileOrFolder
,当节点是文件时为 true,当节点是文件夹时为 false。它还有一个Name
属性,对应于文件或文件夹的名称。最后,它定义了IsExpanded
属性,我们用它来指定相应的节点是否展开(您可以看到它的子节点)。
还有一个public FileTreeNode AddChild(bool fileOrFolder, string name)
方法,允许向当前节点添加子节点,并指定子节点的FileOrFolder
和Name
属性。它返回子节点本身,以便可以向新创建的子节点添加子节点。
此示例的视图模型由TestViewModels
项目下定义的MySimpleFileSystem
类构建。它派生自FileTreeNode
类。MySimpleFileSystem
在其构造函数中创建文件系统树。它将自己的名称设置为“Root”,并将其标记为文件夹。然后它向其中添加“Documents”和“Pictures”文件夹以及文件“MySystemFile”。最后,它填充了其每个子文件夹。
public MySimpleFileSystem() { this.Name = "Root"; this.FileOrFolder = false; // root folder; // create the docFolder object FileTreeNode docFolder = this.AddChild(false, "Documents"); // create the pictureFolder object FileTreeNode pictureFolder = this.AddChild(false, "Pictures"); // create the systemFile object FileTreeNode systemFile = this.AddChild(true, "MySystemFile"); // add two child files Document1 and Document2 to the docFolder object docFolder.AddChild(true, "Document1"); docFolder.AddChild(true, "Document2"); // add two child files Picture1 and Picture2 to the pictureFolder object pictureFolder.AddChild(true, "Picture1"); pictureFolder.AddChild(true, "Picture2"); }
此示例的视觉效果定义在RecursiveDataTemplateSample
主项目的MainWindow.xaml文件中。我们在此文件中将树视图模型定义为Window
的资源。
<!-- The tree-like View Model-->
<testViewModels:MySimpleFileSystem x:Key="TheFileSystem" />
递归数据模板在同一个文件中定义,名为TheFileRepresentationDataTemplate。
<DataTemplate x:Key="TheFileTreeRepresentationDataTemplate"
DataType="viewModels:FileTreeNode">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Left">
<!-- Expander toggle button, it only visible for the items that have children -->
<customControl:MyToggleButton Style="{StaticResource ToggleButtonExpanderStyle}"
IsChecked="{Binding Path=IsExpanded, Mode=TwoWay}"
Visibility="{Binding Path=HasChildren,
Converter={StaticResource TheBooleanToVisibilityConverter}}" />
<!--name of the file or folder-->
<TextBlock Margin="10,0,0,0"
Text="{Binding Name}" />
</StackPanel>
<!-- This is the ItemsControl that represents the children of the current node
It is only visible if the current node is expanded-->
<!-- Note that ItemTemplate of the ItemsControl is set to the
DataTemplate we are currently in: TheFileTreeRepresentation.
This is a recursive template.
-->
<ItemsControl Margin="30,0,0,0"
ItemsSource="{Binding Children}"
ItemTemplate="{DynamicResource TheFileTreeRepresentationDataTemplate}"
Visibility="{Binding Path=IsExpanded, Converter={StaticResource TheBooleanToVisibilityConverter}}" />
</StackPanel>
</DataTemplate>
请注意,当前节点的Children
的ItemsControl
引用了相同的数据模板——TheFileTreeRepresentationDataTemplate:ItemTemplate="{DynamicResource TheFileTreeRepresentationDataTemplate}"
。我们在这里被迫使用DynamicResource
,因为DataTemplate
引用了自身。
最后,我们通过使用ContentControl
将DataTemplate
和视图模型“结合”起来。
<ContentControl ContentTemplate="{StaticResource TheFileTreeRepresentationDataTemplate}"
Content="{StaticResource TheFileSystem}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="20,20,0,0"/>
请注意,相同的递归DataTemplate
模式可用于比示例中所示复杂得多的控件。例如,我曾用它来显示大型组织的完整组织结构图。
结论
在本文中,我们介绍了可用于创建模仿由非可视化对象定义的行为的视觉元素的模式——视图模仿视图模型的行为。下一篇文章将讨论更复杂的(架构)模式。