WPF中的MVVM
模型和视图模型
模型的定义经常引起激烈争论,模型和视图模型之间的界限可能会模糊不清。有些人不喜欢“污染”他们的模型与INotifyPropertyChanged接口,而是在视图模型,它确实实现了这个接口复制的模型属性。像软件开发中的许多东西一样,没有正确或错误的答案。
拆开视图
MVVM的目的是将这三个不同的区域分开 - 模型,视图模型和视图。虽然视图可以接受视图模型(VM)和(间接)模型,但MVVM最重要的规则是VM应该无权访问视图或其控件。 VM应通过公共属性公开视图所需的所有内容。 VM不应直接公开或操纵UI控件,如TextBox , Button等。
在某些情况下,这种严格的分离可能很难处理,特别是如果你需要启动并运行一些复杂的UI功能。在这里,在视图的“代码隐藏”文件中使用事件和事件处理程序是完全可以接受的。如果它纯粹是UI功能,那么无论如何都要利用视图中的事件。这些事件处理程序也可以在VM实例上调用公共方法 - 只是不要将它传递给UI控件或类似的东西。
RelayCommand
不幸的是,这个例子中使用的RelayCommand类不是WPF框架的一部分(应该是!),但几乎每个WPF开发人员的工具箱都会找到它。在线快速搜索将显示大量可以解除的代码片段,以创建自己的代码片段。
RelayCommand一个有用的替代RelayCommand是ActionCommand ,它作为Microsoft.Expression.Interactivity.Core一部分提供,它提供了类似的功能。
使用WPF和C#的基本MVVM示例
这是使用WPF和C#在Windows桌面应用程序中使用MVVM模型的基本示例。示例代码实现了一个简单的“用户信息”对话框。
View
XAML
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> Grid.RowDefinitions> <TextBlock Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Margin="4" Text="{Binding FullName}" HorizontalAlignment="Center" FontWeight="Bold"/> <Label Grid.Column="0" Grid.Row="1" Margin="4" Content="First Name:" HorizontalAlignment="Right"/> <TextBox Grid.Column="1" Grid.Row="1" Margin="4" Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/> <Label Grid.Column="0" Grid.Row="2" Margin="4" Content="Last Name:" HorizontalAlignment="Right"/> <TextBox Grid.Column="1" Grid.Row="2" Margin="4" Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/> <Label Grid.Column="0" Grid.Row="3" Margin="4" Content="Age:" HorizontalAlignment="Right"/> <TextBlock Grid.Column="1" Grid.Row="3" Margin="4" Text="{Binding Age}" HorizontalAlignment="Left"/> Grid>
和背后的代码
public partial class MainWindow : Window { private readonly MyViewModel _viewModel; public MainWindow() { InitializeComponent(); _viewModel = new MyViewModel(); //DataContext作为绑定路径的起点 DataContext = _viewModel; } } // INotifyPropertyChanged通知View属性更改,以便更新绑定。 sealed class MyViewModel : INotifyPropertyChanged { private User user; public string FirstName { get { return user.FirstName; } set { if (user.FirstName != value) { user.FirstName = value; OnPropertyChange("FirstName"); //如果名字已更改,则FullName属性也需要更新。 OnPropertyChange("FullName"); } } } public string LastName { get { return user.LastName; } set { if (user.LastName != value) { user.LastName = value; OnPropertyChange("LastName"); //如果名字已更改,则FullName属性也需要更新。 OnPropertyChange("FullName"); } } } //此属性是如何将模型属性与视图呈现方式不同的示例。 //在这种情况下,我们将出生日期转换为用户的年龄,这是只读的。 public int Age { get { DateTime today = DateTime.Today; int age = today.Year - user.BirthDate.Year; if (user.BirthDate > today.AddYears(-age)) age--; return age; } } //此属性仅用于显示目的,是现有数据的组成。 public string FullName { get { return FirstName + " " + LastName; } } public MyViewModel() { user = new User { FirstName = "John", LastName = "Doe", BirthDate = DateTime.Now.AddYears(-30) }; } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChange(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }
模型
sealed class User { public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } }
MVVM中的命令
命令用于在遵守MVVM模式的同时处理WPF中的Events 。
一个普通的EventHandler看起来像这样(位于Code-Behind ):
public MainWindow() { _dataGrid.CollectionChanged += DataGrid_CollectionChanged; } private void DataGrid_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { //做任何事 }
不在MVVM中做同样的事我们使用Commands :
<Button Command="{Binding Path=CmdStartExecution}" Content="Start" />
我建议为命令属性使用某种前缀( Cmd ),因为你将主要在xaml中使用它们 - 这样它们更容易识别。
因为它是MVVM,所以你想要在ViewModel处理该命令(For Button “eq” Button_Click )。
为此,我们基本上需要两件事:
System.Windows.Input.ICommand
RelayCommand(例如从这里获取) 。
一个简单的例子可能如下所示:
private RelayCommand _commandStart; public ICommand CmdStartExecution { get { if(_commandStart == null) { _commandStart = new RelayCommand(param => Start(), param => CanStart()); } return _commandStart; } } public void Start() { //... } public bool CanStart() { return (DateTime.Now.DayOfWeek == DayOfWeek.Monday); //Can only click that button on mondays. }
那么这个细节是做什么的:
ICommand是xaml中的Control绑定的内容。 RelayCommand将你的命令路由到Action (即调用Method )。 Null-Check只确保每个Command只会初始化一次(由于性能问题)。如果你已经阅读了上面RelayCommand的链接,你可能已经注意到RelayCommand对它的构造函数有两个重载。 (Action
这意味着你可以(附加地)添加第二个Method返回一个bool来告诉Control “事件”是否可以触发。
例如,如果Method将返回false ,那么Button s将是Enabled="false"
CommandParameters
<DataGrid x:Name="TicketsDataGrid"> <DataGrid.InputBindings> <MouseBinding Gesture="LeftDoubleClick" Command="{Binding CmdTicketClick}" CommandParameter="{Binding ElementName=TicketsDataGrid, Path=SelectedItem}" /> DataGrid.InputBindings> <DataGrid />
在这个例子中,我想将DataGrid.SelectedItem传递给我的ViewModel中的Click_Command。
你的方法应如下所示,而ICommand实现本身保持如上所述。
private RelayCommand _commandTicketClick; public ICommand CmdTicketClick { get { if(_commandTicketClick == null) { _commandTicketClick = new RelayCommand(param => HandleUserClick(param)); } return _commandTicketClick; } } private void HandleUserClick(object item) { MyModelClass selectedItem = item as MyModelClass; if (selectedItem != null) { //... } }
视图模型
视图模型是MV VM中的“VM”。这是一个充当中间人的类,将模型公开给用户界面(视图),并处理来自视图的请求,例如按钮点击引发的命令。这是一个基本的视图模型:
public class CustomerEditViewModel { ////// 编辑客户 /// public Customer CustomerToEdit { get; set; } /// /// “ApplyChanges”命令 /// public ICommand ApplyChangesCommand { get; private set; } /// /// 构造函数 /// public CustomerEditViewModel() { CustomerToEdit = new Customer { Forename = "John", Surname = "Smith" }; ApplyChangesCommand = new RelayCommand( o => ExecuteApplyChangesCommand(), o => CustomerToEdit.IsValid); } /// /// 执行 "apply changes" 命令. /// private void ExecuteApplyChangesCommand() { //例如, 将客户保存到数据库 } }
构造函数创建Customer模型对象并将其分配给CustomerToEdit属性,以便它对视图可见。
构造函数还创建一个RelayCommand对象,并将其分配给ApplyChangesCommand属性,再次使其对视图可见。 WPF命令用于处理来自视图的请求,例如按钮或菜单项单击。
RelayCommand有两个参数 - 第一个是在执行命令时调用的委托(例如,响应按钮单击)。第二个参数是一个委托,它返回一个布尔值,指示命令是否可以执行;在这个例子中,它连接到客户对象的IsValid属性。如果返回false,则会禁用绑定到此命令的按钮或菜单项(其他控件的行为可能不同)。这是一个简单但有效的功能,无需编写代码来启用或禁用基于不同条件的控件。
如果你确实启动并运行了此示例,请尝试清空其中一个TextBox (以将Customer模型置于无效状态)。当你离开TextBox你会发现“Apply”按钮被禁用。
视图模型不实现INotifyPropertyChanged (INPC)。这意味着如果要将不同的Customer对象分配给CustomerToEdit属性,则视图的控件不会更改以反映新对象 - TextBox es仍将包含前一个客户的forename和surname。
示例代码的工作原理是因为Customer在视图模型的构造函数中创建,然后才被分配给视图的DataContext (此时绑定被连接起来)。在实际应用程序中,你可能正在使用构造函数以外的方法从数据库中检索客户。为了支持这一点,VM应该实现INPC,并且应该更改CustomerToEdit属性以使用你在示例Model代码中看到的“扩展”getter和setter模式,从而在setter中引发PropertyChanged事件。
视图模型的ApplyChangesCommand不需要实现INPC,因为命令不太可能改变。你会需要实现这种模式,如果你创建了比其他构造的命令的地方,例如某种Initialize()方法。
一般规则是:如果属性绑定到任何视图控件并且属性的值能够在构造函数之外的任何位置更改,则实现INPC。如果仅在构造函数中分配属性值,则不需要实现INPC(并且你将在过程中节省一些输入)。
模型
模型是M VVM中的第一个“M”。该模型通常是一个包含你希望通过某种用户界面公开的数据的类。
这是一个非常简单的模型类,它暴露了几个属性: -
public class Customer : INotifyPropertyChanged { private string _forename; private string _surname; private bool _isValid; public event PropertyChangedEventHandler PropertyChanged; ////// 客户姓氏 /// public string Forename { get { return _forename; } set { if (_forename != value) { _forename = value; OnPropertyChanged(); SetIsValid(); } } } /// /// 客户姓氏。 /// public string Surname { get { return _surname; } set { if (_surname != value) { _surname = value; OnPropertyChanged(); SetIsValid(); } } } /// ///指示模型是否处于有效状态。/// public bool IsValid { get { return _isValid; } set { if (_isValid != value) { _isValid = value; OnPropertyChanged(); } } } /// /// 设置IsValid属性的值。 /// private void SetIsValid() { IsValid = !string.IsNullOrEmpty(Forename) && !string.IsNullOrEmpty(Surname); } /// /// 引发PropertyChanged事件。 /// /// Name of the property. private void OnPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
此类实现INotifyPropertyChanged接口,该接口公开PropertyChanged事件。只要其中一个属性值发生更改,就会引发此事件 - 你可以在上面的代码中看到这一点。 PropertyChanged事件是WPF数据绑定机制中的关键部分,因为没有它,用户界面将无法反映对属性值所做的更改。
该模型还包含一个非常简单的验证例程,可以从属性设置器中调用。它设置一个公共属性,指示模型是否处于有效状态。我已经包含了这个功能来演示WPF 命令的“特殊”功能,你很快就会看到它。 WPF框架提供了许多更复杂的验证方法,但这些方法超出了本文的范围 。
View
View是M V VM中的“V”。这是你的用户界面。你可以使用Visual Studio拖放设计器,但大多数开发人员最终都会编写原始XAML代码 - 这种体验类似于编写HTML。
以下是允许编辑Customer模型的简单视图的XAML。而不是创建一个新视图,只需将其粘贴到WPF项目的MainWindow.xaml文件中,在
<StackPanel Orientation="Vertical" VerticalAlignment="Top" Margin="20"> <Label Content="Forename"/> <TextBox Text="{Binding CustomerToEdit.Forename}"/> <Label Content="Surname"/> <TextBox Text="{Binding CustomerToEdit.Surname}"/> <Button Content="Apply Changes" Command="{Binding ApplyChangesCommand}" /> StackPanel>
此代码创建一个简单的数据输入表单,包含两个TextBox es - 一个用于客户forename,另一个用于surname。每个TextBox上面都有一个Label ,表单底部有一个“Apply” Button 。
找到第一个TextBox并查看它的Text属性:
Text="{Binding CustomerToEdit.Forename}"
这种特殊的大括号语法不是将TextBox的文本设置为固定值,而是将文本绑定到“path” CustomerToEdit.Forename 。这条道路相对于什么?它是视图的“数据上下文” - 在本例中是我们的视图模型。正如你可能想到的那样,绑定路径是视图模型的CustomerToEdit属性,它是Customer类型,后者又显示一个名为Forename的属性 - 因此是“虚线”路径表示法。
类似地,如果查看Button的XAML,它有一个绑定到视图模型的ApplyChangesCommand属性的Command 。这就是将按钮连接到VM命令所需的全部内容。
DataContext
那么如何将视图模型设置为视图的数据上下文?一种方法是在视图的“代码隐藏”中设置它。按F7查看此代码文件,并在现有构造函数中添加一行以创建视图模型的实例,并将其分配给窗口的DataContext属性。它应该看起来像这样:
public MainWindow() { InitializeComponent(); DataContext = new CustomerEditViewModel(); }
在现实世界的系统中,通常使用其他方法来创建视图模型,例如依赖注入或MVVM框架。