• 使用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

  • 相关阅读:
    vim可以打开,gvim无法打开
    Ubuntu用apt-get安装时依赖包无法安装
    如何用mm、mmm编译android中的模块
    装饰模式简单的代码
    FileWriter和FileReader简单使用
    TCP/IP、Http、Socket的区别--特别仔细
    surface实例-小球弹起事例
    android中图片的三级缓存cache策略(内存/文件/网络)
    接口回调
    大公司的Java面试题集
  • 原文地址:https://www.cnblogs.com/luminji/p/2073912.html
Copyright © 2020-2023  润新知