这是我们在生产代码中的做法。
每个类别都知道它的子类别是什么。如果它们来自数据库或磁盘文件,则数据库/Web 服务方法/文件读取器/任何东西都会返回类似的类,并且您将创建与之匹配的视图模型。 viewmodel 理解信息的结构,但不知道也不关心实际的内容;由其他人负责。
请注意,这都是非常声明性的:唯一的循环是伪造演示对象的循环。没有事件处理程序,除了创建视图模型并告诉它用假数据填充自己之外,代码隐藏中没有任何内容。在现实生活中,您经常会为特殊情况(例如拖放)编写事件处理程序。将视图特定的逻辑放在代码隐藏中没有什么非 MVVMish 的。这就是它的用途。但是这个案例太琐碎了,没有必要。我们有许多 .xaml.cs 文件在 TFS 中已经存在多年,与向导创建它们时完全一样。
viewmodel 属性是很多样板文件。我有 sn-ps (steal them here) 来生成这些,带有#regions 和所有内容。其他人复制粘贴。
通常您会将每个视图模型类放在一个单独的文件中,但这是示例代码。
它是为 C#6 编写的。如果您使用的是早期版本,我们可以将其更改为适合,请告诉我。
最后,在某些情况下,考虑让一个组合框(或其他)过滤另一个大型项目集合而不是导航树更有意义。以这种分层格式执行此操作几乎没有意义,尤其是在“类别”:“子类别”关系不是一对多的情况下。
在这种情况下,我们会有一个“类别”的集合和一个所有“子类别”的集合,它们都作为主视图模型的属性。然后我们将使用“类别”选择来过滤“子类别”集合,通常通过CollectionViewSource。但是您也可以为视图模型提供一个包含所有“子类别”的私有完整列表,并与称为FilteredSubCategories 之类的公共ReadOnlyObservableCollection 配对,您可以将其绑定到第二个组合框。当“类别”选择更改时,您将根据 SelectedCategory 重新填充 FilteredSubCategories。
底线是编写反映数据语义的视图模型,然后编写视图让用户看到他需要看到的内容并做他需要做的事情。视图模型不应该知道视图的存在;他们只是公开信息和命令。能够编写以不同方式或不同细节级别显示相同视图模型的多个视图通常很方便,因此可以将视图模型视为只是中性地公开任何人可能想要使用的有关其自身的任何信息。应用通常的因式分解规则:尽可能松散地耦合(但不能再松散地),等等。
ComboDemoViewModels.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace ComboDemo.ViewModels
{
public class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] String propName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
#endregion INotifyPropertyChanged
}
public class ComboDemoViewModel : ViewModelBase
{
// In practice this would probably have a public (or maybe protected) setter
// that raised PropertyChanged just like the other properties below.
public ObservableCollection<CategoryViewModel> Categories { get; }
= new ObservableCollection<CategoryViewModel>();
#region SelectedCategory Property
private CategoryViewModel _selectedCategory = default(CategoryViewModel);
public CategoryViewModel SelectedCategory
{
get { return _selectedCategory; }
set
{
if (value != _selectedCategory)
{
_selectedCategory = value;
OnPropertyChanged();
}
}
}
#endregion SelectedCategory Property
public void Populate()
{
#region Fake Data
foreach (var x in Enumerable.Range(0, 5))
{
var ctg = new ViewModels.CategoryViewModel($"Category {x}");
Categories.Add(ctg);
foreach (var y in Enumerable.Range(0, 5))
{
ctg.SubCategories.Add(new ViewModels.SubCategoryViewModel($"Sub-Category {x}/{y}"));
}
}
#endregion Fake Data
}
}
public class CategoryViewModel : ViewModelBase
{
public CategoryViewModel(String name)
{
Name = name;
}
public ObservableCollection<SubCategoryViewModel> SubCategories { get; }
= new ObservableCollection<SubCategoryViewModel>();
#region Name Property
private String _name = default(String);
public String Name
{
get { return _name; }
set
{
if (value != _name)
{
_name = value;
OnPropertyChanged();
}
}
}
#endregion Name Property
// You could put this on the main viewmodel instead if you wanted to, but this way,
// when the user returns to a category, his last selection is still there.
#region SelectedSubCategory Property
private SubCategoryViewModel _selectedSubCategory = default(SubCategoryViewModel);
public SubCategoryViewModel SelectedSubCategory
{
get { return _selectedSubCategory; }
set
{
if (value != _selectedSubCategory)
{
_selectedSubCategory = value;
OnPropertyChanged();
}
}
}
#endregion SelectedSubCategory Property
}
public class SubCategoryViewModel : ViewModelBase
{
public SubCategoryViewModel(String name)
{
Name = name;
}
#region Name Property
private String _name = default(String);
public String Name
{
get { return _name; }
set
{
if (value != _name)
{
_name = value;
OnPropertyChanged();
}
}
}
#endregion Name Property
}
}
MainWindow.xaml
<Window
x:Class="ComboDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ComboDemo"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel Orientation="Vertical" Margin="4">
<StackPanel Orientation="Horizontal">
<Label>Categories</Label>
<ComboBox
x:Name="CategorySelector"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
DisplayMemberPath="Name"
MinWidth="200"
/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="20,4,4,4">
<Label>Sub-Categories</Label>
<ComboBox
ItemsSource="{Binding SelectedCategory.SubCategories}"
SelectedItem="{Binding SelectedCategory.SelectedSubCategory}"
DisplayMemberPath="Name"
MinWidth="200"
/>
</StackPanel>
</StackPanel>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
namespace ComboDemo
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var vm = new ViewModels.ComboDemoViewModel();
vm.Populate();
DataContext = vm;
}
}
}
额外积分
这是 MainWindow.xaml 的不同版本,它演示了如何以两种不同的方式显示相同的视图模型。请注意,当您在一个列表中选择一个类别时,会更新SelectedCategory,然后会反映在另一个列表中,SelectedCategory.SelectedSubCategory 也是如此。
<Window
x:Class="ComboDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ComboDemo"
xmlns:vm="clr-namespace:ComboDemo.ViewModels"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
>
<Window.Resources>
<DataTemplate x:Key="DataTemplateExample" DataType="{x:Type vm:ComboDemoViewModel}">
<ListBox
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type vm:CategoryViewModel}">
<StackPanel Orientation="Horizontal" Margin="2">
<Label Width="120" Content="{Binding Name}" />
<ComboBox
ItemsSource="{Binding SubCategories}"
SelectedItem="{Binding SelectedSubCategory}"
DisplayMemberPath="Name"
MinWidth="120"
/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</Window.Resources>
<Grid>
<StackPanel Orientation="Vertical" Margin="4">
<StackPanel Orientation="Horizontal">
<Label>Categories</Label>
<ComboBox
x:Name="CategorySelector"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
DisplayMemberPath="Name"
MinWidth="200"
/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="20,4,4,4">
<Label>
<TextBlock Text="{Binding SelectedCategory.Name, StringFormat='Sub-Categories in {0}:', FallbackValue='Sub-Categories:'}"/>
</Label>
<ComboBox
ItemsSource="{Binding SelectedCategory.SubCategories}"
SelectedItem="{Binding SelectedCategory.SelectedSubCategory}"
DisplayMemberPath="Name"
MinWidth="200"
/>
</StackPanel>
<GroupBox Header="Another View of the Same Thing" Margin="4">
<!--
Plain {Binding} just passes along the DataContext, so the
Content of this ContentControl will be the main viewmodel.
-->
<ContentControl
ContentTemplate="{StaticResource DataTemplateExample}"
Content="{Binding}"
/>
</GroupBox>
</StackPanel>
</Grid>
</Window>