轉帖|其它|編輯:郝浩|2011-03-11 10:25:51.000|閱讀 827 次
概述:原來想用主從數據顯示的例子記錄頁面間切換的方法的,后來在園子里看到有一篇寫頁面切換的文章介紹得很詳盡了,代碼做了一半,真是雞肋啊。于是想,干脆把代碼改改,弄成個MVVM模式來展示主從數據吧。
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
原來想用主從數據顯示的例子記錄頁面間切換的方法的,后來在園子里看到有一篇寫頁面切換的文章介紹得很詳盡了,代碼做了一半,真是雞肋啊。于是想,干脆把代碼改改,弄成個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類型的集合。以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,一些靈活處理,在盡可能多的利用模式給我們帶來的便利的同時,完成程序的全部功能。
本站文章除注明轉載外,均為本站原創或翻譯。歡迎任何形式的轉載,但請務必注明出處、不得修改原文相關鏈接,如果存在內容上的異議請郵件反饋至chenjj@fc6vip.cn
文章轉載自:網絡轉載