使用MVVM模式开发自定义UserControl

本篇讲述使用MVVM来开发用户控件。由于用户控件在大部分情况下不涉及到数据的持久化,所以如果将M纯粹理解为DomainModel的话,使用MVVM模式来进行自定义控件开发实际上可以省略掉M,变成了VVM。

一:基本结构

本演示样例包含两个项目,WpfControls是用户控件项目,我们的用户控件全部包含在这里。项目WpfApplication1是Wpf窗体项目,为调用方。我们的第一步的整体解决方案结构如下所示:

image

二:第一阶段源码

建立UserControl1,要求能够对输入属性StudentName和Age,做出反应,即呈现在UI上。

首先创建ViewModel,即StudentViewModel:

public class StudentViewModel : NotificationObject
{
string studentName;
public string StudentName
{
get
{
return studentName;
}
set
{
studentName
= value;
this.RaisePropertyChanged(() => this.StudentName);
}
}

int age;
public int Age
{
get
{
return age;
}
set
{
age
= value;
this.RaisePropertyChanged(() => this.Age);
}
}
}

如果对于NotificationObject不熟悉的,可以参考《Prism安装、MVVM基础概念及一个简单的样例》。

UserControl1部分的前台:

image

后台部分。由于是用户控件,所以我们将要属性直接绑定在控件上:

public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
public StudentViewModel Student
{
get
{
return this.studentsViewModel;
}
set
{
this.studentsViewModel = value;
}
}
}

现在,再来看调用方,即WpfApplication1的MainWindow,前台:

image

后台。出于简化演示需要,WpfApplication1我们就不采用MVVM了,数据的获取,也直接在代码中硬编码:

public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}

private void Button_Click(object sender, RoutedEventArgs e)
{
uc1.Student.StudentName
= "lmj" + DateTime.Now.ToString();
uc1.Student.Age
= 90;
}
}

运行效果为:

image

三:问题来了

第一阶段的UI没有采用绑定机制,如果我们的调用方也要采用类似MVVM模式的架构,则需要我们在使用用户控件的时候采用绑定机制。即,我们现在要将前台修改为:

image

可以看到,控件实例uc2 ,完全采用绑定的机制。采用绑定机制后的后台代码为:

public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}

StudentViewModel student1
= new StudentViewModel();
StudentViewModel student2
= new StudentViewModel();

//初始化
private void Button_Click(object sender, RoutedEventArgs e)
{
//采用非绑定机制
uc1.Student = student1;
//采用绑定机制
uc2.DataContext = student2;
}

//改Model值
private void Button_Click1(object sender, RoutedEventArgs e)
{
student1.StudentName
= "lmj" + DateTime.Now.ToString();
student1.Age
= 90;
student2.StudentName
= "hzh" + DateTime.Now.ToString();
student2.Age
= 100;
}
}

我们没有对控件的属性值直接赋值,而是给控件的DataContext赋值,然后,通过该属性值的变化,前台就能相应的变化。注意,此时编译能通过,但是要运行代码会提示我们Student没有实现为DependencyProperty。这是因为WPF要求我们如果被用于绑定的,则必须要将属性注册为DependencyProperty。如果没有绑定上面的要求,则只要属性类型中的属性能够通过某种关系和INotifyPropertyChanged起来就OK(见源码中的NotificationObject)。

基于以上考虑,相应的,我们要将UserControl1修改一下,变为如下:

public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();

}

public static readonly DependencyProperty StudentProperty = DependencyProperty.Register("Student", typeof(StudentViewModel), typeof(UserControl1));

public StudentViewModel Student
{
get
{
return (StudentViewModel)GetValue(StudentProperty);
}
set
{
SetValue(StudentProperty, value);
}
}

}

题外话:仅仅因为要采用MVVM,所以我们将代码重构为绑定模式,可能并不能说服你增加这些额外工作量。但是,想象一下这个场景,我们将不得不考虑采用绑定机制。即:显示一个学生列表,如果我们采用ListBox来绑定这个列表,势必采用DataTemplate,这将让我们必须采用绑定机制的理由。

四:实现一个列表

继续重构调用者代码,前台为:

image

后台:

StudentViewModel student1 = new StudentViewModel();
StudentViewModel student2
= new StudentViewModel();
ObservableCollection
<StudentViewModel> students = new ObservableCollection<StudentViewModel>();

//初始化
private void Button_Click(object sender, RoutedEventArgs e)
{
//采用非绑定机制
student1.StudentName = "lmj" + DateTime.Now.ToString();
student1.Age
= 90;
uc1.Student
= student1;
//采用绑定机制
uc2.DataContext = student2;
//采用绑定机制,显示学生列表
lb1.ItemsSource = students;
}

//改Model值
private void Button_Click1(object sender, RoutedEventArgs e)
{
student1.StudentName
= "lmj" + DateTime.Now.ToString();
student1.Age
= 90;
student2.StudentName
= "hzh" + DateTime.Now.ToString();
student2.Age
= 90;
StudentViewModel svm1
= new StudentViewModel() { StudentName = "lmj", Age = 2 };
students.Add(svm1);
foreach (var item in students)
{
item.StudentName
= "lmj" + DateTime.Now.ToString();
}
}

注意:

运行代码。结果我们很遗憾的发现,列表虽然在随着Click1的点击而增加,却并未能显示出来任何的Student内容。回到本文最开始的代码去看,我们发现,在用户控件的前台代码中,我们有这样3行XAML语句:

image

这会使得在控件初始化的时候就生成一个StudentViewModel的实例,并绑定到控件的DataContext上,这导致在ListBox中的item元素在初始化的时候把PropertyChanged绑定到该事件上,而不是最终我们赋值给item的StudentViewModel实例。所以,应去掉这三行语句。记住,如果我们发现绑定失效,首先检测绑定元素的命名是否正确,其实就是检查绑定的实例是否只有一个。

到目前为止,源码如下:https://files.cnblogs.com/luminji/WpfApplication2.zip

五:问题来了

VM显然不应该作为控件的公开属性对调用者开放,为什么呢,因为VM的主要作用不是作为实体MODEL对外公开的,它的最重要的作用是作为联系UI和DOMAINMODEL的纽带,有点类似与MVC中的C,同时,它还可以包含一些自身的逻辑。所以,必须将VM中的实体Model部分(在本例中是Student)剥离出去,而仅仅保持逻辑部分。鉴于此,我们建立Student类型,并将其丢入UIModel中。

为了演示VM的逻辑功能,我们将UserControl1的前台加入一些命令绑定的指令(TextBlock的MouseLeftButtonDown),同时在VM中处理这些命令。修改后的UserControl1如下:

image

注意:

1:前台引入了i和Behaviours两个命名空间。Behaviours中的ExecuteCommandAction,请直接查看源码,这里不再详细列出。

2:图中最后一个红框部分绑定了VM,但是VM不负责显示,所以TB的Height=0。

VM,即StudentViewModel如下:

public class StudentViewModel : NotificationObject
{
public StudentViewModel()
{
Clicked
= new ActionCommand(this.Click);
}

public ICommand Clicked { get; private set; }

public void Click(object arg)
{
//为了演示需要,在这里用了一个MessageBox
//应尽量避免在VM中揉杂UI交互功能
MessageBox.Show((arg as Student).StudentName);
}
}

可以看到VM中已经完全没有实体信息了。当然,在本例中,实体信息作为参数可以通过Click方法被传入到VM中。

本例的最终源码的下载:https://files.cnblogs.com/luminji/WpfApplication3.zip

原文地址:https://www.cnblogs.com/luminji/p/2073912.html