1. 程式人生 > 實用技巧 >WPF之再談MVVM

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(string
property) { 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實現了一套略有複雜的訊息通訊,包含了定型別傳送、分組傳送、傳送給包含繼承型別的目標、廣播等。

ViemModel和ViemModel之間的通訊