转载

WPF自定义控件与样式(14)-轻量MVVM模式实践

一.前言

申明 :WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接。

MVVM是WPF中一个非常实用的编程模式,充分利用了WPF的绑定机制,体现了WPF数据驱动的优势。

WPF自定义控件与样式(14)-轻量MVVM模式实践 图片来源:( WPF的MVVM

关于MVVM网上很多介绍或者示例,本文不多做介绍了,本文的主要目的是提供一个轻量级的View Model实现,本文的主要内容:

  • 依赖通知InotifyPropertyChanged实现;
  • 命令Icommand的实现;
  • 消息的实现;
  • 一个简单MVVM示例;

对于是否要使用MVVM、如何使用,个人觉得根据具体需求可以灵活处理,不用纠结于模式本身。用了MVVM,后置*.cs文件就不一定不允许写任何代码,混合着用也是没有问题的, 只要自己决的方便、代码结构清晰、维护方便即可。

二.依赖通知InotifyPropertyChanged实现

依赖通知InotifyPropertyChanged是很简单的一个接口,是View Model标配的接口,一个典型的实现(BaseNotifyPropertyChanged): 

    /// <summary>     /// 实现了属性更改通知的基类     /// </summary>     public class BaseNotifyPropertyChanged : System.ComponentModel.INotifyPropertyChanged     {         /// <summary>         /// 属性值变化时发生         /// </summary>         /// <param name="propertyName"></param>         protected virtual void OnPropertyChanged(string propertyName)         {             if (this.PropertyChanged != null)                 this.PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));         }          public virtual event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;     } 

然后使用方式就是这样的: 

         public int _Age;          public int Age         {             get { return this._Age; }             set { this._Age = value; base.OnPropertyChanged("Age"); }         } 

上面的代码有硬编码,有代码洁癖的人就不爽了,因此网上有多种解决方式,比如这篇: WPF MVVM之INotifyPropertyChanged 接口的几种实现方式 。本文的实现方式如下,使用表达式树:

         /// <summary>         /// 属性值变化时发生         /// </summary>         /// <param name="propertyName"></param>         protected virtual void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)         {             var propertyName = (propertyExpression.Body as MemberExpression).Member.Name;             this.OnPropertyChanged(propertyName);         } 

使用上避免了硬编码,使用示例: 

         public string _Name;         public string Name         {             get { return this._Name; }             set { this._Name = value; base.OnPropertyChanged(() => this.Name); }         } 

三.命令Icommand的实现

命令的实现也很简单,实现Icommand的几个接口就OK了, 考虑到使用时能更加方便,无参数RelayCommand实现: 

     /// <summary>     /// 广播命令:基本ICommand实现接口     /// </summary>     public class RelayCommand : ICommand     {         public Action ExecuteCommand { get; private set; }         public Func<bool> CanExecuteCommand { get; private set; }          public RelayCommand(Action executeCommand, Func<bool> canExecuteCommand)         {             this.ExecuteCommand = executeCommand;             this.CanExecuteCommand = canExecuteCommand;         }          public RelayCommand(Action executeCommand)             : this(executeCommand, null) { }          /// <summary>         /// 定义在调用此命令时调用的方法。         /// </summary>         /// <param name="parameter">此命令使用的数据。如果此命令不需要传递数据,则该对象可以设置为 null。</param>         public void Execute(object parameter)         {             if (this.ExecuteCommand != null) this.ExecuteCommand();         }          /// <summary>         /// 定义用于确定此命令是否可以在其当前状态下执行的方法。         /// </summary>         /// <returns>         /// 如果可以执行此命令,则为 true;否则为 false。         /// </returns>         /// <param name="parameter">此命令使用的数据。如果此命令不需要传递数据,则该对象可以设置为 null。</param>         public bool CanExecute(object parameter)         {             return CanExecuteCommand == null || CanExecuteCommand();         }          public event EventHandler CanExecuteChanged         {             add { if (this.CanExecuteCommand != null) CommandManager.RequerySuggested += value; }             remove { if (this.CanExecuteCommand != null) CommandManager.RequerySuggested -= value; }         }     } 

泛型参数RelayCommand<T>的版本: 

     /// <summary>     /// 广播命令:基本ICommand实现接口,带参数     /// </summary>     public class RelayCommand<T> : ICommand     {         public Action<T> ExecuteCommand { get; private set; }          public Predicate<T> CanExecuteCommand { get; private set; }          public RelayCommand(Action<T> executeCommand, Predicate<T> canExecuteCommand)         {             this.ExecuteCommand = executeCommand;             this.CanExecuteCommand = canExecuteCommand;         }          public RelayCommand(Action<T> executeCommand)             : this(executeCommand, null) { }          /// <summary>         /// 定义在调用此命令时调用的方法。         /// </summary>         /// <param name="parameter">此命令使用的数据。如果此命令不需要传递数据,则该对象可以设置为 null。</param>         public void Execute(object parameter)         {             if (this.ExecuteCommand != null) this.ExecuteCommand((T)parameter);         }          /// <summary>         /// 定义用于确定此命令是否可以在其当前状态下执行的方法。         /// </summary>         /// <returns>         /// 如果可以执行此命令,则为 true;否则为 false。         /// </returns>         /// <param name="parameter">此命令使用的数据。如果此命令不需要传递数据,则该对象可以设置为 null。</param>         public bool CanExecute(object parameter)         {             return CanExecuteCommand == null || CanExecuteCommand((T)parameter);         }          public event EventHandler CanExecuteChanged         {             add { if (this.CanExecuteCommand != null) CommandManager.RequerySuggested += value; }             remove { if (this.CanExecuteCommand != null) CommandManager.RequerySuggested -= value; }         }     } 

带参数和不带参数的命令XAML绑定方式: 

 <core:FButton Margin="5 0 0 0" Command="{Binding ShowUserCommand}">ShowUser</core:FButton> <core:FButton Margin="5 0 0 0" Command="{Binding SetNameCommand}" FIcon=""                           CommandParameter="{Binding Text,ElementName=txtSetName}">SetName</core:FButton> 

上面是针对提供Command模式的控件示例, 但对于其他事件呢,比如MouseOver如何绑定呢?可以借用System.Windows.Interactivity.dll,其中的 Interaction 可以帮助我们实现对命令的绑定,这是在微软Blend中提供的。添加dll应用,然后添加命名空间:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

             <TextBlock VerticalAlignment="Center" Margin="5 0 0 0" Text="MoseOver" x:Name="txbMessage">                 <i:Interaction.Triggers>                 <i:EventTrigger EventName="MouseMove">                     <i:InvokeCommandAction Command="{Binding MouseOverCommand}" CommandParameter="{Binding ElementName=txbMessage}"></i:InvokeCommandAction>                 </i:EventTrigger>                 </i:Interaction.Triggers>             </TextBlock> 

四.消息的实现

消息类Messenger主要目的是实现View与View Model及各个模块之间的通信。本文的消息类Messenger,参考自网络开源的实现(MVVMFoundation)。实现了松散耦合的消息通知机制,对于消息传输参数,内部使用了弱引用(WeakReference),以防止内存泄漏代码: 

      /// <summary>     /// Provides loosely-coupled messaging between various colleague objects.  All references to objects are stored weakly, to prevent memory leaks.     /// 提供松散耦合的消息通知机制,为防止内存泄漏,所有对象都使用了弱引用(WeakReference)     /// </summary>     public class Messenger     {         #region Constructor          public Messenger()         {         }          #endregion // Constructor          #region Register          /// <summary>         /// Registers a callback method, with no parameter, to be invoked when a specific message is broadcasted.         /// 注册消息监听         /// </summary>         /// <param name="message">The message to register for.</param>         /// <param name="callback">The callback to be called when this message is broadcasted.</param>         public void Register(string message, Action callback)         {             this.Register(message, callback, null);         }          /// <summary>         /// Registers a callback method, with a parameter, to be invoked when a specific message is broadcasted.         /// 注册消息监听         /// </summary>         /// <param name="message">The message to register for.</param>         /// <param name="callback">The callback to be called when this message is broadcasted.</param>         public void Register<T>(string message, Action<T> callback)         {             this.Register(message, callback, typeof(T));         }          void Register(string message, Delegate callback, Type parameterType)         {             if (String.IsNullOrEmpty(message))                 throw new ArgumentException("'message' cannot be null or empty.");              if (callback == null)                 throw new ArgumentNullException("callback");              this.VerifyParameterType(message, parameterType);              _messageToActionsMap.AddAction(message, callback.Target, callback.Method, parameterType);         }          [Conditional("DEBUG")]         void VerifyParameterType(string message, Type parameterType)         {             Type previouslyRegisteredParameterType = null;             if (_messageToActionsMap.TryGetParameterType(message, out previouslyRegisteredParameterType))             {                 if (previouslyRegisteredParameterType != null && parameterType != null)                 {                     if (!previouslyRegisteredParameterType.Equals(parameterType))                         throw new InvalidOperationException(string.Format(                             "The registered action's parameter type is inconsistent with the previously registered actions for message '{0}'./nExpected: {1}/nAdding: {2}",                             message,                             previouslyRegisteredParameterType.FullName,                             parameterType.FullName));                 }                 else                 {                     // One, or both, of previouslyRegisteredParameterType or callbackParameterType are null.                     if (previouslyRegisteredParameterType != parameterType)   // not both null?                     {                         throw new TargetParameterCountException(string.Format(                             "The registered action has a number of parameters inconsistent with the previously registered actions for message /"{0}/"./nExpected: {1}/nAdding: {2}",                             message,                             previouslyRegisteredParameterType == null ? 0 : 1,                             parameterType == null ? 0 : 1));                     }                 }             }         }          #endregion // Register          #region Notify          /// <summary>         /// Notifies all registered parties that a message is being broadcasted.         /// 发送消息通知,触发监听执行         /// </summary>         /// <param name="message">The message to broadcast.</param>         /// <param name="parameter">The parameter to pass together with the message.</param>         public void Notify(string message, object parameter)         {             if (String.IsNullOrEmpty(message))                 throw new ArgumentException("'message' cannot be null or empty.");              Type registeredParameterType;             if (_messageToActionsMap.TryGetParameterType(message, out registeredParameterType))             {                 if (registeredParameterType == null)                     throw new TargetParameterCountException(string.Format("Cannot pass a parameter with message '{0}'. Registered action(s) expect no parameter.", message));             }              var actions = _messageToActionsMap.GetActions(message);             if (actions != null)                 actions.ForEach(action => action.DynamicInvoke(parameter));         }          /// <summary>         /// Notifies all registered parties that a message is being broadcasted.         /// 发送消息通知,触发监听执行         /// </summary>         /// <param name="message">The message to broadcast.</param>         public void Notify(string message)         {             if (String.IsNullOrEmpty(message))                 throw new ArgumentException("'message' cannot be null or empty.");              Type registeredParameterType;             if (_messageToActionsMap.TryGetParameterType(message, out registeredParameterType))             {                 if (registeredParameterType != null)                     throw new TargetParameterCountException(string.Format("Must pass a parameter of type {0} with this message. Registered action(s) expect it.", registeredParameterType.FullName));             }              var actions = _messageToActionsMap.GetActions(message);             if (actions != null)                 actions.ForEach(action => action.DynamicInvoke());         }          #endregion // NotifyColleauges          #region MessageToActionsMap [nested class]          /// <summary>         /// This class is an implementation detail of the Messenger class.         /// </summary>         private class MessageToActionsMap         {             #region Constructor              internal MessageToActionsMap()             {             }              #endregion // Constructor              #region AddAction              /// <summary>             /// Adds an action to the list.             /// </summary>             /// <param name="message">The message to register.</param>             /// <param name="target">The target object to invoke, or null.</param>             /// <param name="method">The method to invoke.</param>             /// <param name="actionType">The type of the Action delegate.</param>             internal void AddAction(string message, object target, MethodInfo method, Type actionType)             {                 if (message == null)                     throw new ArgumentNullException("message");                  if (method == null)                     throw new ArgumentNullException("method");                  lock (_map)                 {                     if (!_map.ContainsKey(message))                         _map[message] = new List<WeakAction>();                      _map[message].Add(new WeakAction(target, method, actionType));                 }             }              #endregion // AddAction              #region GetActions              /// <summary>             /// Gets the list of actions to be invoked for the specified message             /// </summary>             /// <param name="message">The message to get the actions for</param>             /// <returns>Returns a list of actions that are registered to the specified message</returns>             internal List<Delegate> GetActions(string message)             {                 if (message == null)                     throw new ArgumentNullException("message");                  List<Delegate> actions;                 lock (_map)                 {                     if (!_map.ContainsKey(message))                         return null;                      List<WeakAction> weakActions = _map[message];                     actions = new List<Delegate>(weakActions.Count);                     for (int i = weakActions.Count - 1; i > -1; --i)                     {                         WeakAction weakAction = weakActions[i];                         if (weakAction == null)                             continue;                          Delegate action = weakAction.CreateAction();                         if (action != null)                         {                             actions.Add(action);                         }                         else                         {                             // The target object is dead, so get rid of the weak action.                             weakActions.Remove(weakAction);                         }                     }                      // Delete the list from the map if it is now empty.                     if (weakActions.Count == 0)                         _map.Remove(message);                 }                  // Reverse the list to ensure the callbacks are invoked in the order they were registered.                 actions.Reverse();                  return actions;             }              #endregion // GetActions              #region TryGetParameterType              /// <summary>             /// Get the parameter type of the actions registered for the specified message.             /// </summary>             /// <param name="message">The message to check for actions.</param>             /// <param name="parameterType">             /// When this method returns, contains the type for parameters              /// for the registered actions associated with the specified message, if any; otherwise, null.             /// This will also be null if the registered actions have no parameters.             /// This parameter is passed uninitialized.             /// </param>             /// <returns>true if any actions were registered for the message</returns>             internal bool TryGetParameterType(string message, out Type parameterType)             {                 if (message == null)                     throw new ArgumentNullException("message");                  parameterType = null;                 List<WeakAction> weakActions;                 lock (_map)                 {                     if (!_map.TryGetValue(message, out weakActions) || weakActions.Count == 0)                         return false;                 }                 parameterType = weakActions[0].ParameterType;                 return true;             }              #endregion // TryGetParameterType              #region Fields              // Stores a hash where the key is the message and the value is the list of callbacks to invoke.             readonly Dictionary<string, List<WeakAction>> _map = new Dictionary<string, List<WeakAction>>();              #endregion // Fields         }          #endregion // MessageToActionsMap [nested class]          #region WeakAction [nested class]          /// <summary>         /// This class is an implementation detail of the MessageToActionsMap class.         /// </summary>         private class WeakAction         {             #region Constructor              /// <summary>             /// Constructs a WeakAction.             /// </summary>             /// <param name="target">The object on which the target method is invoked, or null if the method is static.</param>             /// <param name="method">The MethodInfo used to create the Action.</param>             /// <param name="parameterType">The type of parameter to be passed to the action. Pass null if there is no parameter.</param>             internal WeakAction(object target, MethodInfo method, Type parameterType)             {                 if (target == null)                 {                     _targetRef = null;                 }                 else                 {                     _targetRef = new WeakReference(target);                 }                  _method = method;                  this.ParameterType = parameterType;                  if (parameterType == null)                 {                     _delegateType = typeof(Action);                 }                 else                 {                     _delegateType = typeof(Action<>).MakeGenericType(parameterType);                 }             }              #endregion // Constructor              #region CreateAction              /// <summary>             /// Creates a "throw away" delegate to invoke the method on the target, or null if the target object is dead.             /// </summary>             internal Delegate CreateAction()             {                 // Rehydrate into a real Action object, so that the method can be invoked.                 if (_targetRef == null)                 {                     return Delegate.CreateDelegate(_delegateType, _method);                 }                 else                 {                     try                     {                         object target = _targetRef.Target;                         if (target != null)                             return Delegate.CreateDelegate(_delegateType, target, _method);                     }                     catch                     {                     }                 }                  return null;             }              #endregion // CreateAction              #region Fields              internal readonly Type ParameterType;              readonly Type _delegateType;             readonly MethodInfo _method;             readonly WeakReference _targetRef;              #endregion // Fields         }          #endregion // WeakAction [nested class]          #region Fields          readonly MessageToActionsMap _messageToActionsMap = new MessageToActionsMap();          #endregion // Fields     }  View Code

在后面的示例中有简单使用。

五.简单MVVM示例

5.1 View Model定义实现

实现一个UserViewModel,定义了两个通知属性,3个命令,用于在XAML中实现不同的命令绑定处理,还注册了一个消息,代码: 

    public class UserViewModel : BaseNotifyPropertyChanged     {         public string _Name;         public string Name         {             get { return this._Name; }             set { this._Name = value; base.OnPropertyChanged(() => this.Name); }         }          public int _Age;          public int Age         {             get { return this._Age; }             set { this._Age = value; base.OnPropertyChanged("Age"); }         }          public RelayCommand<string> SetNameCommand { get; private set; }         public RelayCommand ShowUserCommand { get; private set; }         public RelayCommand<FrameworkElement> MouseOverCommand { get; private set; }          public UserViewModel()         {             this.SetNameCommand = new RelayCommand<string>(this.SetName);             this.ShowUserCommand = new RelayCommand(this.ShowUser);             this.MouseOverCommand = new RelayCommand<FrameworkElement>(this.MouseOver);             Page_MVVM.GlobalMessager.Register("123", () =>             {                 MessageBoxX.Info("我是处理123消息的!");             });         }          public void SetName(string name)         {             if (MessageBoxX.Question(string.Format("要把Name值由[{0}]修改为[{1}]吗?", this.Name, name)))             {                 this.Name = name;             }         }          public void ShowUser()         {             MessageBoxX.Info(this.Name + "---" + this.Age);         }          public void MouseOver(FrameworkElement tb)         {             MessageBoxX.Info("我好像摸到了" + tb.Name);         }     } 

5.2 测试页面Page_MVVM.xaml

创建一个测试页面Page_MVVM,后置代码如下,在构造函数里注入View Model,在一个按钮事件里发送消息: 

     public partial class Page_MVVM : Page     {         public static Messenger GlobalMessager = new Messenger();          public Page_MVVM()         {             InitializeComponent();             //set vm             UserViewModel uvm = new UserViewModel();             uvm.Name = "kwong";             uvm.Age = 30;             this.DataContext = uvm;          }          private void ButtonBase_OnClick(object sender, RoutedEventArgs e)         {             GlobalMessager.Notify("123");         }     } 

完整XAML代码: 

 <Page x:Class="Kwong.Framework.WPFTest.Page_MVVM"       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"       xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"        xmlns:core="clr-namespace:XLY.Framework.Controls;assembly=XLY.Framework.Controls"       xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"       mc:Ignorable="d"        d:DesignHeight="600" d:DesignWidth="800"     Title="Page_MVVM">     <Page.Resources>         <Style TargetType="StackPanel">             <Setter Property="Height" Value="80"/>             <Setter Property="Margin" Value="3"/>             <Setter Property="Orientation" Value="Horizontal"/>             <Setter Property="Background" Value="{StaticResource WindowBackground}"/>         </Style>     </Page.Resources>     <StackPanel Style="{x:Null}">         <StackPanel >             <TextBox Height="30" Width="240" Text="{Binding Name,UpdateSourceTrigger=PropertyChanged}" Margin="5 0 0 0"                      core:ControlAttachProperty.Label="{Binding Name.Length,Mode=OneWay}"                       Style="{StaticResource LabelTextBox}"/>             <TextBox Height="30" Width="240" Text="{Binding Age}" core:ControlAttachProperty.Label="Age:"                       Style="{StaticResource LabelTextBox}" Margin="5 0 0 0"/>          </StackPanel>         <StackPanel>             <core:FButton Margin="5 0 0 0" Command="{Binding ShowUserCommand}">ShowUser</core:FButton>             <core:FButton Margin="5 0 0 0" FIcon="" Width="125" Click="ButtonBase_OnClick">Send Message</core:FButton>         </StackPanel>         <StackPanel>             <TextBox Height="30" Width="240" x:Name="txtSetName"  core:ControlAttachProperty.Label="Name-" Margin="5 0 0 0"                      Style="{StaticResource LabelTextBox}"></TextBox>             <core:FButton Margin="5 0 0 0" Command="{Binding SetNameCommand}" FIcon=""                           CommandParameter="{Binding Text,ElementName=txtSetName}">SetName</core:FButton>         </StackPanel>         <StackPanel>             <TextBlock VerticalAlignment="Center" Margin="5 0 0 0" Text="MoseOver" x:Name="txbMessage">                 <i:Interaction.Triggers>                 <i:EventTrigger EventName="MouseMove">                     <i:InvokeCommandAction Command="{Binding MouseOverCommand}" CommandParameter="{Binding ElementName=txbMessage}"></i:InvokeCommandAction>                 </i:EventTrigger>                 </i:Interaction.Triggers>             </TextBlock>         </StackPanel>     </StackPanel> </Page> 

5.3 效果

WPF自定义控件与样式(14)-轻量MVVM模式实践

附录:参考引用

WPF自定义控件与样式(1)-矢量字体图标(iconfont)

WPF自定义控件与样式(2)-自定义按钮FButton

WPF自定义控件与样式(3)-TextBox & RichTextBox & PasswordBox样式、水印、Label标签、功能扩展

WPF自定义控件与样式(4)-CheckBox/RadioButton自定义样式

WPF自定义控件与样式(5)-Calendar/DatePicker日期控件自定义样式及扩展

WPF自定义控件与样式(6)-ScrollViewer与ListBox自定义样式

WPF自定义控件与样式(7)-列表控件DataGrid与ListView自定义样式

WPF自定义控件与样式(8)-ComboBox与自定义多选控件MultComboBox

WPF自定义控件与样式(9)-树控件TreeView与菜单Menu-ContextMenu

WPF自定义控件与样式(10)-进度控件ProcessBar自定义样

WPF自定义控件与样式(11)-等待/忙/正在加载状态-控件实现

WPF自定义控件与样式(12)-缩略图ThumbnailImage /gif动画图/图片列表

WPF自定义控件与样式(13)-自定义窗体Window & 自适应内容大小消息框MessageBox

版权所有,文章来源: http://www.cnblogs.com/anding

个人能力有限,本文内容仅供学习、探讨,欢迎指正、交流。

正文到此结束
Loading...