写本篇纯属意外。原来想用主从数据显示的例子记录页面间切换的方法的,后来在园子里看到有一篇写页面切换的文章介绍得很详尽了,代码做了一半,真是鸡肋啊。于是想,干脆把代码改改,弄成个MVVM模式来展示主从数据吧。
为了突出重点,示例不考虑美工方面的问题——嘿嘿,美工实在太差了,各位见谅。
首先来看完成后的效果:
启动时候,显示一个空的页面,点击“Show Data”,显示出所有的班级信息。
当用户点击其中某一个班级的时候,跳转到一个班级的学生列表中去。详细信息页面底部还提供一个返回按钮,可以返回到班级选择的页面:
整个项目完成了以后,结构如下:
项目大体上分为Models、Views和ViewModels三个部分。其中,Models又被细分为“Entities”、“Interfaces”和“Services”三个部分。
-
Models
Models主要存放两件东西:1.实体类。2.提供的服务。实体类是指对事物的属性的抽象构成的类——这个好像比较抽象啊:-)其实,非常简单,就是一些代表事物的属性的集合,例如,一个班级的ID和名称就代表着一个班级,我们就写成Classes类:
namespace SilverlightNotes.Navigate.Models.Entities { public class Classes { public int ID { get; set; } public string Name { get; set; } } }
类似的,我们把一个学生抽象成由“编号”、“姓名”和“班组”组成,就有了Student类:
namespace SilverlightNotes.Navigate.Models.Entities { public class Student { public int ID { get; set; } public string Name { get; set; } public int ClassID { get; set; } } }
我们看到,实体类只有属性,没有方法。通常,我们需要从某个地方去获取数据来填充或者说生成这些实体类的实例,我们把这一些获取数据的方法做成服务接口。这些接口被统一存放在Interfaces下面。以下是班级类的接口:
using System.Collections.Generic; using SilverlightNotes.Navigate.Models.Entities; namespace SilverlightNotes.Navigate.Models.Interfaces { /// <summary> /// Provide student related services /// </summary> public interface IClassesService { /// <summary> /// Get all classes /// </summary> /// <param name="belongTo"></param> /// <returns></returns> List<Classes> GetClasses(); } }
类似的,学生类的服务接口如下:
using System.Collections.Generic; using SilverlightNotes.Navigate.Models.Entities; namespace SilverlightNotes.Navigate.Models.Interfaces { /// <summary> /// Provide student related services /// </summary> public interface IStudentService { /// <summary> /// Get all students in a class /// </summary> /// <param name="belongTo"></param> /// <returns></returns> List<Student> GetStudentByClasses(Classes belongTo); } }
然后,我们需要具体的服务来完成这一些接口。这些服务应该是通过访问数据库啊之类的数据存储,来提供实体类实例数据。这里为了演示,只写了两个假的数据提供类,来提供一些示例数据,它们分别实现了IClassesService接口和IStudentService接口:
using System.Collections.Generic; using SilverlightNotes.Navigate.Models.Entities; using SilverlightNotes.Navigate.Models.Interfaces; namespace SilverlightNotes.Navigate.Models.Services { public class MockClasses : IClassesService { /// <summary> /// Return mocked 5 classes /// </summary> /// <returns></returns> public List<Classes> GetClasses() { const int classCount = 5; List<Classes> result = new List<Classes>(classCount); for (int i = 0; i < classCount; i++) { result.Add(new Classes() { ID = i, Name = string.Format("Class - {0}", i + 1) }); } return result; } } }
和
using System.Collections.Generic; using SilverlightNotes.Navigate.Models.Entities; using SilverlightNotes.Navigate.Models.Interfaces; namespace SilverlightNotes.Navigate.Models.Services { public class MockStudent:IStudentService { public List<Student> GetStudentByClasses(Classes belongTo) { const int studentCount = 15; List<Student> result = new List<Student>(studentCount); //Create faked student objects and add them into the collection for (int i = 0; i < studentCount; i++) { result.Add(new Student() { ID = i + 1000, ClassID = belongTo.ID, Name = string.Format("Student{0}", i + 1) }); } return result; } } }
好,Model部分完成。
-
View
理论上讲,在MVVM模式中,View和Model是可以同时进行的。因为这两部分不会直接产生任何关系。我们需要做的,只是把界面“画”出来。本例中,一共需要三个View:MainPage、ClassesView和StudentView。
在这里MainPage类似于ASP.NET中的“MasterPage”的作用:我们用一个TextBlock来提供页面的标题,然后,用Border来模拟一个PlaceHolder,初步的想法是,页面切换时,只需要修改Border.Child属性即可。呵呵,在此偷个懒,其实所有的界面是用Blend画出来的。简单的来看一下MainPage的XAML吧:
<Grid x:Name="LayoutRoot" Background="White"> <Grid.ColumnDefinitions> <ColumnDefinition Width="25"/> <ColumnDefinition/> <ColumnDefinition Width="25"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="26"/> <RowDefinition Height="36"/> <RowDefinition Height="314"/> <RowDefinition Height="24"/> </Grid.RowDefinitions> <TextBlock Grid.Column="1" Grid.Row="1" TextWrapping="Wrap" FontFamily="Trebuchet MS" FontSize="18.667"/> <Border x:Name="bdrPlaceHolder" Grid.Column="1" Grid.Row="2" BorderBrush="Black" BorderThickness="1" /> </Grid>
这是一个4行3列的Grid,其实周边一圈是Margin,剩下2行1列。第1行放了一个TextBlock,用来放标题,例如“MVVM Navigation Demo”。Border的作用,前面已经讲过。
ClassesView中直接放了一个StackPanel,然后堆上一个“Show Data”的Button和一个显示数据的ListBox,就可以交差了。而StudentView则堆放了一个DataGrid和一个Button。
-
ViewModel
ViewModel是View和Model之间的纽带。我们把View绑定到ViewModel的类上,而ViewModel类同时又包装了Model的实体和服务。这样,当用户对界面操作时,会引发ViewModel的变化。ViewModel调用Model提供的服务,修改其包装的实体或实体集。由于这些实体或者实体集同样被绑定到了界面,因此,界面对用户的操作作出反应。
那么,如何来创建ViewModel类?让我们以MainPageViewModel类为例:
一、依葫芦画飘——看View搭出ViewModel类
打开MainPage,观察,它有一个TextBlock,因此,我们需要一个string类型的属性;它有一个Border作为PlaceHolder,因此,我们需要一个UIElement类型的属性;它可以加载ClassesView,因此,我们有一个加载ClassesView的方法(NavigateToClasses);它又可以加载StudentView,因此,我们又有了一个加载StudentView的方法(NavigateToStudnet)。创建出的类如下:
using System.ComponentModel; using SilverlightNotes.Navigate.Views; using SilverlightNotes.Navigate.Models.Entities; namespace SilverlightNotes.Navigate.ViewModels { public class MainPageViewModel : INotifyPropertyChanged { #region Construction private ClassesView _classesViewCache; public MainPageViewModel() { PageTitle = "MVVM Navigation Demo"; } #endregion #region Properties public string PageTitle { get; set; } public UIElement DisplayContent { get; set; } #endregion #region Faked Commands public void NavigateToClasses() { } public void NavigateToStudent(Classes selectedClass) { } #endregion } }
二、绑定属性,添加方法调用代码
ViewModel类创建之后,我们就可以把属性和对应的控件绑定起来。例如,把PageTitle绑定到MainPage的TextBlock上:
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding PageTitle}" TextWrapping="Wrap" FontFamily="Trebuchet MS" FontSize="18.667"/>
绑定以后,需要修改ViewModel类,对于一般的属性,修改时需要触发“PropertyChanged”事件,而对于集合类属性,则最好使用ObservableCollection<T>类型的集合。以MainPage中的PageTitle为例,首先要让其实现“INotifyPropertyChanged”接口,而在属性修改时,需要触发相应事件:
using System.ComponentModel; using SilverlightNotes.Navigate.Views; using SilverlightNotes.Navigate.Models.Entities; namespace SilverlightNotes.Navigate.ViewModels { public class MainPageViewModel : INotifyPropertyChanged { #region Events public event PropertyChangedEventHandler PropertyChanged = delegate { }; #endregion #region Construction private ClassesView _classesViewCache; public MainPageViewModel() { PageTitle = "MVVM Navigation Demo"; } #endregion #region Properties private string _pageTitle; public string PageTitle { get { return _pageTitle; } set { _pageTitle = value; PropertyChanged(this, new PropertyChangedEventArgs("PageTitle")); } } ... #endregion ... } }
这里又偷了个小懒,由于不想每次判断事件是否被注册,因此,事件声明的时候,就给它加了个匿名方法,也省得考虑什么线程安全等麻烦事了。
由于我们期望在主页面载入的时候就自动加载班级的页面,因此,我们在MainPage的构造函数里添加少许代码:
public partial class MainPage : UserControl { public MainPage() { InitializeComponent(); InitializeDataBind(); } private void InitializeDataBind() { var mainPageViewModel = new MainPageViewModel(); this.DataContext = mainPageViewModel; mainPageViewModel.NavigateToClasses(); } }
我们首先创建了一个MainPageViewModel的实例作为本页的ViewModel赋给DataContext,然后,调用其NavigateToClasses,让其加载班级页。
另外一种比较典型的情况是,用户点击按钮,调用方法改变界面状态。例如我们在School页面里的“Back”按钮。
三、调用Model,实现方法
我们是想着让MainPage来显示班级视图,但实际上,这个方法还没有实现。让我们来看一下其实现:
using System.ComponentModel; using SilverlightNotes.Navigate.Views; using SilverlightNotes.Navigate.Models.Entities; namespace SilverlightNotes.Navigate.ViewModels { public class MainPageViewModel : INotifyPropertyChanged { #region Construction private ClassesView _classesViewCache; public MainPageViewModel() { PageTitle = "MVVM Navigation Demo"; } #endregion #region Properties ... #endregion #region Faked Commands public void NavigateToClasses() { if (_classesViewCache == null) { ClassViewModel classViewModel = new ClassViewModel(); ClassesView classesView = new ClassesView(); classesView.DataContext = classViewModel; _classesViewCache = classesView; DisplayContent = classesView; } else { DisplayContent = _classesViewCache; } } public void NavigateToStudent(Classes selectedClass) { ... } #endregion } }
首先,检查了一下有没有页面的缓存,如果没有,那么创建一个新的页面对象和它对应的ViewModel,设定好DataContext以后,我们就重新设置DisplayContent属性。由于DisplayContent属性会触发“EventChanged”事件,界面会回应此事件作出相应的变动。
这个页面由于没有涉及到具体后来数据的操作,因此,并没有直接调用Model里的服务。我们再来看一下比较典型的ViewModel:
using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using SilverlightNotes.Navigate.Models; using SilverlightNotes.Navigate.Models.Entities; using SilverlightNotes.Navigate.Models.Interfaces; namespace SilverlightNotes.Navigate.ViewModels { public class ClassViewModel:INotifyPropertyChanged { public ClassViewModel() { Data = new ObservableCollection<Classes>(); } #region Data public ObservableCollection<Classes> Data { get; protected set; } #endregion #region Facked Commands public virtual void ShowData() { //clean original data first Data.Clear(); //Get data IClassesService classService = ServiceProvider.GetClassesService(); //Add them into the Observable collection foreach (var item in classService.GetClasses()) { Data.Add(item); } } #endregion public event PropertyChangedEventHandler PropertyChanged = delegate { }; } }
Data属性即对外暴露的数据集。ShowData方法中,首先清空原来Data中的数据;然后,创建了一个实现IClassService的服务对象。最后,把数据项一一更新到Data集合里去。我们再次看到,由于ViewModel和View是绑定在一起的,因此,我们在写代码的时候,不需要去考虑页面的更新。
意外
本来,这个Demo到此已经全部结束,运行一下,出现却得到一个十分诡异的异常——AG_E_RUNTIME_MANAGED_UNKNOWN_ERROR:
看上去像是XAML的解析出了问题,跟着行列到MainPage.xaml里找了一通,也没看出什么问题来。G了一下,才知道是Broder.Child属性不能正常绑定。应该是一个Silverlight的Bug。这下晕了,这样的话,如果要用ViewModel来控制Navigation,就得在ViewModel里设置页面上“Border.Child”属性,这下子View和ViewModel由绑定这种较松的耦合变成代码的强耦合……后来考虑了一下,借鉴INotifyProperty接口的实现方法,在MainPageViewModel的类里添加一个事件,当DisplayContent修改时,触发这个事件。在View里只需要少量的代码,就可以实现类似于单向绑定的效果:
修改后的MainPageViewModel类:
using System.ComponentModel; using SilverlightNotes.Navigate.Views; using SilverlightNotes.Navigate.Models.Entities; namespace SilverlightNotes.Navigate.ViewModels { public class MainPageViewModel : INotifyPropertyChanged { #region Events /// <summary> /// Provide to inform observers that DisplayContent changed we can't bind a user control to a child of another control. /// </summary> public event EventHandler DisplayContentChanged = delegate { }; public event PropertyChangedEventHandler PropertyChanged = delegate { }; #endregion #region Construction private ClassesView _classesViewCache; public MainPageViewModel() { PageTitle = "MVVM Navigation Demo"; } #endregion #region Properties private string _pageTitle; public string PageTitle { ... } private UIElement _displayContent; public UIElement DisplayContent { get { return _displayContent; } set { _displayContent = value; PropertyChanged(this, new PropertyChangedEventArgs("DisplayContent")); DisplayContentChanged(this, new EventArgs()); } } #endregion #region Faked Commands public void NavigateToClasses() { ... } public void NavigateToStudent(Classes selectedClass) { ... } #endregion } }
另外,在MainPage里,也需要做一点点的小功课——谁让绑定不能用呢:
using SilverlightNotes.Navigate.ViewModels; namespace SilverlightNotes.Navigate { public partial class MainPage : UserControl { public MainPage() { InitializeComponent(); InitializeDataBind(); } private void InitializeDataBind() { var mainPageViewModel = new MainPageViewModel(); this.DataContext = mainPageViewModel; mainPageViewModel.DisplayContentChanged += new EventHandler(mainPageViewModel_DisplayContentChanged); mainPageViewModel.NavigateToClasses(); } private void mainPageViewModel_DisplayContentChanged(object sender, EventArgs e) { MainPageViewModel mainPageViewModel = this.DataContext as MainPageViewModel; if (mainPageViewModel != null) { this.Dispatcher.BeginInvoke( delegate { bdrPlaceHolder.Child = mainPageViewModel.DisplayContent; }); } } } }
-
写在最后
MVVM模式原生应用于WPF,由于Silverlight可以看作是WPF的子集,这一模式同样可以较好的应用于Silverlight。但是由于Silverlight的不成熟,还存在一些BUG,导致模式中有一些部分不能够正常应用。但是,我们可以通过一些Work-around,一些灵活处理,在尽可能多的利用模式给我们带来的便利的同时,完成程序的全部功能。
Technorati Tags: Silverlight,.NET,MVVM,Pattern
Little knowledge is dangerous.