轻量MVVM模式实践

文章来源:http://www.cnblogs.com/anding


一.前言

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

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

 图片来源:(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="&#xe60c;"
                          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),以防止内存泄漏代码:  

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="&#xe61c;" 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="&#xe60c;"
                          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自定义控件与样式(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


猜你喜欢

转载自blog.csdn.net/nodeman/article/details/80620285
今日推荐