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


    一.前言

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

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

     图片来源:(WPF的MVVM)

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

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

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

    二.依赖通知InotifyPropertyChanged实现

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

    1. /// <summary>
    2. /// 实现了属性更改通知的基类
    3. /// </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; }

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

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

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

    1. /// <summary>
    2. /// 属性值变化时发生
    3. /// </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); }

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

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

    三.命令Icommand的实现

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

    1. /// <summary>
    2. /// 广播命令:基本ICommand实现接口
    3. /// </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的版本:  

    1. /// <summary>
    2. /// 广播命令:基本ICommand实现接口,带参数
    3. /// </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),以防止内存泄漏代码:  

    1. /// <summary>
    2. /// Provides loosely-coupled messaging between various colleague objects. All references to objects are stored weakly, to prevent memory leaks.
    3. /// 提供松散耦合的消息通知机制,为防止内存泄漏,所有对象都使用了弱引用(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
  • 相关阅读:
    【Docker与Termux】闲置旧安卓手机上的NAS无缝部署方案
    [附源码]java毕业设计闲置物品线上交易系统
    Vision Transformer这两年
    (49)其他的性能测试场景
    简单的布局的初级智能文本提示器
    【linux网络编程】阻塞、非阻塞、同步、异步
    游戏设计模式专栏(一):工厂方法模式
    SpringBoot-Web开发-文件上传
    基于深度学习的车道检测(一)
    html2canvas与window.devicePixelRatio
  • 原文地址:https://blog.csdn.net/LJianDong/article/details/127739774