zoukankan      html  css  js  c++  java
  • WPF/MVVM快速指引

    简介

    最近微软推出了UWA,又是一波新的C#+xaml学习热。好多小伙伴都对MVVM感觉很好奇,但是有些地方也有点难以理解。特意写了这边文章,希望对你有帮助。

    这边文章会很长,所以我会用几个例子的形式来展示一个小型MVVM框架的诞生以及怎样使用。所有的例子基于.net 4.0,使用的开发工具是Visual Studio Community 2013。

       

    基础知识

    1.对WPF而言最重要的一个点就是数据绑定(data binding)。简单来说,就是你有一堆数据,他们是一种类型的集合,你需要将它们展示给你的用户。所以你可以通过数据绑定的绑定到XAML上。

    2.WPF的单个界面(也就是View,通常情况下以*Window或者*Page命名)由两部分组成,它们分别是XAML和CS格式的文件。XAML设计我们的界面和动画特效等,CS写我们的后台代码。

    3.通常意义下MVVM是Model,View,ViewModel的缩写。而用这个的目的就是一个解耦的思想,也就是界面和业务逻辑的分离。当然理想状态下,我们是希望View中不要写代码的,所以我们尽量向View中没有代码这个目的靠近。

       

    关键的3个点

    1.必须使用ObservableCollection<T>来声明这个数据集合,不能使用ListT<T>或者Dictionary<TKey,TValue>。Observable意味着MVVM中的View可以观察你的集合对象。当我们数据集合变化时,界面会发生相应的变化。

    2.针对于1中所描述的T,我们必须要实现一个INotifyPropertyChanged的接口,这样我们的属性改变时,才会通知界面。

    3.每一个WPF中的控件都有一个DataContext属性,集合控件会有一个ItemSource的属性,这些属性都可以让我们去绑定数据。

       

    好了,我假设你已经有了一个大致的印象了,那接下来我们开始我们的第一个例子。

       

    Example 1:数据能够展示,但是无法更新

    我们第一个例子会用一个Song的类,它看起来是下面代码这样的:

     1 public class Song
     2 {
     3     #region 字段
     4     string _artistName;
     5     string _songTitle;
     6     #endregion
     7 
     8     #region 属性
     9     public string ArtistName
    10     {
    11         get { return _artistName; }
    12         set { _artistName = value; }
    13     }
    14 
    15     public string SongTitle
    16     {
    17         get { return _songTitle; }
    18         set { _songTitle = value; }
    19     }
    20     #endregion
    21 }

    这就是我们MVVM中的Model,接下来我们需要考虑将数据绑定到我们的View上。所以接下来的重点就应该在ViewModel上,我希望能够将ArtisName展示到界面上,所以我把ViewModel命名为SongViewModel,它的代码看上去是这样的:

     1 public class SongViewModel
     2 {
     3     public SongViewModel()
     4     {
     5         _song = new Song() { ArtistName = "陈奕迅", SongTitle = "十年" };
     6     }
     7 
     8     #region 字段
     9     Song _song;
    10     #endregion
    11 
    12     #region 属性
    13     public Song song
    14     {
    15         get { return song; }
    16         set { song = value; }
    17     }
    18 
    19     public string ArtistName
    20     {
    21         get { return _song.ArtistName; }
    22         set { _song.ArtistName = value; }
    23     }
    24     #endregion
    25 }

    接下来就是我们最神奇的地方了,我们要将ViewModel绑定到界面上。

    我们可以通过将后台代码的方式来:

    1 SongViewModel _viewModel;
    2 
    3 public MainWindow()
    4 {
    5     InitializeComponent();
    6     _viewModel = base.DataContext as SongViewModel;
    7     //_viewModel = new SongViewModel();
    8     //base.DataContext = _viewModel;
    9 }

    当然这是被允许的,但是我想强调的是更加声明式的方式。所以我决定把代码写在XAML里: 

     1 <Window x:Class="Example1.MainWindow"
     2         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     3         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     4         xmlns:local="clr-namespace:Example1"
     5         Title="Example1" Height="100" Width="300" ResizeMode="NoResize">
     6     <Window.DataContext>
     7         <local:SongViewModel/>
     8     </Window.DataContext>
     9     <StackPanel VerticalAlignment="Center" Orientation="Horizontal">
    10         <TextBlock Text="歌手:" Margin="20"/>
    11         <TextBlock Text="{Binding ArtistName}" Margin="0,20"/>
    12         <Button Content="更新歌手" Click="Update_Click" Margin="20"/>
    13     </StackPanel>
    14 </Window>

    我们声明了我们的SongViewModel,也在TextBlock中绑定了ArtistName的属性。同时写了一个更新的时间,看下我们的后台代码: 

     1 SongViewModel _viewModel;
     2 
     3 public MainWindow()
     4 {
     5     InitializeComponent();
     6     _viewModel = base.DataContext as SongViewModel;
     7     //_viewModel = new SongViewModel();
     8     //base.DataContext = _viewModel;
     9 }
    10 
    11 private void Update_Click(object sender, RoutedEventArgs e)
    12 {
    13     //界面不会更新
    14     _viewModel.ArtistName = "中孝介";
    15 }

    我们可以试着跑一下,界面上很正常的显示了我们绑定的属性,但是我写的更新按钮却没有正常的工作。

    好了我们第一个例子就结束了,下一个例子中能给我们解决更新的问题。

       

    Example 2:解决1中的问题,实现INotifyPropertyChanged接口

    在例子1中我们成功将数据绑定到了界面上,但是却无法更新,那是因为我们没有实现通知接口。好了,我们接下来给ViewModel实现这个接口。

     1 public class SongViewModel : INotifyPropertyChanged
     2 {
     3     public SongViewModel()
     4     {
     5         _song = new Song() { ArtistName = "陈奕迅", SongTitle = "十年" };
     6     }
     7 
     8     #region 字段
     9     Song _song;
    10     #endregion
    11 
    12     #region 属性
    13     public Song song
    14     {
    15         get { return song; }
    16         set { song = value; }
    17     }
    18 
    19     public string ArtistName
    20     {
    21         get { return _song.ArtistName; }
    22         set 
    23         { 
    24             _song.ArtistName = value;
    25             RaisePropertyChanged("ArtistName");
    26         }
    27     }
    28     #endregion
    29 
    30     #region INotifyPropertyChanged属性
    31     public event PropertyChangedEventHandler PropertyChanged;
    32     #endregion
    33 
    34     #region 方法
    35     private void RaisePropertyChanged(string propertyName)
    36     {
    37         PropertyChangedEventHandler handler = PropertyChanged;
    38         if(handler != null)
    39         {
    40             handler(this, new PropertyChangedEventArgs(propertyName));
    41         }
    42     }
    43     #endregion
    44 }

    我们再来运行一下我们的程序,然后点击更新按钮,如我们过预料的,它有效了。

    到目前为止,似乎一切都工作起来了,但是这并不是我们使用MVVM的正确方式。正如我在开始说的,MVVM的目的是为了解耦,分离界面和业务逻辑,所以我们要尽可能的在View后台不写代码。但是这个例子中,我们将更新ViewModel的代码写在了View里,这是不对的,下一个例子中,我们要通过命令(Command)的来将Button的事件分离出来。

       

    Example 3:更好的实现事件,通过命令的手段

    WPF提供了一个很好的方式来解决事件绑定的问题--ICommand。很多控件都有Command属性(如果没有,我们可以将命令绑定到触发器上面,当然,这超出了这篇文章的篇幅)。接下来我们来先实现一个ICommand接口。

    ICommand需要用户定义两个方法bool CanExecute和void Execute。第一个方法可以可以让我们来判断是否可以执行这个命令,第二个方法就是我们具体的命令。

     1 public class RelayCommand : ICommand
     2 {
     3 
     4     #region 字段
     5 
     6     readonly Func<Boolean> _canExecute;
     7     readonly Action _execute;
     8 
     9     #endregion
    10 
    11     #region 构造函数
    12     public RelayCommand(Action execute)
    13         : this(execute, null)
    14     {
    15     }
    16 
    17     public RelayCommand(Action execute, Func<Boolean> canExecute)
    18     {
    19 
    20         if (execute == null)
    21             throw new ArgumentNullException("execute");
    22         _execute = execute;
    23         _canExecute = canExecute;
    24     }
    25 
    26     #endregion
    27 
    28     #region ICommand的成员
    29 
    30     public event EventHandler CanExecuteChanged
    31     {
    32         add
    33         {
    34 
    35             if (_canExecute != null)
    36                 CommandManager.RequerySuggested += value;
    37         }
    38         remove
    39         {
    40 
    41             if (_canExecute != null)
    42                 CommandManager.RequerySuggested -= value;
    43         }
    44     }
    45 
    46     [DebuggerStepThrough]
    47     public Boolean CanExecute(Object parameter)
    48     {
    49         return _canExecute == null ? true : _canExecute();
    50     }
    51 
    52     public void Execute(Object parameter)
    53     {
    54         _execute();
    55     }
    56 
    57     #endregion
    58 }

    我们再在我们的ViewModel中声明一个ICommand字段:

     1 #region 命令
     2 void UpdateArtistNameExecute()
     3 {
     4     this.ArtistName = "中孝介";
     5 }
     6 
     7 bool CanUpdateArtistNameExecute()
     8 {
     9     return true;
    10 }
    11 
    12 public ICommand UpdateArtistName { get { return new RelayCommand(UpdateArtistNameExecute, CanUpdateArtistNameExecute); } }
    13 
    14 #endregion

    最后,我们再将事件绑定上这个Command:

    1 <Button Content="更新歌手" Margin="20" Command="{Binding UpdateArtistName}"/>

      

    运行一下,嗯,我们成功将事件分离了出来。

    好了,似乎目前为止我们已经很好的解决了所有的问题。我们的数据,事件都是绑定的,实现了界面的完美分离。嗯,但是我们考虑下,我们能否把MVVM提取出来作为一个框架,来去更好的解决问题。

       

    Example 4:更好的解决问题,提取MVVM

    在上一个例子中,我们已经解决了所有的问题了,这个例子中,我们将上面的写好的函数提取出来。

    我把上面的函数提取为两个主要的文件:ObserableObject和RelayCommand,因为代码和上面的类似,所以不再贴出,可以直接去看源码。

       

    Examle 5:使用ObservableCollection

    前面我们都是使用单个的Song,接下来我们尝试使用多个Song。按照我们一开始所说的,我们需要一个ObservableCollection的集合。我们用一个新的ViewModel--AlbumViewModel:

     1 public class AlbumViewModel
     2 {
     3 #region 字段
     4 ObservableCollection<Song> _songs = new ObservableCollection<Song>();
     5 #endregion
     6 
     7 #region 属性
     8 public ObservableCollection<Song> songs
     9 {
    10     get { return _songs; }
    11     set { _songs = value; }
    12 }
    13 #endregion
    14 
    15 public AlbumViewModel()
    16 {
    17     _songs.Add(new Song() { ArtistName = "陈奕迅", SongTitle = "十年" });
    18     _songs.Add(new Song() { ArtistName = "周杰伦", SongTitle = "发如雪" });
    19     _songs.Add(new Song() { ArtistName = "蔡依林", SongTitle = "日不落" });
    20 }
    21 
    22 #region 命令
    23 
    24 void AddAlbumArtistExecute()
    25 {
    26     _songs.Add(new Song { ArtistName = "阿桑", SongTitle = "一直很安静" });
    27 }
    28 
    29 bool CanAddAlbumArtistExecute()
    30 {
    31     return true;
    32 }
    33 
    34 void UpdateAlbumArtistsExecute()
    35 {
    36 
    37     foreach (var song in _songs)
    38     {
    39         song.ArtistName = "Unknow";
    40     }
    41 }
    42 
    43 bool CanUpdateAlbumArtistsExecute()
    44 {
    45     return true;
    46 }
    47 
    48 public ICommand AddAlbumArtist { get { return new RelayCommand(AddAlbumArtistExecute, CanAddAlbumArtistExecute); } }
    49 
    50 public ICommand UpdateAlbumArtists { get { return new RelayCommand(UpdateAlbumArtistsExecute, CanUpdateAlbumArtistsExecute); } }
    51 
    52 #endregion

    我们实现了两个命令,一个是新增歌手,一个是把所有集合里的SongTitle更改为Unknow。

    然后我们把这个ViewModel绑定到界面上: 

    <Window x:Class="Example5.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:Example5"
            Title="Example5" Height="300" Width="300" ResizeMode="NoResize">
        <Window.DataContext>
            <local:AlbumViewModel/>
        </Window.DataContext>
        <StackPanel Orientation="Horizontal">
            <ListView ItemsSource="{Binding songs}" Width="200">
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <Label Content="{Binding ArtistName}" />
                            <Label Content="{Binding SongTitle}" FontSize="10" />
                        </StackPanel>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
            <StackPanel>
                <Button Content="新增歌手" Height="40" Margin="20" Command="{Binding AddAlbumArtist}"/>
                <Button Content="更新歌手" Height="40" Margin="20" Command="{Binding UpdateAlbumArtists}"/>
            </StackPanel>
        </StackPanel>
    </Window>

        

    当我们运行程序的时候,我们发现我们的新增功能是正常工作的,但是我们的更新功能却没有成功把字段更改为Unkown。

    这是可以理解的。为什么?还记得开始我们说的T需要做的吗?因为我们并未有给Song实现INotifyChanged接口,它的属性变化是不会引起界面的变更的。那么我们需要给Song实现这个接口吗?我们通过这样做能实现功能,但是我们不推荐这么做。下一个例子中,我们将通过多加一个ViewModel来解决这个问题。

       

    Example 6:两个ViewModel,解决Model属性改变问题

    上个例子中,我们无法通过改变Model的属性来实现界面的更改。所以我们引入第二个ViewModel来解决问题。我们新建一个SongViewModel:

     1 public class SongViewModel : ObservableObject
     2 {
     3     public SongViewModel()
     4     {
     5         _song = new Song() { ArtistName = "Unknow", SongTitle = "Unknow" };
     6     }
     7 
     8     #region 字段
     9     Song _song;
    10     #endregion
    11 
    12     #region 属性
    13     public Song song
    14     {
    15         get { return song; }
    16         set { song = value; }
    17     }
    18 
    19     public string ArtistName
    20     {
    21         get { return _song.ArtistName; }
    22         set 
    23         { 
    24             _song.ArtistName = value;
    25             RaisePropertyChanged("ArtistName");
    26         }
    27     }
    28 
    29     public string SongTitle
    30     {
    31         get { return _song.SongTitle; }
    32         set
    33         {
    34             _song.SongTitle = value;
    35             RaisePropertyChanged("SongTitle");
    36         }
    37     }
    38     #endregion
    39 }

    然后我们用这个ViewModel来更改AlbumViewModel:

     1 public class AlbumViewModel
     2 {
     3     #region 字段
     4     ObservableCollection<SongViewModel> _songs = new ObservableCollection<SongViewModel>();
     5     #endregion
     6 
     7     #region 属性
     8     public ObservableCollection<SongViewModel> songs
     9     {
    10         get { return _songs; }
    11         set { _songs = value; }
    12     }
    13     #endregion
    14 
    15     public AlbumViewModel()
    16     {
    17         _songs.Add(new SongViewModel() { ArtistName = "陈奕迅", SongTitle = "十年" });
    18         _songs.Add(new SongViewModel() { ArtistName = "周杰伦", SongTitle = "发如雪" });
    19         _songs.Add(new SongViewModel() { ArtistName = "蔡依林", SongTitle = "日不落" });
    20     }
    21 
    22     #region 命令
    23 
    24     void AddAlbumArtistExecute()
    25     {
    26         _songs.Add(new SongViewModel { ArtistName = "阿桑", SongTitle = "一直很安静" });
    27     }
    28 
    29     bool CanAddAlbumArtistExecute()
    30     {
    31         return true;
    32     }
    33 
    34     void UpdateAlbumArtistsExecute()
    35     {
    36 
    37         foreach (var song in _songs)
    38         {
    39             song.ArtistName = "Unknow";
    40         }
    41     }
    42 
    43     bool CanUpdateAlbumArtistsExecute()
    44     {
    45         return true;
    46     }
    47 
    48     public ICommand AddAlbumArtist { get { return new RelayCommand(AddAlbumArtistExecute, CanAddAlbumArtistExecute); } }
    49 
    50     public ICommand UpdateAlbumArtists { get { return new RelayCommand(UpdateAlbumArtistsExecute, CanUpdateAlbumArtistsExecute); } }
    51 
    52     #endregion
    53 }

    我们无需更改界面上任何绑定的东西,直接运行我们的程序,这样我们发现就能工作了。

    到此为止,一个基本的MVVM模型就已经基本完成了。下一个例子我们演示如何在Command中传参数。

       

    (扩展)Example 7:Command传参数

    我们把上面例子中的更新歌手改为更新选中歌手。这样我们就需要只更改选中的歌手的值。我们需要更改界面上的绑定,来将选中的选作为传参传到Command: 

    1 <Button Content="更新选中歌手" Height="40" Margin="20" Command="{Binding UpdateAlbumArtists}" CommandParameter="{Binding ElementName=lv,Path=SelectedItem}"/>

              

    然后修改我们的AlbumViewModel中的Command:

     1 void UpdateAlbumArtistsExecute(SongViewModel song)
     2 {
     3     if(song == null) return;
     4 
     5     song.ArtistName = "Unknow";
     6 }
     7 
     8 bool CanUpdateAlbumArtistsExecute(SongViewModel song)
     9 {
    10     return true;
    11 }
    12 
    13 public ICommand AddAlbumArtist { get { return new RelayCommand(AddAlbumArtistExecute, CanAddAlbumArtistExecute); } }
    14 
    15 public ICommand UpdateAlbumArtists { get { return new RelayCommand<SongViewModel>(new Action<SongViewModel>(UpdateAlbumArtistsExecute), new Predicate<SongViewModel>(CanUpdateAlbumArtistsExecute)); } }

    这样我们很容易就实现了效果:

       

       

    结束语:

    本篇文章对MVVM的一些基本概念做了一些演示,但是还是有一些缺失,比如说控件没有Command属性时如何处理事件。只是希望能对初学者起到一定的帮助。

    最后,感谢你能看到最后。

       

    源代码下载:https://files.cnblogs.com/files/youngytj/WPFMVVMDemo.zip

  • 相关阅读:
    POJ 2513 (Trie树+欧拉通路+并查集判断连通)
    归并排序及序列逆序数
    POJ 2442 Sequence (堆+K路归并)
    POJ 2513 (Trie树+欧拉通路+并查集判断连通)
    J2EE概述
    J2EE概述
    J2EE概述
    学习视频资料下载论坛
    J2EE概述
    J2EE概述
  • 原文地址:https://www.cnblogs.com/youngytj/p/4524763.html
Copyright © 2011-2022 走看看