在UWP中实现自己的MVVM设计模式
其实写这篇博文的时候我是拒绝的,因为这牵扯到一个高大上的东西——"框架"。一说起这个东西,很多朋友就感觉有点蒙了,尤其是编程新手。因为它不像在代码里面定义一个变量那么显而易见,它是需要在你的整个程序架构上体现出来的,并且对于框架来说,并没有什么固定的代码格式,你可以这样写,当然也可以那样写。只要最终可以达到同样的效果,各个模块之间能够体现这种框架的思想就OK。所以当你都是用MVVM框得到两份架写的相同需求的Demo看时,发现里面的很多代码都不一样,请不要惊讶,因为你正在接触一个很抽象的东西,这种东西有的时候还真得你需要自己挖空心思去琢磨一下,光靠别人给你讲还是不行的!
--------------------------------切入正题--------------------------------
在进行搭建自己的MVVM框架的时候你需要提起掌握一下知识(至少要熟悉,如果未达标,建议先自行脑补一下,我可能不会做到面面俱到):
1、熟练掌握数据绑定;
2、熟练使用委托;
3、对MVVM框架有一定的了解;
--------------------------------在你决定继续要往下看的时候我会默认你已经对上述知识有所了解------------------------------
一:为页面绑定数据
按照规范的MVVM框架来说,一个项目中至少要有要有三个文件夹:View、ViewModel、Model;这三个文件夹分别对应该框架的三个组成部分,这一点没什么好说的。针对Model中的一些属性而言,如果想具有属性通知的功能的话就需要继承INotifyPropertyChanged接口,并需要自定义一个函数用于触发对应的PropertyChanged事件,一般情况下我们都会把这一部分封装到一个类中,供其它类来继承它。这样就避免出现代码冗余的问题。示例代码如下所示:
1 public class ObservableObject : INotifyPropertyChanged 2 { 3 public event PropertyChangedEventHandler PropertyChanged; 4 public void RaisePropertyChanged(string propertyName) 5 { 6 if (PropertyChanged != null) 7 { 8 PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 9 } 10 } 11 }
接下来,我们就需要让对应的Model类来继承我们写的这个具有属性通知的类,示例代码如下所示:
1 public class User:ObservableObject 2 { 3 private string _name; 4 public string Name 5 { 6 get { return _name; } 7 set 8 { 9 _name = value; 10 RaisePropertyChanged("Name"); 11 } 12 } 13 14 private int _age; 15 16 public int Age 17 { 18 get { return _age; } 19 set 20 { 21 _age = value; 22 RaisePropertyChanged("Age"); 23 } 24 } 25 26 public User(string name,int age) 27 { 28 this.Name = name; 29 this.Age = age; 30 } 31 32 /// <summary> 33 /// 给ViewModel提供一个获取Model中集合对象的接口 34 /// </summary> 35 /// <returns></returns> 36 public static ObservableCollection<User> GetUsers() 37 { 38 return new ObservableCollection<User>() 39 { 40 new User("hippieZhou",23), 41 new User("小明",12), 42 new User("老王",50) 43 }; 44 } 45 }
Model已经搭建完成,接着我们就开始搭建ViewModel,示例代码如下所示:
1 public class MainPageViewModel : ObservableObject 2 { 3 private ObservableCollection<User> _users; 4 public ObservableCollection<User> Users 5 { 6 get { return _users; } 7 set 8 { 9 _users = value; 10 RaisePropertyChanged("Users"); 11 } 12 } 13 14 public MainPageViewModel() 15 { 16 this.Users = User.GetUsers(); 17 } 18 }
OK,是不是很简单,接下来就是View中的数据绑定,示例代码如下所示:
1 <Page.DataContext> 2 <vm:MainPageViewModel/> 3 </Page.DataContext> 4 5 <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> 6 <ListView x:Name="lv" Grid.Row="1" ItemsSource="{Binding Users}"> 7 <ListView.ItemTemplate> 8 <DataTemplate> 9 <ListViewItem> 10 <Grid> 11 <Grid.ColumnDefinitions> 12 <ColumnDefinition Width="200"/> 13 <ColumnDefinition Width="*"/> 14 </Grid.ColumnDefinitions> 15 <TextBlock Text="{Binding Name}" Grid.Column="0"/> 16 <TextBlock Text="{Binding Age}" Grid.Column="1"/> 17 </Grid> 18 </ListViewItem> 19 </DataTemplate> 20 </ListView.ItemTemplate> 21 </ListView> 22 </Grid>
二:为页面绑定Command
写到这算是完成了1/3,界面是展示了数据,但是我们不能对它进行任何的操作,因此我们还需要让数据能够动态的增加和删除,接下来我们需要使用有关Command的相关知识了,首先,Command属于ButtonBase的一个字段,如果我们想为对应的Command进行绑定的话,那就需要这个绑定源对应的委托继承自ICommand接口,需要重写ICommand对应的两个接口函数。其次,由于ICommand提供了两个接口函数CanExecute和Execute,因此当CanExecute为false时候Execute是不被执行,此时绑定的Command是失效的,那么对应的控件应该自动处于禁用状态的,但是在WindowsStore类型的应用不再像WPF那样具有CommandManager的功能,不能自动触发CanExecuteChanged,这样就导致控件的颜色仍然不是禁用状态的颜色(尽管没有执行对应的函数),因此我们需要手动触发这个事件,来保证前台的控件的显示状态在指定的条件下发生改变。
在此,我们一般会采取封装的思想来处理这种情况,因此我选择封装一个类DelegateCommand,继承至ICommand,示例代码如下所示:
1 public sealed class DelegateCommand : ICommand 2 { 3 public event EventHandler CanExecuteChanged; 4 /// <summary> 5 /// 需要手动触发属性改变事件 6 /// </summary> 7 public void RaiseCanExecuteChanged() 8 { 9 if (CanExecuteChanged != null) 10 { 11 CanExecuteChanged(this, EventArgs.Empty); 12 } 13 } 14 15 /// <summary> 16 /// 决定当前绑定的Command能否被执行 17 /// true:可以被执行 18 /// false:不能被执行 19 /// </summary> 20 /// <param name="parameter">不是必须的,可以依据情况来决定,或者重写一个对应的无参函数</param> 21 /// <returns></returns> 22 public bool CanExecute(object parameter) 23 { 24 return this.MyCanExecute == null ? true : this.MyCanExecute(parameter); 25 } 26 27 /// <summary> 28 /// 用于执行对应的命令,只有在CanExecute可以返回true的情况下才可以被执行 29 /// </summary> 30 /// <param name="parameter"></param> 31 public void Execute(object parameter) 32 { 33 try 34 { 35 this.MyExecute(parameter); 36 } 37 catch (Exception ex) 38 { 39 #if DEBUG 40 41 Debug.WriteLine(ex.Message); 42 43 #endif 44 } 45 } 46 47 /// <summary> 48 /// 49 /// </summary> 50 public Action<Object> MyExecute { get; set; } 51 public Func<Object, bool> MyCanExecute { get; set; } 52 53 /// <summary> 54 /// 构造函数,用于初始化 55 /// </summary> 56 /// <param name="execute"></param> 57 /// <param name="canExecute"></param> 58 public DelegateCommand(Action<Object> execute, Func<Object, bool> canExecute) 59 { 60 this.MyExecute = execute; 61 this.MyCanExecute = canExecute; 62 } 63 }
然后在我们的ViewModel中创建对应的Command就可以了,我们可以将ViewModel改造成下面这个样子:
1 public class MainPageViewModel : ObservableObject 2 { 3 private ObservableCollection<User> _users; 4 public ObservableCollection<User> Users 5 { 6 get { return _users; } 7 set 8 { 9 _users = value; 10 RaisePropertyChanged("Users"); 11 } 12 } 13 14 private DelegateCommand _addCommand; 15 16 /// <summary> 17 /// 当当前集合项的个数小于5时允许用户继续添加,否则就不允许用户添加 18 /// </summary> 19 public DelegateCommand AddCommand 20 { 21 get 22 { 23 return _addCommand ?? (_addCommand = new DelegateCommand 24 ((Object obj) => 25 { 26 //添加一条记录 27 this.Users.Add(new User(DateTime.Now.ToString(),DateTime.Now.Hour)); 28 //手动触发CanExecuteChanged事件来改变对应控件的显示状态 29 this._addCommand.RaiseCanExecuteChanged(); 30 this._delCommand.RaiseCanExecuteChanged(); 31 }, 32 (Object obj) => this.Users.Count < 5)); 33 } 34 } 35 36 /// <summary> 37 /// 当当前集合项的个数大于1时允许用户继续删除,否则就不允许用户删除 38 /// </summary> 39 private DelegateCommand _delCommand; 40 public DelegateCommand DelCommand 41 { 42 get 43 { 44 return _delCommand ?? (_delCommand = 45 new DelegateCommand((Object obj) => 46 { 47 //删除一条记录 48 this.Users.RemoveAt(0); 49 //手动触发CanExecuteChanged事件来改变对应控件的显示状态 50 this._addCommand.RaiseCanExecuteChanged(); 51 this._delCommand.RaiseCanExecuteChanged(); 52 }, 53 (Object obj) => this.Users.Count > 1)); 54 } 55 } 56 57 public MainPageViewModel() 58 { 59 this.Users = User.GetUsers(); 60 } 61 }
并将对应的View改造成下面这个样子:
1 <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> 2 <Grid.RowDefinitions> 3 <RowDefinition Height="Auto"/> 4 <RowDefinition Height="*"/> 5 </Grid.RowDefinitions> 6 <StackPanel Grid.Row="0" HorizontalAlignment="Center" Width="100"> 7 <Button Content="Add" Command="{Binding AddCommand}" HorizontalAlignment="Stretch" Margin="6"/> 8 <Button Content="Del" Command="{Binding DelCommand}" HorizontalAlignment="Stretch" Margin="6"/> 9 </StackPanel> 10 <ListView x:Name="lv" Grid.Row="1" ItemsSource="{Binding Users}"> 11 <ListView.ItemTemplate> 12 <DataTemplate> 13 <ListViewItem> 14 <Grid> 15 <Grid.ColumnDefinitions> 16 <ColumnDefinition Width="200"/> 17 <ColumnDefinition Width="*"/> 18 </Grid.ColumnDefinitions> 19 <TextBlock Text="{Binding Name}" Grid.Column="0"/> 20 <TextBlock Text="{Binding Age}" Grid.Column="1"/> 21 </Grid> 22 </ListViewItem> 23 </DataTemplate> 24 </ListView.ItemTemplate> 25 </ListView> 26 </Grid>
这个地方提醒新手朋友要注意的一个问题,如果你希望你的控件在指定条件下显示的状态不一样就需要手动触发CanExecuteChanged事件。
推荐链接:Implement ICommand.CanExecuteChanged in portable class library (PCL)
Re-enabling the CommandManager feature with RelayCommand in MVVM Light V5
三:Event To Command
接下来算是一个重点内容吧,如何将一个事件绑定到Command上? 这个问题很现实,并不是所有的控件都有Command属性,当一个控件只有Event而没有Command我们该怎么办?
我们现在需求是选中一项后弹出一个对话框,显示你选中项的相关信息(通过EventToCommand来实现)
这个微软为我们提供了一个解决方案,如果你安装了Blend工具,你可以把 目录C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1\ExtensionSDKs\BehaviorsXamlSDKManaged\12.0打开,你会发现有两个动态库很有用:Microsoft.Xaml.Interactions.dll和Microsoft.Xaml.Interactivity.dll;没错,就是它俩可以达成你的心愿,迅速将这两个动态库加入到工程中,然后你可以在你的XAML页面中进行绑定的,我把完整的代码罗列出来供大家参考:
1 <Page 2 x:Class="MVVM.MainPage" 3 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 4 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 5 xmlns:local="using:MVVM" 6 xmlns:vm="using:MVVM.ViewModel" 7 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 8 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 9 mc:Ignorable="d" 10 11 xmlns:Interactivity="using:Microsoft.Xaml.Interactivity" 12 xmlns:Core="using:Microsoft.Xaml.Interactions.Core"> 13 <!-- 14 C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1\ExtensionSDKs\BehaviorsXamlSDKManaged\12.0 15 --> 16 <Page.DataContext> 17 <vm:MainPageViewModel/> 18 </Page.DataContext> 19 20 <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> 21 <Grid.RowDefinitions> 22 <RowDefinition Height="Auto"/> 23 <RowDefinition Height="*"/> 24 </Grid.RowDefinitions> 25 <StackPanel Grid.Row="0" HorizontalAlignment="Center" Width="100"> 26 <Button Content="Add" Command="{Binding AddCommand}" HorizontalAlignment="Stretch" Margin="6"/> 27 <Button Content="Del" Command="{Binding DelCommand}" HorizontalAlignment="Stretch" Margin="6"/> 28 </StackPanel> 29 <ListView x:Name="lv" Grid.Row="1" ItemsSource="{Binding Users}"> 30 <ListView.ItemTemplate> 31 <DataTemplate> 32 <ListViewItem> 33 <Grid> 34 <Grid.ColumnDefinitions> 35 <ColumnDefinition Width="200"/> 36 <ColumnDefinition Width="*"/> 37 </Grid.ColumnDefinitions> 38 <TextBlock Text="{Binding Name}" Grid.Column="0"/> 39 <TextBlock Text="{Binding Age}" Grid.Column="1"/> 40 </Grid> 41 </ListViewItem> 42 </DataTemplate> 43 </ListView.ItemTemplate> 44 <Interactivity:Interaction.Behaviors> 45 <Core:EventTriggerBehavior EventName="SelectionChanged"> 46 <Core:InvokeCommandAction Command="{Binding ShowDialog}" CommandParameter="{Binding ElementName=lv,Path=SelectedItem,Converter={StaticResource converter}}"/> 47 </Core:EventTriggerBehavior> 48 </Interactivity:Interaction.Behaviors> 49 </ListView> 50 </Grid> 51 </Page>
这里面用到了一个非MVVM的知识:值转换器(只是为了弹出框能够显示我想要的数据而已,没什么其他的作用),示例代码如下所示:
1 /// <summary> 2 /// 定义一个值转换器,用于将绑定的数据格式化为指定的格式 3 /// </summary> 4 public class ItemConverter : IValueConverter 5 { 6 public object Convert(object value, Type targetType, object parameter, string language) 7 { 8 User user = value as User; 9 if (user != null) 10 { 11 return user.Name; 12 } 13 else 14 { 15 return "you have not select!"; 16 } 17 } 18 19 public object ConvertBack(object value, Type targetType, object parameter, string language) 20 { 21 throw new NotImplementedException(); 22 } 23 }
然后对应的命令写法和之前的是一样的,如下所示:
1 private DelegateCommand _showDialog; 2 public DelegateCommand ShowDialog 3 { 4 get 5 { 6 return _showDialog ?? (_showDialog= new DelegateCommand( 7 async (Object obj) => 8 { 9 await new Windows.UI.Popups.MessageDialog(obj.ToString()).ShowAsync(); 10 }, 11 (Object obj) => true)); 12 } 13 }
写到这,我们的MVVM框架已经搭建的差不多了,还算满意,我运行的效果是这样的(你的也是这样的吗?):
我不知道我用我的这种方式理解和设计应用程序的MVVM框架在诸位眼中是否规范,合法,还请高手不吝赐教呀:)!!!!
四:写在最后
如果你能够熟练理解并能够将MVVM运用到自己的项目中,并计划使用第三方MVVM框架的话,我建议你使用MVVMLight,简单易用上手快,并且它已经支持UWP的项目模板了。我真的很佩服作者(官网地址)的编码能力,我的很多思路都是从他的博客中获得灵感的,希望你也是如此!