使用MVVM模式开发自定义UserControl

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

本篇讲述使用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实例。所以,应去掉这三行语句。记住,如果我们发现绑定失效,首先检测绑定元素的命名是否正确,其实就是检查绑定的实例是否只有一个。

到目前为止,源码如下:http://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中。

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

Creative Commons License本文基于 Creative Commons Attribution 2.5 China Mainland License发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名 http://www.cnblogs.com/luminji(包含链接)。如您有任何疑问或者授权方面的协商,请给我留言。
目录
相关文章
|
前端开发 JavaScript
使用Vue+Element-UI从0搭建一个常见的前端模板
使用Vue+Element-UI从0搭建一个常见的前端模板
709 0
|
安全 数据库 数据安全/隐私保护
密码管理工具-KeePass入门指南
密码管理工具-KeePass入门指南
1883 0
|
前端开发 编译器 测试技术
Kotlin Multiplatform 跨平台开发的优化策略与实践
本文深入讲解Kotlin Multiplatform(KMP)的优化策略与实践。KMP是由JetBrains推出的开源技术,允许跨平台共享代码同时保持原生优势。文章覆盖KMP核心概念、性能优化技巧(如代码结构优化、利用`expect`/`actual`关键字、Kotlin/Native性能特性等),以及在移动、桌面和Web应用的实际案例分析。此外,还介绍了如何利用KMP生态系统工具进行快速开发,并展望了KMP的未来发展。
449 0
|
JavaScript 前端开发 应用服务中间件
Vue开发中,在实现单页面应用(SPA)前端路由时的hash模式和history模式的区别及详细介绍
Vue开发中,在实现单页面应用(SPA)前端路由时的hash模式和history模式的区别及详细介绍
477 0
|
Linux Android开发 iOS开发
Android经典实战之Kotlin Multiplatform跨平台开发
KMP(Kotlin Multiplatform)是由JetBrains开发的开源技术,让开发者能在多平台间高效重用代码,保留原生编程优势。适用于Android/iOS应用、多平台库及桌面应用开发。KMP支持代码共享、预期与实际声明机制,具备灵活性、稳定性和性能优势。通过Compose Multiplatform可实现跨平台UI共享。开发者可访问官方文档开始学习。
682 1
|
Java 测试技术 Spring
Java 新手入门:依赖注入的 N 种姿势,总有一款适合你!
Java 新手入门:依赖注入的 N 种姿势,总有一款适合你!
440 2
|
Web App开发 编解码 移动开发
网页不安装插件如何播放RTSP/FLV视频
点量云流提出了一种基于后台拉流转码的方案,将RTSP/RTMP/FLV等协议的视频流转换为WebRTC格式,实现在现代浏览器中的无插件播放。此方案具有良好的兼容性,支持主流浏览器,无需担心兼容问题。它利用浏览器硬件解码能力,减少终端计算资源消耗,并且具备低延迟和高实时性的优点,延迟可控制在100ms以内,非常适合摄像头监控领域。此外,前端集成简单,仅需使用标准WebRTC接口即可接入,降低了复杂度。
602 9
|
SQL 分布式计算 监控
在hue上部署spark作业
7月更文挑战第11天
348 3
|
传感器 存储
基于STM32与FreeRTOS的四轴机械臂项目-1
基于STM32与FreeRTOS的四轴机械臂项目
基于STM32与FreeRTOS的四轴机械臂项目-1
|
存储 监控 安全
计算机网络硬件
一、计算机网络硬件 计算机网络硬件是指构成计算机网络的物理设备和组件,包括以下几个主要部分: 1. 网络接口卡(Network Interface Card, NIC):也称为网卡,是连接计算机到网络的接口设备。它负责将计算机内部的数据转换为网络可以识别和传输的格式,同时也负责将网络传输的数据转换为计算机可以理解和处理的格式。 2. 集线器(Hub):是一种用于将多个计算机连接在一起的设备。它通过物理层的广播方式将数据从一个端口广播到其他所有端口,所有连接在集线器上的计算机都可以接收到这些数据。 3. 交换机(Switch):与集线器类似,也是用于将多个计算机连接在一起的设备。但是交换机通过数
350 0