MVVM是wp7开发的一种设计模式,其目的也是为了将数据层(Model)与UI层(View)分开。ViewModel则是用来连接数据层与UI层的C#类。个人感觉,MVVM和MVC是差不多的。下面就用微软的sample code来理解下MVVM 设计模式。
这个例子是一个游戏成就的记录工具,分为两个部分:收集品成就,以及等级成就。
收集品成就 又分为药品数量,硬币数量,心数量。
等级成就 有1,2,3三个等级。
游戏效果如下
下面讲实现代码,先建立一个Silverlight for Windows Phone工程。
solution结构如下
建立Model层类
using System; using System.ComponentModel; namespace MVVMTestApp.Model { //实现INotifyPropertyChanged接口的目的是,当Property改变的时候通知view刷新。 public class Accomplishment : INotifyPropertyChanged { // 成就名. public string Name { get; set; } // 成就种类. public string Type { get; set; } // 每种收集品的名称. private int _count; public int Count { get { return _count; } set { _count = value; //当收集品数量改变时触发属性改变事件 RaisePropertyChanged("Count"); } } // 等级是否完成 private bool _completed; public bool Completed { get { return _completed; } set { _completed = value; //当等级成就完成度改变时触发属性改变事件 RaisePropertyChanged("Completed"); } } public event PropertyChangedEventHandler PropertyChanged; private void RaisePropertyChanged(string propertyName) { if (this.PropertyChanged != null) { this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } // Create a copy of an accomplishment to save. // 克隆出一个对象,但是不包括已绑定的数据 public Accomplishment GetCopy() { Accomplishment copy = (Accomplishment)this.MemberwiseClone(); return copy; } } }
建立ViewModel层类
using System; using System.Windows; using System.Collections.ObjectModel; using System.IO.IsolatedStorage; using MVVMTestApp.Model; namespace MVVMTestApp.ViewModelNamespace { /* 该类的作用是,建立一条用以存储成就元素的链表,并提供初期化,存储,取得链表的方法 */ public class ViewModel { //这种容器的特点是当发生item的增删改时会触发事件 public ObservableCollection<Accomplishment> Accomplishments { get; set; } public void GetAccomplishments() { if (IsolatedStorageSettings.ApplicationSettings.Count > 0) { GetSavedAccomplishments(); } else { GetDefaultAccomplishments(); } } public void GetDefaultAccomplishments() { ObservableCollection<Accomplishment> a = new ObservableCollection<Accomplishment>(); // Items to collect a.Add(new Accomplishment() { Name = "Potions", Type = "Item" }); a.Add(new Accomplishment() { Name = "Coins", Type = "Item" }); a.Add(new Accomplishment() { Name = "Hearts", Type = "Item" }); a.Add(new Accomplishment() { Name = "Swords", Type = "Item" }); a.Add(new Accomplishment() { Name = "Shields", Type = "Item" }); // Levels to complete a.Add(new Accomplishment() { Name = "Level 1", Type = "Level" }); a.Add(new Accomplishment() { Name = "Level 2", Type = "Level" }); a.Add(new Accomplishment() { Name = "Level 3", Type = "Level" }); Accomplishments = a; //MessageBox.Show("Got accomplishments from default"); } public void GetSavedAccomplishments() { ObservableCollection<Accomplishment> a = new ObservableCollection<Accomplishment>(); foreach (Object o in IsolatedStorageSettings.ApplicationSettings.Values) { a.Add((Accomplishment)o); } Accomplishments = a; //MessageBox.Show("Got accomplishments from storage"); } public void SaveAccomplishments() { IsolatedStorageSettings settings = IsolatedStorageSettings.ApplicationSettings; foreach (Accomplishment a in Accomplishments) { if (settings.Contains(a.Name)) { settings[a.Name] = a; } else { settings.Add(a.Name, a.GetCopy()); } } settings.Save(); MessageBox.Show("Finished saving accomplishments"); } } }
建立两个新View,ItemView.xaml(收集品的表示区域)和LevelView.xaml(等级的表示区域)
编辑ItemView.xaml,在GRID element里添加以下source
<ListBox ItemsSource="{Binding}"> <ListBox.ItemTemplate> <DataTemplate> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="200"/> <ColumnDefinition Width="80"/> <ColumnDefinition Width="100"/> </Grid.ColumnDefinitions> <!--Mode 属性用于定义绑定模式,它将决定数据如何在源和目标之间流动。除 OneWay 之外,还有另外三种绑定模式:OneTime、OneWayToSource 和 TwoWay。 正如前面的代码段中所示,使用 OneWay 绑定时,每当源发生变化,数据就会从源流向目标。尽管我在示例中显式指定了此绑定模式,但其实 OneWay 绑定是 TextBlock 的 Text 属性的默认绑定模式,无需对其指定。和 OneWay 绑定一样,OneTime 绑定也会将数据从源发送到目标;但是,仅当启动了应用程序或 DataContext 发生更改时才会如此操作,因此,它不会侦听源中的更改通知。与 OneWay 和 OneTime 绑定不同,OneWayToSource 绑定会将数据从目标发送到源。最后,TwoWay 绑定会将源数据发送到目标,但如果目标属性的值发生变化,则会将它们发回给源。 确定依赖项属性绑定在默认情况下是单向还是双向的编程方法是:使用 GetMetadata 来获取属性的属性元数据,然后检查 BindsTwoWayByDefault 属性的布尔值。 --> <TextBlock x:Name="Item" Text="{Binding Path=Name, Mode=OneWay}" Grid.Column="0" HorizontalAlignment="Left" VerticalAlignment="Center" /> <TextBox x:Name="Count" Text="{Binding Path=Count, Mode=TwoWay}" Grid.Column="1" TextAlignment="Center" InputScope="Number"/> <TextBlock x:Name="Check" Text="{Binding Path=Count, Mode=OneWay}" Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Grid> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
建立第二个view
在头部的UserControl增加以下代码。这里表示要引用MVVMTestApp.View的source。这里:src是自定义的tag名,也可以用别的命名。
下面调用BoolOpposite的时候要跟前缀src:。
xmlns:src="clr-namespace:MVVMTestApp.View"
在UserControl 和 GRID 之间, 增加以下代码,这里添加source的目的是增加一个Converter。
因为开发者认为等级成就完成后,是不能退回去的。也就是说当等级成就为false时,该等级是可编辑的。
但等级变成true以后就不能再编辑了。
<UserControl.Resources>
<src:BoolOpposite x:Key="BoolOpposite"/>
</UserControl.Resources>
在GRID element里添加以下source
<ListBox ItemsSource="{Binding}"> <ListBox.ItemTemplate> <DataTemplate> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="200"/> <ColumnDefinition Width="80"/> <ColumnDefinition Width="100"/> </Grid.ColumnDefinitions> <TextBlock x:Name="Level" Text="{Binding Path=Name, Mode=OneWay}" Grid.Column="0" HorizontalAlignment="Left" VerticalAlignment="Center"/> <CheckBox x:Name="Completed" IsChecked="{Binding Path=Completed, Mode=TwoWay}" Grid.Column="1" HorizontalAlignment="Center" IsEnabled="{Binding Path=Completed, Converter={StaticResource BoolOpposite}}"/> <TextBlock x:Name="Check" Text="{Binding Path=Completed, Mode=OneWay}" Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Center"/> </Grid> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
这里,CheckBox是否钩上,以及能否编辑都与Completed属性绑定,但是能否编辑增加了反转Completed属性的Converter:BoolOpposite
<CheckBox x:Name="Completed" IsChecked="{Binding Path=Completed, Mode=TwoWay}" Grid.Column="1" HorizontalAlignment="Center" IsEnabled="{Binding Path=Completed, Converter={StaticResource BoolOpposite}}"/>
将第二个View里,UI层的对应代码替换如下:
Copy using System; using System.Windows.Controls; using System.Globalization; namespace MVVMTestApp.View { public partial class LevelView : UserControl { public LevelView() { InitializeComponent(); } } public class BoolOpposite : System.Windows.Data.IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { bool b = (bool)value; return !b; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { string s = value as string; bool b; if (bool.TryParse(s, out b)) { return !b; } return false; } } }
编辑主View,在phone tag里添加一下代码:
xmlns:views="clr-namespace:MVVMTestApp.View"
在ContentPanel GRID element里,增加以下属性
<!--ContentPanel - place additional content here-->
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"></Grid>
替换ContentPanel GRID element 的默认代码如下
<TextBlock Text="Levels Completed" Foreground="{StaticResource PhoneAccentBrush}" Style="{StaticResource PhoneTextLargeStyle}" />
<views:LevelView x:Name="LevelViewOnPage" Height="200"/>
</StackPanel>
</Grid>
修改主View对应的UI层代码
using System; using System.Linq; using System.Windows; using Microsoft.Phone.Controls; using MVVMTestApp.ViewModelNamespace; namespace MVVMTestApp { public partial class MainPage : PhoneApplicationPage { private ViewModel vm; // Constructor public MainPage() { InitializeComponent(); vm = new ViewModel(); } protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e) { base.OnNavigatedTo(e); // Later, you will replace this next line with something better. vm.GetAccomplishments(); // There are two different views, but only one view model. // 这里使用Linq从.Accomplishments里检索出Accomplishment集,绑定到ListBox控件 // Set the data context for the Item view. // from 临时item对象 in item容器 where 条件 select 临时item对象里的值 ItemViewOnPage.DataContext = from Accomplishment in vm.Accomplishments where Accomplishment.Type == "Item" select Accomplishment; // Set the data context for the Level view. LevelViewOnPage.DataContext = from Accomplishment in vm.Accomplishments where Accomplishment.Type == "Level" select Accomplishment; // If there is only one view, you could use the following code // to populate the view. //AccomplishmentViewOnPage.DataContext = vm.Accomplishments; } } }
增加一个类用以维护页面的状态。
using System; namespace MVVMTestApp { public static class StateUtilities { private static Boolean isLaunching; public static Boolean IsLaunching { get { return isLaunching; } set { isLaunching = value; } } } }
修改App.xaml对应的Code,替换如下
private void Application_Launching(object sender, LaunchingEventArgs e) { StateUtilities.IsLaunching = true; } private void Application_Activated(object sender, ActivatedEventArgs e) { StateUtilities.IsLaunching = false; }
主View 的对应Code,替换如下:
// Old instance of the application // The user started the application from the Back button. if (!StateUtilities.IsLaunching && this.State.ContainsKey("Accomplishments")) { vm = (ViewModel)this.State["Accomplishments"]; //MessageBox.Show("Got data from state"); } // New instance of the application // The user started the application from the application list, // or there is no saved state available. else { vm.GetAccomplishments(); //MessageBox.Show("Did not get data from state"); }
在OnNavigatedTo 函数后增加以下方法,当State里没有Accomplishments属性时,增添Accomplishments属性,否则替换它。
protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e) { base.OnNavigatedFrom(e); if (this.State.ContainsKey("Accomplishments")) { this.State["Accomplishments"] = vm; } else { this.State.Add("Accomplishments", vm); }增加application bar, OnNavigatedFrom method后面添加一下代码
在MainPage.xaml 里,把</phone:PhoneApplicationPage.ApplicationBar>-->替换如下:private void AppBarSave_Click(object sender, EventArgs e) { vm.SaveAccomplishments(); }<phone:PhoneApplicationPage.ApplicationBar> <shell:ApplicationBar IsVisible="True" IsMenuEnabled="True" > <shell:ApplicationBarIconButton IconUri="AppBarSave.png" Text="Save" Click="AppBarSave_Click" /> </shell:ApplicationBar> </phone:PhoneApplicationPage.ApplicationBar>
完成!