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之间的通信