🧭 WPF MVVM入门系列教程
在前面的文章中,我们介绍了数据绑定,因为这部分内容是MVVM模式
开发中ViewModel
的基础。
接下来我们将会围绕ViewModel展开更详细的介绍。
关注点分离(Separation of Concerns, SOC)
在对MVVM模式
进行介绍时,就提到过这个概念。
它指的是确保代码有一个单一的、定义良好的功能目的,并且不承担任何多余的责任。
这个概念是为了帮助我们理解 MVVM模式
开发,为什么要分层的问题。
依赖问题
代码依赖关系不一定引用程序集引用。
这里的依赖指的是一个单元的代码是否需要知道其它单元代码的存在。
如果一个类需要使用另一个类,前者变得依赖于后者。
具体来说,依赖关系在于类的接口——它的方法,属性和构造函数。建议的做法是将类的接口与它的实现进行分离。
我们首先看一下下面的代码片段
代码片段1
1 public class ShapeRenderer 2 { 3 private IGraphicsContext _graphicsContext; 4 5 public void DrawShape(Circle circleShape) 6 { 7 _graphicsContext.DrawCircle(circleShape.Position, circleShape.Radius); 8 } 9 }
代码片段2
1 public class ShapeRenderer 2 { 3 private IGraphicsContext graphicsContext; 4 5 public void DrawShape(IShape shape) 6 { 7 shape.Draw(graphicsContext); 8 } 9 }
这两个代码片段的功能都是为了绘制形状。但是在实现方式上稍有不同。
代码片段1:
这个代码片段,在绘制形状的函数接收一个Circle
对象。
它存在几个明显的问题
1、如果我们需要绘制其它的形状,就必须增加重载,以接收每个额外的形状类型作为参数。
2、每次添加新的形状,DrawShape
代码也必须更改,从而增加了维护负担。
3、圆形类以及任何其他形状,也必须在编译时对该代码可见。
4、DrawShape
方法对圆类的实现了解太多,它获取了圆类的位置和半径,圆类的这些属性是任何人都可以公开读取的,这就不必要地破坏了封装。
如果可能,“圆 ”对象所包含的数据不应透露给第三方
当然,这只是用于演示,不必去细究程序的合理性。
代码片段2:
在这个代码片段中,通过使用各种技术来实现关注点分离,从而纠正了原始代码中的一些问题。
1、实现关注点分离。现在,DrawShape
方法接受一个接口 IShape
,而不是形状的单一具体实现。任何实现 IShape
接口的类都可以传入该方法,而无需对方法进行任何修改。
2、我们会使用另一种技术来保持对每个形状的封装:控制反转(也称为 IoC)
。该方法不是查询形状的成员来绘制形状,而是要求形状自己绘制。
然后,它使用依赖注入(DI)
向形状传递IGraphicsContext
接口。从维护的角度来看,这种实现更具可扩展性。
在代码片段2中,添加新形状非常简单,只需实现 IShape
接口并编写其 Draw(IGraphicsContext)
方法即可。每当引入一个新形状时,DrawShape
方法或其类都无需更改。
代码片段2中的代码有一个明显的缺点。就是它不如代码片段1的代码直观。但这种缺点是可以被接受的,因为代码片段2在后期维护过程中会更加方便。
有些小伙伴可能理解不了这里描述的概念,我们用代码演示一下。
在代码片段1中,如果我们要绘制一个矩形
,我们必须要先定义一个矩形类型
1 public class Rect 2 { 3 Point Pos{get;set;} 4 double Width{get;set;} 5 double Height{get;set;} 6 }
然后增加一个DrawShap
重载,用于绘制矩形,也就是下面加粗的部分
1 public class ShapeRenderer 2 { 3 private IGraphicsContext _graphicsContext; 4 5 public void DrawShape(Circle circleShape) 6 { 7 _graphicsContext.DrawCircle(circleShape.Position, circleShape.Radius); 8 } 9 10 public void DrawShape(Rect rectShape) 11 { 12 _graphicsContext.DrawRect(rectShape.Pos,rectShape.X,rectShape.Y); 13 } 14 }
这种情况下,我们必须引用Rect类型
,而且还需要在IGraphicsContext
里增加重载。在模块化开发时,这种引用会带来一些不必要的麻烦。
在代码片段2中,如果我们要绘制一个矩形,我们也需要定义一个矩形类型。
但是这个类型可以定义在任意地方,它可以是当前程序集,也可以是单独的程序集(模块)里。
1 public class Rect : IShape 2 { 3 Point Pos{get;set;} 4 double Width{get;set;} 5 double Height{get;set;} 6 7 8 public void Draw(IGraphicsContext graphicsContext) 9 { 10 //绘制矩形 11 } 12 13 }
增加形状后,ShapeRenderer
类不用做任何修改,将需要将Rect
作为参数传入DrawShape
函数即可。
使用代码片段2的结构 ,ShapeRenderer
类不用去了解各种形状是什么,它具备什么属性。
如果使用代码片段1的结构,就必须清楚知道每种形状是怎么定义的,具备哪些属性。
关于Soc的总结
SoC 的一个关键目标是尽可能地限制依赖关系,并在必须存在依赖关系的地方在必须存在依赖关系的情况下,将其抽象化,以保护客户端代码不被更改。
过于相互依赖的代码是很难维护的,因为一次修改就会破坏无数个部分。
最糟糕的代码依赖循环依赖,即两个方法或两个类相互依赖。相互依赖。
为了解决循环依赖问题,我们必须确保依赖关系有正确的方向。换句话说,代码从下到上形成一个层次结构,较高层次的代码依赖于较低层次的代码。
MVVM 架构
正是使用了这样一种结构
View层
对Model层
一无所知。它从ViewModel层
获取一切。反过来,ViewModel
也会从Model层
获取它所需要的一切。
ViewModel层
会使用View层
能理解的方式来对数据进行操作和修饰,这里的View层
能理解的方式指的就是数据绑定(DataBinding)
和命令系统(Command)
。
View层
中的变化与Model层
完全无关,因为Model层
对View层
的存在没有概念。理想情况下,View层
所在程序集甚至不会包含对Model层
程序集的引用,这就是 ViewModel
所提供的分离效果。
观察者模式
在前面介绍ViewModel
时,提到过这个概念。这里详细讲解一下。
在《Design Patterns: Elements of Reusable Object-Oriented Software》(《设计模式》)一书中对观察者模式的说明如下:
“Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.”
-- 在对象之间定义一对多的依赖关系,这样当一个对象改变状态时,其所有依赖对象都会收到通知并自动更新。
在 C# 中,“观察者 ”模式是以事件的形式内置在语言中的。
事件遵循发布/订阅模式,即一个对象将触发其事件,任何注册为监听者的对象都将收到事件触发的通知。
订阅者列表由事件内部管理,甚至还有一些语法糖,以便使用 += 和 -= 操作符注册和取消注册事件。
观察者模式实现了发布者和订阅者之间的松散耦合:发布者完全不知道订阅者的情况,而订阅者则引用发布者。
因此,我们可以说依赖关系是从订阅者到发布者的。
如果没有这种模式,要想知道对象的状态变化,就必须不断检查其值是否发生变化。
这个过程被称为轮询,它与观察相反。为了轮询变化,你必须不断循环,直到发现变化,然后根据变化采取行动。
这样做效率极低,因此基于事件的模型通常是首选。
在 UML 中,观察者模式的类图如下所示:
C#为实现观察者模式的实现提供了接口:IObserver
和 IObservable
接口。但事件机制是该模式的隐式实现。
Model层
Model
是一个系统,它专注于通过软件解决方案解决特定问题。在设计时,它不用考虑使用的环境系统,不管是ASP.Net 应用程序
、Windows 窗体应用程序
,还是WPF程序
。
通常的做法是,让业务意识最强的软件工程师负责实现领域模型,使用面向对象的最佳实践来创建可扩展、可管理的软件解决方案。
例如在医疗影像行业,我们会用到Dicom格式(Digital Imaging and Communications in Medicine,医学数字成像和通信)
下面就是Dicom文件模型的部分定义
DicomFile.cs
1 public class DicomFile 2 { 3 // 4 // 摘要: 5 // Gets the file reference of the DICOM file. 6 public IFileReference File { get; protected set; } 7 8 // 9 // 摘要: 10 // Gets the DICOM file format. 11 public DicomFileFormat Format { get; protected set; } 12 13 // 14 // 摘要: 15 // Gets the DICOM file meta information of the file. 16 public DicomFileMetaInformation FileMetaInfo { get; protected set; } 17 18 // 19 // 摘要: 20 // Gets the DICOM dataset of the file. 21 public DicomDataset Dataset { get; protected set; } 22 23 // 24 // 摘要: 25 // Save DICOM file. 26 // 27 // 参数: 28 // fileName: 29 // File name. 30 // 31 // options: 32 // Options to apply during writing. 33 public void Save(string fileName, DicomWriteOptions options = null) 34 { 35 PreprocessFileMetaInformation(); 36 File = Setup.ServiceProvider.GetService<IFileReferenceFactory>().Create(fileName); 37 File.Delete(); 38 OnSave(); 39 using FileByteTarget target = new FileByteTarget(File); 40 new DicomFileWriter(options).Write(target, FileMetaInfo, Dataset); 41 }
可能 就有小伙伴有疑问了,Model层定义的类型是不是只是定义领域模型,它可以包含逻辑吗?
答案是可以的。模型的目的是代表业务领域。领域中所包含的业务逻辑是可以定义在模型里面的。
以上面的DicomFile为例,
它除了定义Dicom文件所具备的属性外,对于Dicom的一些领域业务逻辑也包含在了里面。例如保存Dicom文件、打开Dicom文件等。
注意:这里的业务逻辑只能是领域模型内业务逻辑,而不是程序流程上的业务逻辑。
View层(UI层)
在MVVM 模式
下,View层
仍负责显示数据、收集用户输入并将其传递,只不过现在是将其传递给ViewModel
。View
现在使用WPF中的绑定系统与ViewModel
进行通信,不再包含后台逻辑代码。
前面介绍的Model
层,可以解决软件的领域模型问题,而View
层则提供了人机交互界面,可以让任何人都能使用这些代码,而无需具备使用模型的编码知识。
在以前未使用MVVM模式
进行开发时,我们都会使用Code-Behind,将后台逻辑代码直接写在xxx.xaml.cs
下。
类似下面这样
MainWindow.xaml.cs
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 } 7 8 //关闭按钮点击事件 9 private void btnClose_Click(object sender, RoutedEventArgs e) 10 { 11 this.Close(); 12 } 13 14 //最大化按钮点击事件 15 private void btnMinimize_Click(object sender, RoutedEventArgs e) 16 { 17 this.WindowState = WindowState.Minimized; 18 }
这种情况下,UI和逻辑是绑定在一起的。
使用MVVM后,View层和逻辑是分开的。
类似下面这样
MainWindowViewModel.cs
1 public class MainWindowViewModel 2 { 3 public ICommand CloseCommand { get; set; } 4 5 public ICommand MaximizeCommand { get; set; } 6 7 public MainWindowViewModel() 8 { 9 CloseCommand = new RelayCommand(Close); 10 MaximizeCommand = new RelayCommand(Maximize); 11 } 12 13 private void Maximize() 14 { 15 throw new NotImplementedException(); 16 } 17 18 private void Close() 19 { 20 throw new NotImplementedException(); 21 } 22 }
我们只需要在合适的位置,将带有后台逻辑的ViewModel对象
赋值到UI的DataContext
属性,就可以实现后台逻辑和UI的绑定。
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 7 this.DataContext = new MainWindowViewModel(); 8 } 9 }
ViewModel层
在前面我们介绍过关注点分离这个概念。
介绍完Model
层和View
层之后,我们了解了各个层的作用。
将Model
层和View
层分开后,就实现了软件产品中主要子系统职责分离的第一阶段。
然而,View
层却不能直接使用Model
层所提供的业务领域的服务。所以就需要一个ViewModel
层, ViewModel
就是View
和Model
两个子系统之间的中介。
ViewModel
作为Model
和View的中介,它起到承上启下的作用。它将数据从Model
移动到View
,将用户输入从View
传送到Model
。
ViewModel层采用的是一种叫做中介者模式(Mediator)的开发模式。
在《Design Patterns: Elements of Reusable Object-Oriented Software》(《设计模式》)一书中对中介者模式的说明如下:
“Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.
--定义一个对象,封装一组对象的交互方式。Mediator 可避免对象之间显式地相互引用,从而促进松散耦合,并可让你独立地改变它们之间的交互。
参考前面的示意图,我们再来看一下ViewModel层跟各层是如何进行交互的
View <=> ViewModel
View
通过Command
和DataBinding
绑定到ViewModel
,是一个双向的交互过程
ViewModel -> Model
ViewModel
处理Model
中的数据,并使用Model
中提供的函数进行业务逻辑决策。
Model-> ViewModel
Model
返回数据或结果,ViewModel
处理这些数据,并将其发送回 View
显示。
第一个ViewModel
在前面介绍数据绑定时,我们一直是将某个数据对象绑定到DataContext
上。
像下面这样
数据对象
1 public class MyData 2 { 3 private string displayText = "HelloWorld"; 4 5 public string DisplayText 6 { 7 get => displayText; 8 set => displayText = value; 9 } 10 }
绑定到DataContext
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 7 var myData = new MyData(); 8 this.textbox.DataContext = myData; 9 } 10 }
其实这个对象它已经是属于一个简化版的ViewModel
了。
但是它只包含属性,还不包含其它类型,如集合、命令等。
为了方便大家理解数据绑定的概念,所以一直没有命名为XXXViewModel
从这里开始,我们会将设置到上下文的对象都以ViewModel命名结尾。
一般来说,ViewModel
包含的内容包括:Commands(命令), Bindable properties and collections(可绑定的属性和集合), Validation(数据校验)等等。
下面是一个较为完整的ViewModel
展示
1 public class SimpleViewModel : INotifyPropertyChanged, IDataErrorInfo 2 { 3 //本地变量 4 private bool xxx; 5 private int nxxx; 6 7 //基础类型字段 8 private string property1; 9 10 //属性 11 public string Property1 12 { 13 get 14 { 15 return property1; 16 } 17 18 set 19 { 20 property1 = value; 21 //属性更改通知 22 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Property1") ); 23 } 24 } 25 26 //对象类型字段 27 private Model1 model1; 28 29 //属性 30 public Model1 Model1 31 { 32 get 33 { 34 return model1; 35 } 36 37 set 38 { 39 model1 = value; 40 //属性更改通知 41 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Model1")); 42 } 43 } 44 45 //可通知的列表 46 private ObservableCollection<string> observableList = new ObservableCollection<string>(); 47 48 //属性 49 public ObservableCollection<string> ObservableList 50 { 51 get 52 { 53 return observableList; 54 } 55 56 set 57 { 58 observableList = value; 59 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ObservableList")); 60 } 61 } 62 63 /// <summary> 64 /// 命令 65 /// </summary> 66 public ICommand MyCommand { get; private set; } 67 68 //Service类 69 private IMyService myService; 70 71 public SimpleViewModel(IMyService myService) 72 { 73 //通过Ioc注入Service类 74 this.myService = myService; 75 76 //初始化命令,并绑定到对应的回调函数 77 MyCommand = new RelayCommand(MyMethod); 78 } 79 80 /// <summary> 81 /// MyCommand绑定的函数 82 /// </summary> 83 /// <exception cref="NotImplementedException"></exception> 84 private void MyMethod() 85 { 86 throw new NotImplementedException(); 87 } 88 89 //数据校验 90 public string Error 91 { 92 get 93 { 94 throw new NotImplementedException(); 95 } 96 } 97 98 99 public string this[string columnName] 100 { 101 get 102 { 103 //验证 104 return ""; 105 } 106 } 107 108 public event PropertyChangedEventHandler? PropertyChanged; 109 }
目前我们不必了解ViewModel
的每一个细节,在后面的文章中会慢慢深入介绍。
INotifyPropertyChanged接口
INotifyPropertyChanged
接口是观察者模式实现的一部分。
它实现的功能是:通知订阅者发布者的属性值刚刚发生了变化。在WPF里,订阅者就是UI。
如果没有这个接口,订阅者只能不断轮询发布者的属性,以便确定值发生了变化。
接口定义如下:
1 // 2 // 摘要: 3 // 通知客户端属性已经更改. 4 public interface INotifyPropertyChanged 5 { 6 // 7 // 摘要: 8 // 当属性值更改时引发 9 event PropertyChangedEventHandler? PropertyChanged; 10 }
PropertyChangedEventHandler
是一个委托,它接受一个发送者对象和一个 PropertyChangedEventArgs
实例。
与所有接口成员一样,该事件必须是公共事件。与所有 EventArgs
派生类一样,该事件的相关参数类也非常简单,只是对事件上下文进行不可更改的封装,如下所示。
1 public PropertyChangedEventArgs(string propertyName)
使用方法如下:
新建一个类,实现INotifyPropertyChanged
接口,然后在属性值更新时,引发 PropertyChanged
事件,进行属性更改通知。
这里我们通过在界面上自动更新时间来进行演示。
界面布局如下:
MainWindow.caml
1 <Window x:Class="INotifyPropertyChangedDemo.MainWindow" 2 mc:Ignorable="d" 3 Title="MainWindow" Height="450" Width="800"> 4 <Grid> 5 <Label Content="{Binding CurrentTime}" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="30"></Label> 6 </Grid> 7 </Window>
MainViewModel.cs
MainViewModel
里增加一个可通知的CurrentTime
属性
1 public class MainViewModel : INotifyPropertyChanged 2 { 3 public event PropertyChangedEventHandler? PropertyChanged; 4 5 private string currentTime; 6 7 public string CurrentTime 8 { 9 get => currentTime; 10 set 11 { 12 currentTime = value; 13 14 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("CurrentTime")); 15 } 16 } 17 }
我们也可以进行简易包装一下PropertyChanged
的调用
1 public class MainViewModel : INotifyPropertyChanged 2 { 3 public event PropertyChangedEventHandler? PropertyChanged; 4 5 private string currentTime; 6 7 public string CurrentTime 8 { 9 get => currentTime; 10 set 11 { 12 currentTime = value; 13 14 RaiseChanged(); 15 } 16 } 17 18 private void RaiseChanged([CallerMemberName] string memeberName = "") 19 { 20 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(memeberName)); 21 } 22 }
在MainViewModel
构造函数中开启一个定时器
1 public MainViewModel() 2 { 3 DispatcherTimer dispatcherTimer = new DispatcherTimer(); 4 dispatcherTimer.Interval = TimeSpan.FromSeconds(1); 5 dispatcherTimer.Tick += (sender, args) => { CurrentTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); }; 6 dispatcherTimer.IsEnabled = true; 7 }
设置DataContext
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 7 this.DataContext = new MainViewModel(); 8 } 9 }
运行效果
ObservableCollection
在MVVM开发中,需要向UI通知变化的不仅仅是单个对象实例。集合也需要通知绑定的列表项已经发生变化 。
这包括在列表项内部发生变化时触发事件。
还包括在添加新项目或删除现有项目时触发事件。
列表项内部发生变化时触发事件
对于列表项内部来说,只要实现了 INotifyPropertyChanged
,UI就可以被通知到。
例如我将一个Student
列表绑定到ListBox
上,在某个时间我们将ListBox
的某一项进行更新,也就是其中的某一个Student
对象,我们直接操作这个对象即可。
下面我们用代码演示一下
首先创建一个Student类
1 public class Student 2 { 3 public int Id { get; set; } 4 5 public string Name { get; set; } 6 }
然后我们在界面上放置一个ListBox
,并绑定到StudentList
1 <ListBox ItemsSource="{Binding StudentList}"> 2 <ListBox.ItemTemplate> 3 <DataTemplate> 4 <Grid> 5 <Grid.RowDefinitions> 6 <RowDefinition></RowDefinition> 7 <RowDefinition></RowDefinition> 8 </Grid.RowDefinitions> 9 10 <Label Content="{Binding Id}" FontSize="15" FontWeight="Bold" Margin="5,0,0,0" VerticalAlignment="Center"></Label> 11 <Label Content="{Binding Name}" Margin="5,0,0,0" VerticalAlignment="Center" Grid.Row="1"></Label> 12 </Grid> 13 </DataTemplate> 14 </ListBox.ItemTemplate> 15 </ListBox>
创建ViewModel
我们在构造函数中,初始化列表,并在等待2秒后,更新列表第一项Student
的Name
属性
1 public class MainWindowViewModel : INotifyPropertyChanged 2 { 3 private ObservableCollection<Student> studentList; 4 5 public ObservableCollection<Student> StudentList 6 { 7 get => studentList; 8 set 9 { 10 studentList = value; 11 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("StudentList")); 12 } 13 } 14 15 public event PropertyChangedEventHandler? PropertyChanged; 16 17 public MainWindowViewModel() 18 { 19 LoadDemoData(); 20 } 21 22 private async void LoadDemoData() 23 { 24 StudentList = new ObservableCollection<Student>() 25 { 26 new Student(){Id = 1,Name = "姓名1" }, 27 new Student(){Id = 2,Name = "姓名2" } 28 }; 29 30 //等待2秒 31 await Task.Delay(2000); 32 33 //更新列表项属性 34 StudentList[0].Name = "姓名2_修改"; 35 } 36 }
运行效果如下:
可以看到列表项并没有被更新
这是因为Student
类没有实现INotifyPropertyChanged
接口,在属性更改时,没有通知到UI。
我们将Student
类进行升级,实现INotifyPropertyChanged
接口
1 public class Student2 : INotifyPropertyChanged 2 { 3 private int id; 4 5 public int Id 6 { 7 get => id; 8 set 9 { 10 id = value; 11 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Id")); 12 } 13 } 14 15 private string name; 16 17 public string Name 18 { 19 get => name; 20 set 21 { 22 name = value; 23 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name")); 24 } 25 } 26 27 public event PropertyChangedEventHandler? PropertyChanged; 28 }
运行效果如下
可以看到程序在运行2秒后,Name属性的修改也同步显示在UI上。
添加新项目或删除现有项目时触发事件
这里我们使用一个示例来进行演示
假设我们在ViewModel
中增加了一个List
命合,并绑定到ListBox
。如果我们用代码去操作这个List
,比如增加项,删除项。发现界面不会有任何变化。
MainWindow.xaml
1 <Grid> 2 <ListBox ItemsSource="{Binding NormalList}"></ListBox> 3 </Grid>
MainViewModel.cs
我们创建一个列表,并在2秒后,移除列表最后一项
1 public class MainViewModel : INotifyPropertyChanged 2 { 3 private List<string> normalList = new List<string>(); 4 5 public event PropertyChangedEventHandler? PropertyChanged; 6 7 public List<string> NormalList 8 { 9 get 10 { 11 return normalList; 12 } 13 14 set 15 { 16 normalList = value; 17 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("NormalList")); 18 } 19 } 20 21 public MainViewModel() 22 { 23 InitializeList(); 24 } 25 26 private async void InitializeList() 27 { 28 NormalList.Add("1"); 29 NormalList.Add("2"); 30 NormalList.Add("3"); 31 NormalList.Add("4"); 32 33 await Task.Delay(2000); 34 //等待两秒,移除最后一项 35 NormalList.RemoveAt(3); 36 MessageBox.Show("移除最后一项"); 37 } 38 39 }
绑定到DataContext
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 7 this.DataContext = new MainViewModel(); 8 } 9 }
运行后可以发现,界面上的列表项并不会减少
INotifyCollectionChanged
前面的示例中,界面上不会更新的原因是没有在NormalList更新时通知UI。
这就需要用到INotifyCollectionChanged接口。
INotifyCollectionChanged
与 INotifyPropertyChanged
接口类似,但它适用于对象集合,而不是单个属性。
INotifyCollectionChanged
接口定义如下:
1 namespace System.Collections.Specialized 2 { 3 // 4 // 摘要: 5 // 监听并通知动态变化,例如项目被添加和删除或整个列表被清除。 6 public interface INotifyCollectionChanged 7 { 8 // 9 // 摘要: 10 // 集合更改时引发事件 11 event NotifyCollectionChangedEventHandler? CollectionChanged; 12 } 13 }
CollectionChanged
事件使用 NotifyCollectionChangedEventHandler
委托,
它接受一个发送者对象和一个 NotifyCollectionChangedEventArgs
实例。
1 // 2 // 摘要: 3 // 处理System.Collections.Specialized.INotifyCollectionChanged.CollectionChanged 4 // 事件的方法 5 // 6 // 参数: 7 // sender: 8 // 引发 事件的对象 9 // 10 // e: 11 // 事件信息 12 public delegate void NotifyCollectionChangedEventHandler(object? sender, NotifyCollectionChangedEventArgs e);
我们可以通过NotifyCollectionChangedEventArgs
的Action属性来确定集合的变化方式,它是一个NotifyCollectionChangedAction
枚举
取值如下
Add | 至少有一项被添加到集合 |
Remove | 至少有一项被从集合中移除 |
Replace | 至少有一项被从集合中替换 |
Move | 至少有一项被从命令中移动位置 |
Reset | 列表被重置 |
ObservableCollection<T>
但是在使用INotifyCollectionChanged
接口时,我们并不需要像属性那样,手动去引发事件进行通知。
因为WPF提供了ObservableCollection
类型, 可以直接使用ObservableCollection
类型。
该类位于 System.Collections.ObjectModel
命名空间中。
ObservableCollection
封装了完成了所有繁重的工作:通过使用正确的 Action
和适用数据引发 NotifyCollectionChanged
事件来响应集合内的更改。
现在我们将前面的示例升级为使用ObservableCollection
MainViewModel.cs
1 public class MainViewModel : INotifyPropertyChanged 2 { 3 private ObservableCollection<string> normalList = new ObservableCollection<string>(); 4 5 public event PropertyChangedEventHandler? PropertyChanged; 6 7 public ObservableCollection<string> NormalList 8 { 9 get 10 { 11 return normalList; 12 } 13 14 set 15 { 16 normalList = value; 17 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("NormalList")); 18 } 19 } 20 21 public MainViewModel() 22 { 23 InitializeList(); 24 } 25 26 private async void InitializeList() 27 { 28 NormalList.Add("1"); 29 NormalList.Add("2"); 30 NormalList.Add("3"); 31 NormalList.Add("4"); 32 33 await Task.Delay(2000); 34 //等待两秒,移除最后一项 35 NormalList.RemoveAt(3); 36 MessageBox.Show("移除最后一项"); 37 } 38 39 }
此时我们在等待2秒后,可以看到界面上列表的最后一项被移除
设置数据上下文(DataContext)的方法
在前面我们提到过,通过设置ViewModel
到View
的DataContext
属性,以实现绑定。
这里我们介绍一下常用的设置DataContext
的方法。
在后续的文章中,还会介绍几种新的方法。
方法1、直接在视图的构造函数里设置DataContext
在前面的示例中,我们大量使用了这种方法。这是最简单、快速直接的方法。
例如我们有一个窗口MainWindow.xaml
和MainWindowViewModel.cs
直接在MainWindow
的构造函数中设置。
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 7 this.DataContext = new MainViewModel(); 8 } 9 }
方法2、在XAML中设置
首先在XAML中引入ViewModel
类所在的命名空间
1 xmlns:viewmodel="clr-namespace:MethodsToSetViewModel.ViewModels"
设置DataContext
1 <Window.DataContext> 2 <viewmodel:MainWindowViewModel></viewmodel:MainWindowViewModel> 3 </Window.DataContext>
方法3、使用ViewModelLocator
首先我们新建一个ViewModelLocator
类
这里我们可以看到,在ViewModelLocator
类中构造了一个MainWindowViewModel
属性。
在属性中获取MainWindowViewModel
实例。
在后面的文章中,我们会使用Ioc来完成这里的功能。
1 public class ViewModelLocator 2 { 3 private MainWindowViewModel mainWindowViewModel; 4 5 public MainWindowViewModel MainWindowViewModel 6 { 7 get 8 { 9 if (mainWindowViewModel == null) 10 mainWindowViewModel = new MainWindowViewModel(); 11 12 return mainWindowViewModel; 13 } 14 } 15 }
然后在App.xaml
中增加资源,并创建ViewModelLocator
的实例
1 <Application.Resources> 2 <ResourceDictionary> 3 <ResourceDictionary.MergedDictionaries> 4 <ResourceDictionary> 5 <viewmodel:ViewModelLocator x:Key="Locator"></viewmodel:ViewModelLocator> 6 </ResourceDictionary> 7 </ResourceDictionary.MergedDictionaries> 8 </ResourceDictionary> 9 </Application.Resources>
最后在窗口的XAML
里使用Locator
定位ViewModel
1 DataContext="{Binding Source={StaticResource Locator},Path=MainWindowViewModel}"
示例代码
WPF-MVVM-Beginner/4_ViewModel at main · zhaotianff/WPF-MVVM-Beginner · GitHub