寫本篇純屬意外。原來想用主從資料顯示的例子記錄頁面間切換的方法的,後來在園子裡看到有一篇寫頁面切換的文章介紹得很詳盡了,代碼做了一半,真是雞肋啊。於是想,乾脆把代碼改改,弄成個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,一些靈活處理,在儘可能多的利用模式給我們帶來的便利的同時,完成程式的全部功能。
本文來自CSDN部落格,轉載請標明出處:http://blog.csdn.net/rise51/archive/2011/03/09/6234351.aspx