IValueConverter,做 WPF 的都应该接触过,把值换成 Visibility 、Margin 等等是最常见的例子,也有很多很好的博文解释过用法。本文只是解释一下,MVVM 中一些情景。
我遇到过一个案例,做些简单的数据可视化。要求把 enum 换成图片。
MVVM 模式下,是通过 ViewModel 把业务类 Model 暴露给 View,用绑定完成 ViewModel 和 View 的连接。这样的话,Model 内的 enum 直接绑到 View 时候,视图显示了 enum 值 ToString() 的结果。
用个真实例子说明。为代码更简单,以下没有用任何其他第三方控件或类库。
看看 Model 是怎样的:
break-word;overflow: auto;">using System.ComponentModel; namespace Lepton_Practical_MVVM_4 { public class MyModel : INotifyPropertyChanged { #region Domain Properties private long _ID; /// <summary> /// DB Key /// </summary> public virtual long ID { get { return _ID; } set { _ID = value; if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("ID")); } } private MyStatusEnum _Status; /// <summary> /// Document Status /// </summary> public virtual MyStatusEnum Status { get { return _Status; } set { _Status = value; if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Status")); } } private string _DocNo; /// <summary> /// Document number /// </summary> public virtual string DocNo { get { return _DocNo; } set { _DocNo = value; if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("DocNo")); } } #endregion #region INotifyPropertyChanged Members public virtual event PropertyChangedEventHandler PropertyChanged; #endregion } }
MyStatusEnum:
namespace Lepton_Practical_MVVM_4 { public enum MyStatusEnum { OK = 0, Cancel = 1, Delete = 2 } }
这个业务类,将使用 ORM, NHibernate 做 persistence,这个样子加上 XXX.hbm.xml 映射能直接调用 session.Save() 来储存的了。enum 会自动储存为 int。数据层不写出来了。使用其他 ORM 也大同小异。
但直接绑定后,这样子不太 User-Friendly,也不够华丽。
这用例是做可视化,把状态换成图片。
要绑定图片,绑定的话,最直观的想法,改 Model。Model 内有图片Source 属性,好像就什么都解决了。
using System.ComponentModel; using System.Windows.Media; using System; using System.Windows.Media.Imaging; namespace Lepton_Practical_MVVM_4 { public class MyModel : INotifyPropertyChanged { #region Domain Properties private long _ID; /// <summary> /// DB Key /// </summary> public virtual long ID { get { return _ID; } set { _ID = value; if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("ID")); } } private MyStatusEnum _Status; /// <summary> /// Status /// </summary> public virtual MyStatusEnum Status { get { return _Status; } set { _Status = value; if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Status")); } } private string _DocNo; /// <summary> /// Document Number /// </summary> public virtual string DocNo { get { return _DocNo; } set { _DocNo = value; if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("DocNo")); } } #endregion public virtual ImageSource StatusImage { get { switch (Status) { case MyStatusEnum.OK: return new BitmapImage(new Uri("pack://application:,,,/Img/Check.png")); case MyStatusEnum.Cancel: return new BitmapImage(new Uri("pack://application:,,,/Img/Cancel.png")); case MyStatusEnum.Delete: return new BitmapImage(new Uri("pack://application:,,,/Img/Delete.png")); default: throw new InvalidEnumArgumentException("MyStatusEnum out of expected range"); } } } #region INotifyPropertyChanged Members public virtual event PropertyChangedEventHandler PropertyChanged; #endregion } }
XAML:
<Window x:Class="Lepton_Practical_MVVM_4.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Lepton Practical MVVM 4 IConverter" Height="350" Width="525"> <Grid> <ListView ItemsSource="{Binding Path=MyItems}"> <ListView.View> <GridView> <GridViewColumn DisplayMemberBinding="{Binding Path=Status}" Header="状态" Width="50"/> <GridViewColumn DisplayMemberBinding="{Binding Path=DocNo}" Header="单号" Width="100"/> <GridViewColumn Header="图片"> <GridViewColumn.CellTemplate> <DataTemplate> <Image Source="{Binding Path=StatusImage}" Width="20" Height="20"/> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> </GridView> </ListView.View> </ListView> </Grid> </Window>
效果:
StatusImage 这个字段,不需要映射不用保存到数据库,貌似没问题。
但一旦运行时 Status 值变化,问题就出来了。
运行时,MyStatusEnum 值如果动态改,你会发现,状态栏的值会变,图片却不变。问题原因很明显,值变化发生在 Status,由它发出 PropertyChanged 通知,通知 Status 变了,不是 StatusImage变。点击这里下载这版本的代码。
然后又想办法,或许在 Status 变动时同时发Status 和 StatusImage 两个变化通知?又或许 PropertyChanged(null) 全部刷新?这不是某某方式能不能做到的问题,开发嘛,再乱再差代码,有时候也能做到用例要求。
以上是真实项目中我见过的修改过程,这做法当然被推翻,原因如下:
正确做法,是用 IValueConverter 做转换,IValueConverter 是显示层(View)的。需要不同逻辑的话,每个界面用自己的 Converter,逻辑独立,图片只跟 View 捆绑,ViewModel 和 Model 不需理会。
IValueConverter 写法很简单,在其他博文已经多次介绍。这里不重复了。用它的时候,绑定的是业务数据的字段,当它变化,图片自然会更新了。点击这里下载使用了 IValueConverter 的完整代码。
使用 MVVM 模式,一旦牵涉到改 Model 或 ViewModel,请三思。只为显示的话,几乎可以马上肯定,不该做在 Model 和 ViewModel。反过来,业务逻辑出现在 View 也是不正确。破坏了设计,自然享受不了良好设计的优势。