简介

最近微软推出了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的类,它看起来是下面代码这样的:

 public class Song
{
#region 字段
string _artistName;
string _songTitle;
#endregion #region 属性
public string ArtistName
{
get { return _artistName; }
set { _artistName = value; }
} public string SongTitle
{
get { return _songTitle; }
set { _songTitle = value; }
}
#endregion
}

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

 public class SongViewModel
{
public SongViewModel()
{
_song = new Song() { ArtistName = "陈奕迅", SongTitle = "十年" };
} #region 字段
Song _song;
#endregion #region 属性
public Song song
{
get { return song; }
set { song = value; }
} public string ArtistName
{
get { return _song.ArtistName; }
set { _song.ArtistName = value; }
}
#endregion
}

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

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

 SongViewModel _viewModel;

 public MainWindow()
{
InitializeComponent();
_viewModel = base.DataContext as SongViewModel;
//_viewModel = new SongViewModel();
//base.DataContext = _viewModel;
}

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

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

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

 SongViewModel _viewModel;

 public MainWindow()
{
InitializeComponent();
_viewModel = base.DataContext as SongViewModel;
//_viewModel = new SongViewModel();
//base.DataContext = _viewModel;
} private void Update_Click(object sender, RoutedEventArgs e)
{
//界面不会更新
_viewModel.ArtistName = "中孝介";
}

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

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

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

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

 public class SongViewModel : INotifyPropertyChanged
{
public SongViewModel()
{
_song = new Song() { ArtistName = "陈奕迅", SongTitle = "十年" };
} #region 字段
Song _song;
#endregion #region 属性
public Song song
{
get { return song; }
set { song = value; }
} public string ArtistName
{
get { return _song.ArtistName; }
set
{
_song.ArtistName = value;
RaisePropertyChanged("ArtistName");
}
}
#endregion #region INotifyPropertyChanged属性
public event PropertyChangedEventHandler PropertyChanged;
#endregion #region 方法
private void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if(handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}

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

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

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

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

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

 public class RelayCommand : ICommand
{ #region 字段 readonly Func<Boolean> _canExecute;
readonly Action _execute; #endregion #region 构造函数
public RelayCommand(Action execute)
: this(execute, null)
{
} public RelayCommand(Action execute, Func<Boolean> canExecute)
{ if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
} #endregion #region ICommand的成员 public event EventHandler CanExecuteChanged
{
add
{ if (_canExecute != null)
CommandManager.RequerySuggested += value;
}
remove
{ if (_canExecute != null)
CommandManager.RequerySuggested -= value;
}
} [DebuggerStepThrough]
public Boolean CanExecute(Object parameter)
{
return _canExecute == null ? true : _canExecute();
} public void Execute(Object parameter)
{
_execute();
} #endregion
}

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

 #region 命令
void UpdateArtistNameExecute()
{
this.ArtistName = "中孝介";
} bool CanUpdateArtistNameExecute()
{
return true;
} public ICommand UpdateArtistName { get { return new RelayCommand(UpdateArtistNameExecute, CanUpdateArtistNameExecute); } } #endregion

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

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

 

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

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

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

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

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

Examle 5:使用ObservableCollection

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

 public class AlbumViewModel
{
#region 字段
ObservableCollection<Song> _songs = new ObservableCollection<Song>();
#endregion #region 属性
public ObservableCollection<Song> songs
{
get { return _songs; }
set { _songs = value; }
}
#endregion public AlbumViewModel()
{
_songs.Add(new Song() { ArtistName = "陈奕迅", SongTitle = "十年" });
_songs.Add(new Song() { ArtistName = "周杰伦", SongTitle = "发如雪" });
_songs.Add(new Song() { ArtistName = "蔡依林", SongTitle = "日不落" });
} #region 命令 void AddAlbumArtistExecute()
{
_songs.Add(new Song { ArtistName = "阿桑", SongTitle = "一直很安静" });
} bool CanAddAlbumArtistExecute()
{
return true;
} void UpdateAlbumArtistsExecute()
{ foreach (var song in _songs)
{
song.ArtistName = "Unknow";
}
} bool CanUpdateAlbumArtistsExecute()
{
return true;
} public ICommand AddAlbumArtist { get { return new RelayCommand(AddAlbumArtistExecute, CanAddAlbumArtistExecute); } } public ICommand UpdateAlbumArtists { get { return new RelayCommand(UpdateAlbumArtistsExecute, CanUpdateAlbumArtistsExecute); } } #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:

 public class SongViewModel : ObservableObject
{
public SongViewModel()
{
_song = new Song() { ArtistName = "Unknow", SongTitle = "Unknow" };
} #region 字段
Song _song;
#endregion #region 属性
public Song song
{
get { return song; }
set { song = value; }
} public string ArtistName
{
get { return _song.ArtistName; }
set
{
_song.ArtistName = value;
RaisePropertyChanged("ArtistName");
}
} public string SongTitle
{
get { return _song.SongTitle; }
set
{
_song.SongTitle = value;
RaisePropertyChanged("SongTitle");
}
}
#endregion
}

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

 public class AlbumViewModel
{
#region 字段
ObservableCollection<SongViewModel> _songs = new ObservableCollection<SongViewModel>();
#endregion #region 属性
public ObservableCollection<SongViewModel> songs
{
get { return _songs; }
set { _songs = value; }
}
#endregion public AlbumViewModel()
{
_songs.Add(new SongViewModel() { ArtistName = "陈奕迅", SongTitle = "十年" });
_songs.Add(new SongViewModel() { ArtistName = "周杰伦", SongTitle = "发如雪" });
_songs.Add(new SongViewModel() { ArtistName = "蔡依林", SongTitle = "日不落" });
} #region 命令 void AddAlbumArtistExecute()
{
_songs.Add(new SongViewModel { ArtistName = "阿桑", SongTitle = "一直很安静" });
} bool CanAddAlbumArtistExecute()
{
return true;
} void UpdateAlbumArtistsExecute()
{ foreach (var song in _songs)
{
song.ArtistName = "Unknow";
}
} bool CanUpdateAlbumArtistsExecute()
{
return true;
} public ICommand AddAlbumArtist { get { return new RelayCommand(AddAlbumArtistExecute, CanAddAlbumArtistExecute); } } public ICommand UpdateAlbumArtists { get { return new RelayCommand(UpdateAlbumArtistsExecute, CanUpdateAlbumArtistsExecute); } } #endregion
}

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

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

(扩展)Example 7:Command传参数

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

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

          

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

 void UpdateAlbumArtistsExecute(SongViewModel song)
{
if(song == null) return; song.ArtistName = "Unknow";
} bool CanUpdateAlbumArtistsExecute(SongViewModel song)
{
return true;
} public ICommand AddAlbumArtist { get { return new RelayCommand(AddAlbumArtistExecute, CanAddAlbumArtistExecute); } } public ICommand UpdateAlbumArtists { get { return new RelayCommand<SongViewModel>(new Action<SongViewModel>(UpdateAlbumArtistsExecute), new Predicate<SongViewModel>(CanUpdateAlbumArtistsExecute)); } }

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

结束语:

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

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

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

WPF/MVVM快速指引的更多相关文章

  1. WPF/MVVM 快速开始指南(译)(转)

    WPF/MVVM 快速开始指南(译) 本篇文章是Barry Lapthorn创作的,感觉写得很好,翻译一下,做个纪念.由于英文水平实在太烂,所以翻译有错或者译得不好的地方请多指正.另外由于原文是针对W ...

  2. WPF/MVVM Quick Start Tutorial - WPF/MVVM 快速入门教程 -原文,翻译及一点自己的补充

    转载自 https://www.codeproject.com/articles/165368/wpf-mvvm-quick-start-tutorial WPF/MVVM Quick Start T ...

  3. [转]WPF/MVVM快速开始手册

    I will quickly introduce some topics, then show an example that explains or demonstrates each point. ...

  4. WPF/MVVM 快速开发

    http://www.codeproject.com/Articles/165368/WPF-MVVM-Quick-Start-Tutorial 这篇文章醍醐灌顶,入门良药啊! Introductio ...

  5. WPF MVVM UI分离之《交互与数据分离》 基础才是重中之重~delegate里的Invoke和BeginInvoke 将不确定变为确定系列~目录(“机器最能证明一切”) 爱上MVC3系列~全局异常处理与异常日志 基础才是重中之重~lock和monitor的区别 将不确定变成确定~我想监视我的对象,如果是某个值,就叫另一些方法自动运行 将不确定变成确定~LINQ DBML模型可以对

    WPF MVVM UI分离之<交互与数据分离>   在我们使用WPF过程中,不可避免并且超级喜欢使用MVVM框架. 那么,使用MVVM的出发点是视觉与业务逻辑分离,即UI与数据分离 诸如下 ...

  6. WPF MVVM 验证

    WPF MVVM(Caliburn.Micro) 数据验证 书接前文 前文中仅是WPF验证中的一种,我们暂且称之为View端的验证(因为其验证规是写在Xaml文件中的). 还有一种我们称之为Model ...

  7. WPF MVVM初体验

    首先MVVM设计模式的结构, Views: 由Window/Page/UserControl等构成,通过DataBinding与ViewModels建立关联: ViewModels:由一组命令,可以绑 ...

  8. WPF MVVM实现TreeView

    今天有点时间,做个小例子WPF MVVM 实现TreeView 只是一个思路大家可以自由扩展 文章最后给出了源码下载地址 图1   图2     模版加上了一个checkbox,选中父类的checkb ...

  9. A WPF/MVVM Countdown Timer

    Introduction This article describes the construction of a countdown timer application written in C# ...

随机推荐

  1. 面试被问到SPI总结

    SPI驱动框架 枚举过程 drivers/spi/spi.c: spi_register_board_info /* 对于每一个spi_master,调用spi_match_master_to_boa ...

  2. # 20155327 2016-20017-3 《Java程序设计》第3周学习总结

    教材学习内容总结 第四章 认识对象 区分基本类型与类类型 基本类型: 1.整数:包括int,short,byte,long ,初始值为0 2.浮点型:float,double ,初始值为0.0 3.字 ...

  3. Hibernate框架用法

    一,Hibernate框架介绍 没有Hibernate之前,使用jdbc来连接数据库时,需要反射加载驱动,再获取连接 在连接上获取sql承载块,传入sql语句执行,获取结果集,解析结果 Hiberna ...

  4. Python+MySQL开发医院网上预约系统(课程设计)一

    一:开发环境的配置 1:桌面环境为cnetos7+python2.7 2:MySQL的安装与配置 1)MySQL的安装 MySQL官方文档: http://dev.mysql.com/doc/mysq ...

  5. 数据库sql优化总结之1-百万级数据库优化方案+案例分析

    项目背景 有三张百万级数据表 知识点表(ex_subject_point)9,316条数据 试题表(ex_question_junior)2,159,519条数据 有45个字段 知识点试题关系表(ex ...

  6. 爬虫2.5-scrapy框架-下载中间件

    目录 scrapy框架-下载中间件 scrapy框架-下载中间件 middlewares.py中有两个类,一个是xxSpiderMiddleware类 一个是xxDownloaderMiddlewar ...

  7. spring-boot 项目整合logback

    使用spring-boot项目中添加日志输出,java的日志输出一共有两个大的方案log4j/log4j2 ,logback.log4j2算是对log4j的一个升级版本. 常规做法是引入slf4j作为 ...

  8. day02——作业讲解

    # 设定⼀个理想数字⽐如:66,让⽤户输⼊数字,如果⽐66⼤,则显示猜测# 的结果⼤了:如果⽐66⼩,则显示猜测的结果⼩了;只有等于66,显示猜测结果# 正确,然后退出循环 #升级版# 可以帮我们生成 ...

  9. ICPC 沈阳 Problem C

    题意 求n的全排列中将前k个数排序后最长公共子序列>=n-1的个数 思考 我们先把最后可能产生的结果找出来,再找有多少种排列能构成这些结果 设排列为s S like 1,2,3,...,n , ...

  10. Hyperledger Fabric中的Identity

    Hyperledger Fabric中的Identity 什么是Identity 区块链网络中存在如下的角色:peers, orderers, client application, administ ...