Mvvm的框架的实现依赖于完善的数据绑定机制,因此熟练使用mvvm就必须熟练掌握WPF/SL的数据绑定机制。下面我们从几个方面来看看mvvm数据绑定与传统的.net控件使用方式有什么不一样;
一、给控件属性赋值
首先我们定义个公有的普通属性:
1 public string TextProperty { get; set; }
传统的.net控件的赋值都是在页面的后台代码中通过以下方式实现:
1 this.TextBox1.Text = TextProperty;
数据绑定方式需要在Xaml中的Text属性中添加绑定语法:
1 <TextBox x:Name="TextBox1" Text="{Binding TextProperty}"/>
如果TextProperty在后台更改了,要想在UI也反映更改,传统的方式仍然是重新给TextBox1.Text赋值,那么数据绑定方式该怎么做呢,这时需要在ViewModel中实现INotifyPropertyChanged接口,通过触发PropertyChanged事件达到通知UI更改的目的;这里我们定义的ViewModel都继承自ViewModelBase,ViewModelBase封装在MvvmLight框架中,它已经实现了INotifyPropertyChanged接口,因此我们在定义ViewModel属性时,只需要调用RaisePropertyChanged(PropertyName)就可以进行属性更改通知了;具备属性更改通知的属性定义如下:
1 // DatePicker 选中日期 2 private DateTime _SelectedDate; 3 public DateTime SelectedDate 4 { 5 get 6 { 7 return _SelectedDate; 8 } 9 10 set 11 { 12 if (_SelectedDate == value) 13 return; 14 15 _SelectedDate = value; 16 17 RaisePropertyChanged("SelectedDate"); 18 } 19 }
或者使用代码段mvvminpc自动生成ViewModel属性。
ViewModelBase中INotifyPropertyChanged接口实现部分如下:
1 /// <summary> 2 /// Occurs when a property value changes. 3 /// </summary> 4 public event PropertyChangedEventHandler PropertyChanged; 5 6 [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", 7 Justification = "This cannot be an event")] 8 protected virtual void RaisePropertyChanged(string propertyName) 9 { 10 VerifyPropertyName(propertyName); 11 12 var handler = PropertyChanged; 13 14 if (handler != null) 15 { 16 handler(this, new PropertyChangedEventArgs(propertyName)); 17 } 18 } 19 20 [Conditional("DEBUG")] 21 [DebuggerStepThrough] 22 public void VerifyPropertyName(string propertyName) 23 { 24 var myType = this.GetType(); 25 if (myType.GetProperty(propertyName) == null) 26 { 27 throw new ArgumentException("Property not found", propertyName); 28 } 29 }
两种方式实现起来都很简单,看起来都差不多,那么使用数据绑定有什么好处呢:
1、绑定方式使业务逻辑和UI设计可以完全分离,因此没必要在后台代码xaml.cs中操作控件属性,而绑定的属性都定义在ViewModel中,因此我们完全可以将xaml.cs删除,ViewModel与View(UI)的关联通过UI控件的DataContext属性实现,通常我们都将ViewModel绑定到UI的顶层布局控件上(如Window,UserControl),从而使得ViewModel对整个UI可见。
2、简单的赋值可能看不出绑定的优势,而且要让View反映ViewModel的更改还必须在ViewModel中实现INotifyPropertyChanged接口,但是在这种情况,只要属性更改了,View控件就会响应更改,ViewModel是面向属性进行编程,而传统方式需要面对控件编程,如果属性更改,需要查找对应的UI控件,然后重新设置控件属性;数据绑定在对列表数据绑定时,优势更为突出,比如数据源的数据更改了,传统方式必须重新绑定才能反映数据源的更改,而绑定方式数据源更改甚至数据源部分字段数据的更改,UI都可以自动响应更改
3、传统方式必须给控件命名才能在后台代码使用,而绑定方式控件与ViewModel的属性绑定,使用ViewModel的属性跟使用控件的属性一样,可以不用给控件命名
二、读取控件属性值
通常在UI控件属性更改时,需要在逻辑处理的地方重新获取控件的值,传统方式必须找到控件,然后获取控件属性的值,而在数据绑定的方式中只需要在绑定语法中添加绑定方式Mode=TwoWay或OneWayToSource,这样,只要UI中绑定目标的值更改,就会触发ViewModel中属性的Set方法对属性进行赋值,保证任何时候使用属性都是UI的最新值,xaml中绑定语法如下:
1 <TextBox Text="{Binding BindText,UpdateSourceTrigger=PropertyChanged,Mode=TwoWay}" /> 2 <TextBox Text="{Binding BindText,UpdateSourceTrigger=PropertyChanged,Mode=OneWayToSource}"/>
注意:
SL中不支持OneWayToSource,只能使用TwoWay;
WPF中对于TextBox、Combobox等控件,默认(不写Mode)的绑定方式是TwoWay,而SL中好像默认都是OneWay,如果你搞不清楚,索性都写全了就不会混淆了;
对于更新数据源的时机,TextBox默认都是LostFocus方式,也就是失去焦点时才会更新源,WPF中可以指定更新源的时机,Wpf示例中为TextBox属性更改时同时更新数据源,也就是每输一个字母,数据源都会更改一次,而SL中不支持UpdateSourceTrigger,因此只能使用默认的LostFocus方式。
本章示例中我们创建了3个文本框来测试绑定方式,3个文本框以不同绑定方式绑定到相同数据源,更改其中一个文本框,我们可以观察其他2个文本框的值变化情况
另一个示例中我演示了省市联动的下拉框绑定效果,WPF中xaml代码如下:
1 <ComboBox x:Name="cmbProvince" Margin="3" ItemsSource="{Binding Provinces}" 2 Width="100" SelectedItem="{Binding Province}" 3 DisplayMemberPath="ProvinceName" SelectedValuePath="Cities" SelectedValue="{Binding Cities}"/> 4 <TextBlock Margin="3" Text="城市"/> 5 <ComboBox Margin="3" ItemsSource="{Binding Cities}" Width="100" 6 DisplayMemberPath="CityName" SelectedValuePath="CityName" SelectedValue="{Binding City}" /> 7 <ComboBox Margin="3" ItemsSource="{Binding Cities}" 8 DataContext="{Binding SelectedItem,ElementName=cmbProvince}" Width="100" 9 DisplayMemberPath="CityName" SelectedValuePath="CityName" 10 SelectedValue="{Binding DataContext.City,ElementName=LayoutRoot}" />
后台代码:
1 // 省市列表 2 public List<Model.Province> Provinces 3 { 4 get 5 { 6 return new List<Model.Province> 7 { 8 new Model.Province{ 9 ProvinceName="湖北", 10 Cities=new List<Model.City>{ 11 new Model.City{CityName="武汉",Population=1000}, 12 new Model.City{CityName="宜昌",Population=1000}, 13 new Model.City{CityName="孝感",Population=1000}, 14 } , 15 Capital = "武汉"}, 16 new Model.Province{ 17 ProvinceName="广东", 18 Cities=new List<Model.City>{ 19 new Model.City{CityName="深圳",Population=1000}, 20 new Model.City{CityName="广州",Population=1000}, 21 new Model.City{CityName="珠海",Population=1000}, 22 } , 23 Capital = "广州"}, 24 new Model.Province{ 25 ProvinceName="湖南", 26 Cities=new List<Model.City>{ 27 new Model.City{CityName="长沙",Population=1000}, 28 new Model.City{CityName="湘潭",Population=1000}, 29 new Model.City{CityName="岳阳",Population=1000}, 30 } , 31 Capital = "长沙"}, 32 }; 33 } 34 } 35 36 // 选中省份的城市列表 37 private List<Model.City> _Cities; 38 public List<Model.City> Cities 39 { 40 get { return _Cities; } 41 set 42 { 43 if (_Cities == value) 44 return; 45 46 _Cities = value; 47 48 RaisePropertyChanged("Cities"); 49 50 // 列表更改时,默认选中省会城市 51 _City = Province.Cities.SingleOrDefault(p => p.CityName == Province.Capital); 52 RaisePropertyChanged("City"); 53 54 if (Cities != null) 55 { 56 _RadioButtons = (from c in Cities 57 select new ToggleButtonViewModel 58 { 59 Content = c.CityName, 60 IsChecked = false, 61 }).ToList(); 62 63 _CheckButtons = (from c in Cities 64 select new ToggleButtonViewModel 65 { 66 Content = c.CityName, 67 IsChecked = false, 68 }).ToList(); 69 } 70 71 RaisePropertyChanged("RadioButtons"); 72 RaisePropertyChanged("CheckButtons"); 73 } 74 } 75 76 // 选中省份 77 private Model.Province _Province; 78 public Model.Province Province 79 { 80 get { return _Province; } 81 set 82 { 83 if (_Province == value) 84 return; 85 86 _Province = value; 87 88 RaisePropertyChanged("Province"); 89 } 90 } 91 92 // 选中城市 93 private Model.City _City; 94 public Model.City City 95 { 96 get { return _City; } 97 set 98 { 99 if (_City == value) 100 return; 101 102 _City = value; 103 104 RaisePropertyChanged("City"); 105 } 106 } 107 108 // 选中城市 109 private string _CityName; 110 public string CityName 111 { 112 get { return _CityName; } 113 set 114 { 115 if (_CityName == value) 116 return; 117 118 _CityName = value; 119 120 RaisePropertyChanged("CityName"); 121 } 122 }
可以看出,通过绑定到属性就可以完成下拉列表的联动,这里要注意的是SL中不能通过绑定到CityName来设置城市列表的选中项,如果省份列表更改,CityName就不能关联到Combobox的选中项,原因可能是SL通过引用查找选中项,WPF可以通过字符串相等来查找选中项,因此SL是通过SelectedItem来绑定选中项,可以看出来,在WPF中,我们的绑定可以比较随意,而在SL中要特别小心,一个小小的不同可能让你花很长时间找不到问题的原因,这时就要仔细看帮助文档了
SL设置下拉列表选中项方法:
1 // 列表更改时,默认选中省会城市 2 _City = Province.Cities.SingleOrDefault(p => p.CityName == "武汉"); 3 RaisePropertyChanged("City");
WPF中除了以上方法,可以直接给_CityName赋值字符串来设置列表选中项
1 // 列表更改时,默认选中省会城市 2 _CityName = "武汉"; 3 RaisePropertyChanged("CityName");
三、控件事件处理
传统方式处理控件事件都是在xaml中定义事件属性,然后在后台xaml.cs中添加事件处理方法:
Xaml:
1 <Button Content="按钮" Click="Button_Click" />
xaml.cs:
1 private void Button_Click(object sender, RoutedEventArgs e){}
在mvvm中,可以通过Command绑定进行按钮点击事件的处理:
xaml:
1 <Button Content="按钮" Command="{Binding ButtonCommand}"/>
ViewModel:
1 // 按钮点击命令 2 public ICommand ButtonCommand 3 { 4 get 5 { 6 return new RelayCommand( 7 () => System.Windows.MessageBox.Show("当前时间:" + Clock) 8 ); 9 } 10 }
本示例中,可以通过点击Button按钮弹出MessageBox显示当前时钟。
当然,我们不光要处理Button的点击事件,还需要处理其他一些事件类型,我们会在下一章节专门介绍mvvm命令和事件
四、 列表类型控件的处理
这里的列表类型指的是包括Combobox、ListBox、Datagrid等所有能绑定到集合的控件,到这里我们更需要了解一下WPF/SL的控件的模板和样式与绑定之间的关系。
比如ListBox控件,依然绑定到之前定义的Provinces列表,Xaml中定义如下:
1 <ListBox ItemsSource="{Binding Provinces}" DisplayMemberPath="ProvinceName"/>
xaml定义的显示DisplayMemberPath就是列表要显示的字段名,执行结果显示:
湖北
广东
湖南
ListBox其实有一个默认的ItemTemplate,它以一个Content控件来显示DisplayMemberPath定义的字段的内容,我们可以自定义ItemTemplate让ListBox显示更多的内容,注意ItemTemplate与DisplayMemberPath不能同时定义
1 <ListBox ItemsSource="{Binding Provinces}"> 2 <ListBox.ItemTemplate> 3 <DataTemplate> 4 <StackPanel Orientation="Horizontal"> 5 <TextBlock Text="{Binding ProvinceName}"/> 6 <TextBlock Margin="5,0,2,0" Text="省会:"/> 7 <TextBlock Text="{Binding Capital}"/> 8 </StackPanel> 9 </DataTemplate> 10 </ListBox.ItemTemplate> 11 </ListBox>
显示结果如下:
我们可以将ListBox看成是集合数据的显示方式,一旦ItemsSource被绑定到集合List<Model.Province> ,那么ListBox的每个ItemTemplate的DataContext对应集合中的每个Model.Province对象,ItemTemplate的内容是呈现Model.Province对象的方式;这是我的理解方式,每个列表控件呈现数据方式不一样,但是数据源集合可以是一样的,UI设计人员则可以根据需要选择不同的数据显示方式。同样是List集合的数据源,让我们看看示例中另一种列表显示方式:
这个是一个完成分组和排序功能的Datagrid,同样只是简单的绑定到List集合,后台不用额外的代码,所有功能都在Xaml中完成:
首先在UI中定义CollectionViewSource资源,在这里定义排序和分组的规则
WPF中定义如下:
1 <Window.Resources> 2 <CollectionViewSource x:Key="ProductsGroup" Source="{Binding Products}"> 3 <CollectionViewSource.GroupDescriptions> 4 <PropertyGroupDescription PropertyName="ProductDate" /> 5 </CollectionViewSource.GroupDescriptions> 6 <CollectionViewSource.SortDescriptions> 7 <scm:SortDescription PropertyName="ProductDate" Direction="Descending" /> 8 <scm:SortDescription PropertyName="ID" Direction="Ascending" /> 9 </CollectionViewSource.SortDescriptions> 10 </CollectionViewSource> 11 </Window.Resources> 12 13 ... 14 ... 15 16 <DataGrid DataContext="{StaticResource ProductsGroup}" AutoGenerateColumns="False" 17 ItemsSource="{Binding}" SelectedItem="{Binding SelectedProduct}" CanUserAddRows="False"> 18 <DataGrid.GroupStyle> 19 <GroupStyle> 20 <GroupStyle.HeaderTemplate> 21 <DataTemplate> 22 <TextBlock x:Name="txt" Background="LightBlue" FontWeight="Bold" 23 Foreground="White" Margin="1" Padding="4,2,0,2" 24 Text="{Binding Name,StringFormat='生产日期:/{0/}'}" /> 25 </DataTemplate> 26 </GroupStyle.HeaderTemplate> 27 </GroupStyle> 28 </DataGrid.GroupStyle> 29 <DataGrid.Columns> 30 <DataGridTextColumn Binding="{Binding ID}" Header="编号" /> 31 <DataGridTextColumn Binding="{Binding Name}" Header="名称" /> 32 <DataGridTextColumn Binding="{Binding Desc}" Header="说明" /> 33 </DataGrid.Columns> 34 </DataGrid>
SL中定义如下:
1 <UserControl.Resources> 2 <CollectionViewSource x:Key="ProductsGroup" Source="{Binding Products}"> 3 <CollectionViewSource.GroupDescriptions> 4 <PropertyGroupDescription PropertyName="ProductDate" /> 5 </CollectionViewSource.GroupDescriptions> 6 <CollectionViewSource.SortDescriptions> 7 <scm:SortDescription PropertyName="ProductDate" Direction="Descending" /> 8 <scm:SortDescription PropertyName="ID" Direction="Ascending" /> 9 </CollectionViewSource.SortDescriptions> 10 </CollectionViewSource> 11 </UserControl.Resources> 12 13 ... 14 ... 15 16 <sdk:DataGrid DataContext="{StaticResource ProductsGroup}" AutoGenerateColumns="False" 17 ItemsSource="{Binding}" SelectedItem="{Binding SelectedProduct}" Width="300" > 18 <sdk:DataGrid.Columns> 19 <sdk:DataGridTextColumn Binding="{Binding ID}" Header="编号" /> 20 <sdk:DataGridTextColumn Binding="{Binding Name}" Header="名称" /> 21 <sdk:DataGridTextColumn Binding="{Binding Desc}" Header="说明" /> 22 </sdk:DataGrid.Columns> 23 </sdk:DataGrid>
后台代码:
1 public List<Model.Product> Products 2 { 3 get 4 { 5 return new List<Model.Product> 6 { 7 new Model.Product{ID=11,Name="P1",ProductDate=new DateTime(2010,1,1),Desc="产品1"}, 8 new Model.Product{ID=1,Name="P2",ProductDate=new DateTime(2010,1,2),Desc="产品2"}, 9 new Model.Product{ID=13,Name="P3",ProductDate=new DateTime(2010,1,1),Desc="产品3"}, 10 new Model.Product{ID=7,Name="P4",ProductDate=new DateTime(2010,1,2),Desc="产品4"}, 11 new Model.Product{ID=21,Name="P5",ProductDate=new DateTime(2010,1,1),Desc="产品5"}, 12 new Model.Product{ID=19,Name="P6",ProductDate=new DateTime(2010,1,1),Desc="产品6"}, 13 new Model.Product{ID=41,Name="P7",ProductDate=new DateTime(2010,1,3),Desc="产品7"}, 14 }; 15 } 16 }
最后还有一种特殊的列表控件,那就是TreeView ,TreeView 用一个专门绑定层次关系的ItemTemplate来显示树状结构,它就是HierarchicalDataTemplate,WPF模板定义方式如下:
1 <TreeView x:Name="treeview1" ItemsSource="{Binding TreeData}"> 2 <TreeView.ItemTemplate> 3 <HierarchicalDataTemplate ItemsSource="{Binding Children}"> 4 <TextBlock Text="{Binding NodeName}"/> 5 </HierarchicalDataTemplate> 6 </TreeView.ItemTemplate> 7 </TreeView>
SL中定义方法一样,不同是TreeView所在的命名控件不一样
数据源的类定义如下:
1 public class TreeNode 2 { 3 public string NodeID { get; set; } 4 public string NodeName { get; set; } 5 6 public List<TreeNode> Children { get; set; } 7 }
Treeview绑定显示结果如下:
使用ItemsControl控件还可以用来处理动态生成控件的情况,只需将控件的属性映射到Model属性,然后在ItemTemplate中将控件属性绑定到Model属性,就可以动态创建控件列表;通过自定义ItemsControl的ItemsPanel模板,就可以控制ItemsControl内的控件排列方式,比如示例中使用WrapPanel来显示一个图片浏览器,当一行显示的图片总宽度超过WrapPanel宽度,就会自动换行显示,WPF中xaml代码如下:
1 <ListBox x:Name="ListBox1" ItemsSource="{Binding ListBoxData}" Width="300"> 2 <ListBox.ItemsPanel> 3 <ItemsPanelTemplate> 4 <WrapPanel Width="{Binding ActualWidth,RelativeSource={RelativeSource AncestorType={x:Type ListBox}}}"/> 5 </ItemsPanelTemplate> 6 </ListBox.ItemsPanel> 7 <ListBox.ItemTemplate> 8 <DataTemplate> 9 <StackPanel> 10 <Image Source="{Binding ImageSource}" Width="96"/> 11 <TextBlock HorizontalAlignment="Center" Text="{Binding Text}"/> 12 </StackPanel> 13 </DataTemplate> 14 </ListBox.ItemTemplate> 15 </ListBox>
WrapPanel 的宽度绑定到它的父元素ListBox的宽度,这么写的好处是ItemsPanelTemplate可以当作公共资源定义在资源文件中供多处同时使用,而在SL中,由于不支持FindAncestor绑定语法,需要指定父元素的ElementName来绑定,或者直接使用数字
本章主要介绍MvvmLight中数据绑定的实现,但并不涉及WPF/SL数据绑定的全部内容,下章我们将主要介绍MvvmLight中命令和事件的用法。
本章示例代码如下:
http://download.csdn.net/source/3254233
原文:http://blog.csdn.net/duanzilin/article/details/6399365