WPF之再談MVVM
MVVM簡介
MVVM模式由Model,View,ViewModel三部分組成。
Model需繼承INotifyPropertyChange(屬性修改通知)
ViewModel負責業務邏輯,連線View和Model
View上面的控制元件繫結model和命令(command)
注:資料繫結binding實現了INotifyPropertyChange介面的事件。
MVVM框架實現了資料雙向繫結,即View和Model雙向繫結。最終實現包含Model,Command,View,ViewModel四部分。
問題的關鍵
關鍵是要能準確的進行ViewModel的建模,處理好View與ViewModel之間的關係
只有2種關係:
資料傳遞 --- 雙向,使用Binding實現;
操作傳遞 --- 單向(只從View傳遞給ViewModel),使用命令Command實現;
資料繫結
1、建立NotificationObject
首先建立NotificationObject,它是所以ViewModel的基類
因為要使用Binding,而ViewModel就充當資料來源的角色,而要實現當值有變化時會自動響應,就必須實現INotifyPropertyChanged介面,程式碼如下:
using System; using System.Collections.Generic;using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MVVMTest.ViewModels { public class NotificationObject:INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void RaisePropertyChanged(stringproperty) { if (this.PropertyChanged != null) this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(property)); } } }
2、建立ViewModel
using MVVMTest.Commands; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MVVMTest.ViewModels { public class MainWindowViewModel:NotificationObject { private string txt1; public string Txt1 { get { return txt1; } set { txt1 = value; this.RaisePropertyChanged("Txt1"); } } private string txt2; public string Txt2 { get { return txt2; } set { txt2 = value; this.RaisePropertyChanged("Txt2"); } } private string result; public string Result { get { return result; } set { result = value; this.RaisePropertyChanged("Result"); } } public DelegateCommand ConcatCommand { get; set; } public void Concat(object parameter) { Result = Txt1 + " and " + Txt2; } public MainWindowViewModel() { ConcatCommand = new DelegateCommand(); ConcatCommand.ExecuteAction = new Action<object>(Concat); } } }
然後所有資料型別都實現NotificationObject這個類
最後:
DataContext = new MainWindowViewModel();
這樣就能實現資料的繫結
命令繫結
命令繫結要關注的核心就是兩個方面的問題,命令能否執行和命令怎麼執行。也就是說當View中的一個Button綁定了ViewModel中一個命令後,什麼時候這個Button是可用的,按下Button後執行什麼操作。解決了這兩個問題基本就實現了命令繫結。另外一個問題就是執行過程中需要的資料(引數)要如何傳遞。
自定義一個能夠被繫結的命令需要實現ICommand介面。該介面包含:
public event EventHandler CanExecuteChanged // 在命令可執行狀態發生改變時觸發 public bool CanExecute(object parameter) //檢查命令是否可用的方法 public void Execute(object parameter) //命令執行的方法
建立一個類(當然也可以建立支援泛型的命令)
public class MyCommand : ICommand { public bool CanExecute(object parameter) { return true; //表示是否執行下面那個Execute方法. } public event EventHandler CanExecuteChanged; public void Execute(object parameter) //這裡是定義按鈕按下去,需要執行的內容 { MessageBox.Show("我這裡是定義死了,你可以通過傳值的方法,來自定義顯示的內容."); } }
在ViewModel中建立命令屬性
public class ViewModel:NotificationObject { public ICommand MyCmd { get { return new MyCommand(); } } }
DataContext = new ViewModel(); //將viewModel這個例項繫結到當前頁面的D資料上下文上!前邊實現了的
最後介面繫結
<Button Content="ShowMsg" Command="{Binding MyCmd}" Height="158" Margin="91,244,106,0" Name="button1" VerticalAlignment="Top" />
事件繫結
為什麼要用事件繫結?這個問題其實是很好理解的,因為事件是豐富多樣的,單純的命令繫結遠不能覆蓋所有的事件。例如Button的命令繫結能夠解決Click事件的需求,但Button的MouseEnter、窗體的Loaded等大量的事件要怎麼處理呢?這就用到了事件繫結。
方法一:重寫InvokeCommandAction來擴充返回的引數
public class EventToCommand : TriggerAction<DependencyObject> { private string commandName; public readonly static DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(EventToCommand), null); public readonly static DependencyProperty CommandParameterProperty = DependencyProperty.Register("CommandParameter", typeof(object), typeof(EventToCommand), new PropertyMetadata(null, (DependencyObject s, DependencyPropertyChangedEventArgs e) => { EventToCommand sender = s as EventToCommand; if (sender == null) { return; } if (sender.AssociatedObject == null) { return; } })); /// <summary> /// 獲取或設定此操作應呼叫的命令。這是依賴屬性。 /// </summary> /// <value>要執行的命令。</value> /// <remarks>如果設定了此屬性和 CommandName 屬性,則此屬性將優先於後者。</remarks> public ICommand Command { get { return (ICommand)base.GetValue(EventToCommand.CommandProperty); } set { base.SetValue(EventToCommand.CommandProperty, value); } } /// <summary> /// 獲得或設定命令引數。這是依賴屬性。 /// </summary> /// <value>命令引數。</value> /// <remarks>這是傳遞給 ICommand.CanExecute 和 ICommand.Execute 的值。</remarks> public object CommandParameter { get { return base.GetValue(EventToCommand.CommandParameterProperty); } set { base.SetValue(EventToCommand.CommandParameterProperty, value); } } /// <summary> /// 獲得或設定此操作應呼叫的命令的名稱。 /// </summary> /// <value>此操作應呼叫的命令的名稱。</value> /// <remarks>如果設定了此屬性和 Command 屬性,則此屬性將被後者所取代。</remarks> public string CommandName { get { base.ReadPreamble(); return this.commandName; } set { if (this.CommandName != value) { base.WritePreamble(); this.commandName = value; base.WritePostscript(); } } } /// <summary> /// 呼叫操作。 /// </summary> /// <param name="parameter">操作的引數。如果操作不需要引數,則可以將引數設定為空引用。</param> protected override void Invoke(object parameter) { if (base.AssociatedObject == null) return; ICommand command = this.ResolveCommand(); /* * ★★★★★★★★★★★★★★★★★★★★★★★★ * 注意這裡添加了事件觸發源和事件引數 * ★★★★★★★★★★★★★★★★★★★★★★★★ */ ExCommandParameter exParameter = new ExCommandParameter { Sender = base.AssociatedObject, //Parameter = GetValue(CommandParameterProperty), Parameter = this.CommandParameter, EventArgs = parameter as EventArgs }; if (command != null && command.CanExecute(exParameter)) { /* * ★★★★★★★★★★★★★★★★★★★★★★★★ * 注意將擴充套件的引數傳遞到Execute方法中 * ★★★★★★★★★★★★★★★★★★★★★★★★ */ command.Execute(exParameter); } } private ICommand ResolveCommand() { if (this.Command != null) return this.Command; if (base.AssociatedObject == null) return null; ICommand result = null; Type type = base.AssociatedObject.GetType(); PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); for (int i = 0; i < properties.Length; i++) { PropertyInfo propertyInfo = properties[i]; if (typeof(ICommand).IsAssignableFrom(propertyInfo.PropertyType) && string.Equals(propertyInfo.Name, this.CommandName, StringComparison.Ordinal)) { result = (ICommand)propertyInfo.GetValue(base.AssociatedObject, null); break; } } return result; } }
其中:EventToCommand 類是自定義的擴充類
public class ExCommandParameter { /// <summary> /// 事件觸發源 /// </summary> public DependencyObject Sender { get; set; } /// <summary> /// 事件引數 /// </summary> public EventArgs EventArgs { get; set; } /// <summary> /// 額外引數 /// </summary> public object Parameter { get; set; } }
引入xaml命令空間
xmlns:loc="clr-namespace:WpfProgect.Base"
然後就可以呼叫了,如下:
<i:Interaction.Triggers> <i:EventTrigger EventName="SelectionChanged"> <!--★★★擴充套件的InvokeCommandAction★★★--> <loc:EventToCommand Command="{Binding StretchSelectionChangedCommand}" CommandParameter ="{Binding ElementName=sampleViewBox}"/> </i:EventTrigger>
後臺:
首先需要在ViewModel裡進行命令繫結的初始化,如:
StretchSelectionChangedCommand = new DelegateCommand() { ExecuteActionObj = new Action<object>(StretchSelectionChanged) };
當然,具體實現方式要根據自己編寫的DelegateCommand類來決定。
繫結的StretchSelectionChanged方法實現如下:
private void StretchSelectionChanged(object obj) { ComboBox cbStretch = ((ExCommandParameter)obj).Sender as ComboBox; Viewbox sampleViewBox = ((ExCommandParameter)obj).Parameter as Viewbox; if (cbStretch.SelectedItem != null) { sampleViewBox.Stretch = uiModel.StretchMode; } }
方法二:運用Behavior來實現事件,再運用檢視樹VisualTree來找所需的父控制元件或者子控制元件(控制元件到手了,就可以取到所需的引數),或者通過寫擴充套件屬性的方式來獲取控制元件,以下Demo是通過寫擴充套件屬性來實現的。
xaml呼叫方式如下:
<Slider x:Name="HSlider" Minimum="0" Maximum="100" Height="24" Margin="79,0,91,42" VerticalAlignment="Bottom" Width="150"> <i:Interaction.Behaviors> <behav:SliderBehavior TargetGrid="{Binding ElementName=theContainer}" TargetViewBox="{Binding ElementName=sampleViewBox}"/> </i:Interaction.Behaviors> </Slider>
SliderBehavior類如下:
class SliderBehavior : Behavior<Slider> { public readonly static DependencyProperty TargetGridProperty = DependencyProperty.Register("TargetGrid", typeof(Grid), typeof(SliderBehavior), null); public readonly static DependencyProperty TargetViewBoxProperty = DependencyProperty.Register("TargetViewBox", typeof(Viewbox), typeof(SliderBehavior), null); /// <summary> /// 獲得或設定命令引數。這是依賴屬性。 /// </summary> /// <value>命令引數。</value> /// <remarks>這是傳遞給 ICommand.CanExecute 和 ICommand.Execute 的值。</remarks> public Grid TargetGrid { get { return (Grid)base.GetValue(SliderBehavior.TargetGridProperty); } set { base.SetValue(SliderBehavior.TargetGridProperty, value); } } public Viewbox TargetViewBox { get { return (Viewbox)base.GetValue(SliderBehavior.TargetViewBoxProperty); } set { base.SetValue(SliderBehavior.TargetViewBoxProperty, value); } } protected override void OnAttached() { base.OnAttached(); this.AssociatedObject.ValueChanged += new RoutedPropertyChangedEventHandler<double>(HSlider_ValueChanged); } protected override void OnDetaching() { base.OnDetaching(); this.AssociatedObject.ValueChanged -= HSlider_ValueChanged; } void HSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { if (this.AssociatedObject.Name == "HSlider") TargetViewBox.Width = TargetGrid.ActualWidth * this.AssociatedObject.Value / 100.0; else TargetViewBox.Height = TargetGrid.ActualHeight * this.AssociatedObject.Value / 100.0; } }
View和ViemModel通訊
MVVMLight實現了一套略有複雜的訊息通訊,包含了定型別傳送、分組傳送、傳送給包含繼承型別的目標、廣播等。