我自学WPF。我的窗口有两个组合框:一个用于 Categories ,另一个用于子类别。当类别选择发生变化时,我希望子类别列表更新为仅包含所选类别的子类别。
我为两个组合框创建了一个简单的视图类。我的SubcategoryView
课程'构造函数接受对我的CategoryView
类的引用,并在类别选择发生更改时附加事件处理程序。
public class SubcategoryView : INotifyPropertyChanged
{
protected CategoryView CategoryView;
public SubcategoryView(CategoryView categoryView)
{
CategoryView = categoryView;
CategoryView.PropertyChanged += CategoryView_PropertyChanged;
}
private void CategoryView_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "SelectedItem")
{
_itemsSource = null;
}
}
private ObservableCollection<TextValuePair> _itemsSource;
public ObservableCollection<TextValuePair> ItemsSource
{
get
{
if (_itemsSource == null)
{
// Populate _itemsSource
}
return _itemsSource;
}
}
}
我这样指定DataContext
。
cboCategory.DataContext = new CategoryView();
cboSubcategory.DataContext = new SubcategoryView(cboCategory.DataContext as CategoryView);
问题是在我的类别组合框中选择一个新项目不会导致子类别重新填充(即使我确认正在调用我的PropertyChanged
处理程序)。
导致列表重新填充的正确方法是什么?
另外,我欢迎任何其他关于这种方法的评论。而不是将我的CategoryView
传递给构造函数,最好在XAML中以某种方式以声明方式指示它吗?
答案 0 :(得分:3)
以下是我们在生产代码中的使用方法。
每个类别都知道它的子类别。如果它们来自数据库或磁盘文件,那么数据库/ webservice方法/文件读取器/无论什么都会像这样返回类,并且你创建了匹配的视图模型。 viewmodel理解信息的结构,但对实际内容一无所知;别人负责这个。
请注意,这都是非常声明的:唯一的循环是伪造演示对象的循环。没有事件处理程序,除了创建viewmodel并告诉它用伪数据填充自己之外,没有任何代码隐藏。在现实生活中,你经常会为特殊情况编写事件处理程序(例如拖放)。关于在代码隐藏中放置特定于视图的逻辑,没有任何非MVVMish;这就是它的用途。但这种情况对于必要的情况来说太微不足道了。我们有许多.xaml.cs
个文件,这些文件在TFS中已经存在多年,就像向导创建它们一样。
viewmodel属性是很多样板。我有片段(steal them here)来生成那些#regions和所有内容。其他人复制粘贴。
通常你将每个viewmodel类放在一个单独的文件中,但这是示例代码。
它是为C#6编写的。如果您使用的是早期版本,我们可以将其更改为适合,请告诉我。
最后,有些情况下,考虑使用一个组合框(或其他)过滤另一大项目集合,而不是导航树,这更有意义。以这种分层格式执行此操作几乎没有意义,特别是如果“类别”:“子类别”关系不是一对多关系。
在这种情况下,我们有一个“类别”集合和所有“子类别”的集合,两者都是主视图模型的属性。然后,我们将使用“类别”选项来过滤“子类别”集合,通常通过CollectionViewSource
。但是你也可以给viewmodel一个私有的所有“子类别”的完整列表,这个“子类别”与一个名为ReadOnlyObservableCollection
的公共FilteredSubCategories
配对,你将它绑定到第二个组合框。当“类别”选择发生变化时,您会根据FilteredSubCategories
重新填充SelectedCategory
。
底线是编写反映数据语义的视图模型,然后编写视图,让用户看到他需要查看的内容并执行他需要执行的操作。 Viewmodels不应该意识到存在视图;他们只是暴露信息和命令。能够编写以不同方式或不同细节级别显示相同视图模型的多个视图通常很方便,因此将视图模型视为中立地暴露任何人可能想要使用的任何有关自身的信息。通常的保理规则适用:夫妻尽可能松散(但不要松散),等等。
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的不同版本,它演示了如何以两种不同的方式显示相同的viewmodel。请注意,当您在一个列表中选择一个类别时,会更新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>
答案 1 :(得分:1)
在这种情况下使用单一视图模型非常简单,如评论中所述。例如,我只使用字符串作为组合框项目。
为了证明正确使用视图模型,我们将通过绑定而不是UI事件跟踪类别的更改。因此,除了ObservableCollection
之外,您还需要SelectedCategory
属性。
查看模型:
public class CommonViewModel : BindableBase
{
private string selectedCategory;
public string SelectedCategory
{
get { return this.selectedCategory; }
set
{
if (this.SetProperty(ref this.selectedCategory, value))
{
if (value.Equals("Category1"))
{
this.SubCategories.Clear();
this.SubCategories.Add("Category1 Sub1");
this.SubCategories.Add("Category1 Sub2");
}
if (value.Equals("Category2"))
{
this.SubCategories.Clear();
this.SubCategories.Add("Category2 Sub1");
this.SubCategories.Add("Category2 Sub2");
}
}
}
}
public ObservableCollection<string> Categories { get; set; } = new ObservableCollection<string> { "Category1", "Category2" };
public ObservableCollection<string> SubCategories { get; set; } = new ObservableCollection<string>();
}
SetProperty
的实施位置INotifyPropertyChanged
。
当您选择 category 时,SelectedCategory
属性的设置者会触发,您可以根据所选类别值填写子目录项目。 不要替换集合对象本身!您应该清除现有项目,然后添加新项目。
在xaml中,除了两个组合框ItemsSource
之外,您还需要绑定SelectedItem
category 组合框。
XAML:
<StackPanel x:Name="Wrapper">
<ComboBox ItemsSource="{Binding Categories}" SelectedItem="{Binding SelectedCategory, Mode=OneWayToSource}" />
<ComboBox ItemsSource="{Binding SubCategories}" />
</StackPanel>
然后将视图模型分配给包装器的数据上下文:
Wrapper.DataContext = new CommonViewModel();
BindableBase
的代码:
using System.ComponentModel;
using System.Runtime.CompilerServices;
public abstract class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(storage, value))
{
return false;
}
storage = value;
this.OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}