Silverlight 学习笔记——MVVM模式实现主从数据显示

转自http://www.cnblogs.com/xiaomi7732/archive/2010/01/21/1653482.html

 写本篇纯属意外。原来想用主从数据显示的例子记录页面间切换的方法的,后来在园子里看到有一篇写页面切换的文章介绍得很详尽了,代码做了一半,真是鸡肋啊。于是想,干脆把代码改改,弄成个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.

 

时间: 2024-11-17 22:00:31

Silverlight 学习笔记——MVVM模式实现主从数据显示的相关文章

WPF + Silverlight学习笔记

WPF and Silverlight学习笔记(三十):Brush(2) WPF and Silverlight学习笔记(二十九):Brush(1) WPF and Silverlight学习笔记(二十八):基本图形的使用(3)图 WPF and Silverlight学习笔记(二十七):基本图形的使用(2)Pa WPF and Silverlight学习笔记(二十六):基本图形使用(1) WPF and Silverlight学习笔记(二十五) WPF and Silverlight学习笔记(

WPF and Silverlight学习笔记(二十五)

WPF and Silverlight学习笔记(二十五):使用CollectionView实现对绑定数据的排序.筛选.分组 在第二十三节,我们使用CollectionView实现了对于绑定数据的导航,除导 航功能外,还可以通过CollectionView对数据进行类似于DataView的排序.筛选 等功能. 一.数据的排序: 使用第二十四节的数据源,查询所有 的产品信息: 1: <Window x:Class="WPF_24.CollectionViewSortData" 2:

WPF and Silverlight学习笔记(十二)

WPF and Silverlight学习笔记(十二):WPF Panel内容模型.Decorator内容模型及其他 一.Panel内容模型 Panel内容模型指从 System.Windows.Controls.Panel继承的控件,这些控件都是容器,可以在内部 承载其他的控件和子容器.Panel内容模型包含的容器有: Canvas DockPanel Grid TabPanel ToolBarO verflowPanel UniformGrid StackPanel ToolBarPanel

WPF and Silverlight学习笔记(七)

WPF and Silverlight学习笔记(七):WPF布局管理之StackPanel.WrapPanel.DockPanel 一.StackPanel StackPanel是以堆叠的方式显示其中的控件 1 .可以使用Orientation属性更改堆叠的顺序 Orientation="Vertical" 默认,由上到下显示各控件 .控件在未定义的前提下,宽度为StackPanel的宽度,高度自动适应控件中内容 的高度 1: <StackPanel Orientation=&q

ArcGIS API for Silverlight学习笔记

ArcGIS API for Silverlight学习笔记(一):为什么要用Silverlight API(转) 你用上3G手机了吗?你可能会说,我就是喜欢用nokia1100,ABCDEFG跟我都没关系.但你不能否认3G是一种趋势,最终我们每个人都会 被包裹在3G网络中.1100也不是一成不变,没准哪天为了打击犯罪,会在你的1100上强制装上GPS.GIS工作既然建立在计算机的基础上,当然也得 随着IT行业与时俱进.       看看现在计算机应用的趋势吧.云(计算),这个东西可讲不清楚,因

WPF and Silverlight学习笔记(二十):WPF数据绑定概述

WPF数据绑定为应用程序提供了一种表示数据和与数据交互的简单而又一致的 方法.元素能够以公共语言运行库 (CLR) 对象和 XML 的形式绑定到各种数据源 中的数据. 一.数据绑定的基本概念: 数据绑定涉及到两个方面 :一个是绑定源,再一个是绑定目标.绑定源即控件绑定所使用的源数据,绑定 目标即数据显示的控件. 1.对于绑定源,在WPF可以是以下四种: CLR对象:可以绑定到CLR类的公开的属性.子属性.索引器上 ADO.Net对象:例如DataTable.DataView等 XML文件:使用X

WPF and Silverlight学习笔记(一):开发环境及参考资料

前一段时间一直很忙很忙,从4月份开始终于有时间学习一些新的东西了.回 头一看,要学习整理的东西太多了:WPF.WCF.WF.Silverlight.JQuery. Ajax.ASP.Net MVC.ADO.Net Entry-想了想,终于决定先学习整理一下 WPF和Silverlight的内容.并与园子里的朋友分享. 笔者的开发环境如 下: 操作系统:Vista+SP1 英文版 开发工具:Visual Studio 2008+SP1英文版 Silverlight 3.0 Beta Silverl

WPF and Silverlight学习笔记(二十九):Brush(1)

在WPF和Silverlight中,Brush是应用很多的一种类型,主要用于填充各种图 形及控件.Brush及其子类位于System.Windows.Media命名空间,其继承关系如 下图所示: 一.SolidColorBrush SolidColorBrush是最简单的一种Brush,包含 一个Color属性,表示单色的画刷,例如使用红色填充一个矩形: 1: <Rectangle Margin="5" Height="50"> 2: <Recta

WPF and Silverlight学习笔记(二十二):使用代码实现绑定、绑定数据的验证

一.通过代码实现数据绑定 通过代码实现数据绑定,使用的是 System.Windows.Data命名空间的Binding类,主要使用Binding类的如下的属性 : Source属性:绑定到的数据源 Mode属性:绑定的模式 (OneTime.OneWay.TwoWay.OneWayToSource或Default) Path属性: 绑定到的数据源的属性 Converter属性:绑定时所使用的类型转换器 在绑定目标控件上使用SetBinding方法添加数据绑定.例如将MyData的 Name属性