一.前言
申明:WPF自定義控件與樣式是一個系列文章,前后是有些關聯的,但大多是按照由簡到繁的順序逐步發布的,若有不明白的地方可以參考本系列前面的文章,文末附有部分文章鏈接。
MVVM是WPF中一個非常實用的編程模式,充分利用了WPF的綁定機制,體現了WPF數據驅動的優勢。
關於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 }
在后面的示例中有簡單使用。
五.簡單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自定義控件與樣式(1)-矢量字體圖標(iconfont)
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
個人能力有限,本文內容僅供學習、探討,歡迎指正、交流。