WPF自定義控件與樣式(14)-輕量MVVM模式實踐


一.前言

  申明: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),以防止內存泄漏代碼:  

    /// <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="&#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

 

版權所有,文章來源:http://www.cnblogs.com/anding

個人能力有限,本文內容僅供學習、探討,歡迎指正、交流。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM