注:本文出现的所有代码为了简单明了均省略了很多细节部分,只注重原理,直接复制粘贴运行得不到对应的结果。
WPF的数据驱动理念
传统的winform开发方式是事件驱动模式,比如点击一个按钮,激发对应的事件函数,从而操纵数据和其他控件,这种开发方式有一个潜在的弊端,就是当一个界面中显示逻辑比较复杂,控件较多时,开发工作非常繁琐,比如动态生成一个100项的列表,列表中还有按钮、勾选框、文本框等控件,事件处理函数不仅要控制控件的状态,还要进行数据操控,开发和维护难度变大。
WPF的数据驱动模式免去了上述的winform开发模式中的很多缺点,首先,每个控件直接和数据对应,控件和控件之间不进行任何交互,其次,控件操控数据进行更改后,数据与对应的控件进行数据绑定,数据更改,前端即时刷新。那么,如果数据数量不是固定的而是动态的,比如一个列表,事先不知道有多少项,要根据列表项动态生成控件,怎么办呢,答案是WPF精心设计的数据模板。
winform和WPF相比的话简单来说就是下图:WPF中的数据绑定
要实现数据驱动,则需要手动将数据和控件进行绑定,什么样的数据格式可以进行绑定呢,答案是依赖属性或者特殊的集合比如ObservableCollection<T>,
一个典型的依赖属性如下:
public string DemoStr
{
get { return (string)GetValue(DemoStrProperty); }
set { SetValue(DemoStrProperty, value); }
}
// Using a DependencyProperty as the backing store for DemoStr. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DemoStrProperty =
DependencyProperty.Register("DemoStr", typeof(string), typeof(MainWindow));
依赖属性定义了属性名称(DemoStr),属性类型(string),所属的类(MainWindow),我们就可以将它当成普通属性进行赋值,修改了,如果要绑定到文本框的内容上,实现数据变动时文本框实时变动,需要在xaml文件中进行定义:
<Label x:Name="label1" Content="{Binding DemoStr}"/>
此时还没有完,我们需要定义前端的控件是与哪一个对象绑定,所以要定义绑定的对象,如果直接在窗口后台代码文件中定义的依赖属性,则可以直接将当前窗口对象定义为控件的数据来源:
label1.DataContext = this;
所以,基本的数据绑定有三步:
- 设计好需要绑定的数据
- 在控件上设置好需要绑定的属性名称
- 设置控件的数据来源
需要转换数据格式的数据绑定
很多时候数据不是直接显示到前端的,比如一个表示成绩是否及格的bool数据,在前端应该以红色方格或者绿色方格来显示(及格为绿色,不及格为红色),这时就涉及到了数据转换,数据转换需要专门定义一个定义数据转换类,而且要实现IValueConverter接口,官方有示例,这里不再赘述,示例链接地址:https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visual-studio-2010/dd434207(v=vs.100)
路由事件与传统winform事件的区别
传统的winform事件的触发对象只能是控件本身,而路由事件可以将触发对象沿着控件数向上或者向下传递,比如一个grid网格控件内有多个按钮控件,可以设置grid拦截这些按钮的点击并处理,这在winform上是无法实现的。下面的代码示例是一个grid网格控件拦截到内部的按钮点击事件:
<Grid x:Name="grid" Button.Click ="Grid_Click" >
<Button Content="ShowMe"/>
</Grid>
后台代码文件如下:
private void Grid_Click(object sender, RoutedEventArgs e)
{
Button button = e.OriginalSource as Button;//通过e.OriginalSource得到点击的按钮,注意不是sender参数
MessageBox.Show(button.Content.ToString());//弹窗显示ShowMe
}
上面的示例中,sender参数是grid控件,代表事件的拥有者,我们让grid控件响应Grid_Click函数,所以sender就是grid控件,而想要得到点击的控件,需要使用e.OriginalSource
属性并转化为对应的控件格式。
对比在传统的winform开发中,如果需要动态生成按钮,并且让按钮进行数据或者空间操作是相当麻烦的,有很多时候还需要根据给控件命名然后查找比如button1
,button2
,button3
...来进行对应的事件处理,而在WPF开发中,通过路由事件,则能使一个父级控件监控所有子控件的各种事件来进行集中处理。
数据绑定的精华:数据模板
前文提到的动态生成列表,适用于后端数据为集合的情况。在传统的winform中,由于数据项未知,所以要使用代码来进行动态生成控件,根据给控件命名来访问控件也十分繁琐,WPF彻底解决了这个问题,那就是在xaml中定义数据模板来控制数据如何显示在前端,下面是一个数据模板的示例:
<local:BoolToBrushConverter x:Key="boolToBrushConverter" />
<DataTemplate x:Key="listBoxTemplate">
<StackPanel Orientation="Horizontal" >
<Rectangle Fill="{Binding Path=IsEnrolled,
Converter={StaticResource boolToBrushConverter}}"/>
<TextBlock Text="{Binding Path=StudentName}"/>
<Button Content="弹出名字" Click="btn_Click" />
</StackPanel>
</DataTemplate>
<Grid x:Name="grid">
<ListBox x:Name="listbox"
ItemsSource="{Binding}"
ItemTemplate="{Binding Mode=OneWay, Source={StaticResource listBoxTemplate}}"/>
</Grid>
上面引入了一个boolToBrushConverter
转换器,作用是将数据中的bool数据转化为红色和绿色的画刷格式,这样前端就可以根据数据的true
或者false
来显示红色或者绿色,具体代码见前文的官方教程,这里省略,然后我们定义了一个数据模板,定义了一个数据项如何进行显示,其中有一个颜色方块显示红色或者绿色,TextBlock
显示StudentName
属性,还有一个按钮来响应btn_Click
事件,然后在grid控件中定义了一个ListBox
控件,用来显示我们的数据,然后看一下后台代码:
//我们要绑定的数据类型
public class Student
{
public Student(){}
public Student(string name, bool isenrolled)
{
StudentName = name;
IsEnrolled = isenrolled;
}
public string StudentName { get; set; }
public bool IsEnrolled { get; set; }
}
public class StudentList : ObservableCollection<Student>
{
}
//窗口后台代码
public partial class MainWindow : MetroWindow
{
StudentList students = new StudentList();//实例化我们的数据
public MainWindow()
{
InitializeComponent();
//添加几项数据
students.Add(new Student("Syed Abbas", false));
students.Add(new Student("Lori Kane", true));
students.Add(new Student("Steve Masters", false));
students.Add(new Student("Tai Yee", true));
//为listbox绑定数据源
listbox.DataContext = students;
}
//数据模板中的按钮事件处理函数
private void btn_Click(object sender, RoutedEventArgs e)
{
//通过e.OriginalSource拿到点击的按钮
var button = e.OriginalSource as FrameworkElement;
//通过TemplatedParent属性拿到所在的模板生成控件
var cp = button.TemplatedParent as ContentPresenter;
//通过Content属性拿到绑定的单个数据实例
Student stu = cp.Content as Student;
//显示该数据实例的StudentName属性
MessageBox.Show(stu.StudentName);
}
}
前端界面效果如下:
单击按钮后,触发了提示框:
至此,我们可以得出一个WPF的开发流程:
1.利用xaml搭建前端界面,同时在xaml中设定好控件要绑定的属性、样式以及数据模板、事件、命令、动画等
2.构建我们的数据格式,可以设计成一个ViewModel类来适配前端需要绑定的数据格式,如果是单个属性,则在类中定义依赖属性,如果是集合数据,则需使用ObservableCollection<T>等特殊的集合。
3.在后台代码文件中实例化我们定义的ViewModel类,并且指定前端的DataContext为我们的ViewModel类实例
4.编写事件处理函数,根据控件行为处理数据
从上面的步骤中,我们可以看到控件之间的状态不再互相靠事件函数进行更新,而是每个控件根据自己绑定的数据进行状态更新,我们的程序只专注于数据的状态,这就是数据驱动的理念,传统的MVC模式是Model(数据)View(显示)Control(控制),在winform中,随着程序的复杂,Control也随之变得复杂(各种事件处理函数同时控制view和model),在WPF中,数据绑定自动完成了控件状态的更新,MVC也转化为MVVM,即Model(数据)View(显示,即xaml)ViewModel(数据显示层,即我们用来适配前端的ViewModel类和前端用来更改ViewModel数据的事件处理函数)。