WPF MVVM
Model就是一个class,是对现实中事物的抽象,开发过程中涉及到的事物都可以抽象为Model,例如客户,客户的姓名、编号、电话、住址等属性也对应了class中的Property,客户的下订单、付款等行为对应了class中的方法。
View很好理解,就是界面。
上面说过Model抽象,那么ViewModel就是对View的抽象。显示的数据对应着ViewMode中的Property,执行的命令对应着ViewModel中的Command。
WPF中MVVM的解耦方式
在WPF的MVVM模式中,View和ViewModel之间数据和命令的关联都是通过绑定实现的,绑定后View和ViewModel并不产生直接的依赖。具体就是View中出现数据变化时会尝试修改绑定的目标。同样View执行命令时也会去寻找绑定的Command并执行。反过来,ViewModel在Property发生改变时会发个通知说“名字叫XXX的Property改变了,你们这些View中谁绑定了XXX也要跟着变啊!”,至于有没有View收到是不是做出变化也不关心。ViewModel中的Command脱离View就更简单了,因为Command在执行操作过程中操作数据时,根本不需要操作View中的数据,只需要操作ViewModel中的Property就可以了,Property的变化通过绑定就可以反映到View上。这样在测试Command时也不需要View的参与。这也是我在接触WPF初期时根本理解不了的所谓数据驱动。
数据绑定要达到的效果
数据绑定要达到什么效果呢,就是在界面中绑定了数据源之后,数据在界面上的修改能反映到绑定源,同时绑定源的修改也能反映到界面上。从界面反映到绑定的数据源是很容易理解的,因为在绑定过程中我们指定了DataContext和Binding的对象,很容易找到绑定的源并修改。但数据源修改时怎么通知界面呢?因为ViewModel中被绑定的属性并不知道谁绑定了它,如果在ViewModel中存一个View的引用,在数据发生变化时修改View,这无疑又将ViewModel和View耦合在了一起,而且这样做View中相应的控件没有开发完善难以进行测试,同样View中控件类型或名称发生改变时,ViewModel中相关代码都需要修改。在WPF中从数据源通知界面发生变化是通过发送通知的方式进行的,你可以想象一个string类型的Property,名字是TestString,在它发生变化时对着View大喊“TestString发生变化了,你们谁绑定了TestString需要跟着变啊!”,至于绑定的是TextBlock的Text,还是Label的Content,还是TextBox的Text,ViewModel并不关心,同样喊了后结果如何ViewModel也不关心。View在收到这个通知后看有没有绑定 了TestString的地方,找到了就修改,找不到就不管了,也不会在乎这个通知是哪个类型的ViewModel发的。这样ViewModel和View就解耦了,谁也不依赖对方。
INotifyPropertyChanged接口
在WPF中能够实现ViewModel向View喊话功能的就是INotifyPropertyChanged接口,它就像一个大喇叭一样,我们实现了这个接口,就可以通过触发PropertyChanged事件并给出改变的数据源的对象和属性名称,以此来通知数据的变化。这个接口的实现是非常简单的,下图代码就是一种非常简易的实现方式。由于在MVVM中所有的ViewModel和部分Model都需要实现这个接口来达到绑定的效果,因此一般会专门用一个类来实现这个接口,并将这个类作为ViewModel等需要数据更改后发送通知的类的基类。
ObservableCollection<t>集合</t>
有了NotifyObject这个基类后,我们就可以测试数据绑定了。为了让示例针对性更强,该示例仅仅用到了数据绑定,未添加命令绑定等其它内容。示例界面如图所示:
- 拖动滑块时,最上面TextBox中的数值会跟着变化,显示滑块条的当前值,同时主界面背景色的透明度也会跟着变化。
- 左下角的DataGrid和右下角的ListBox以及右边标签为下拉菜单的ComboBox中的数据都是一样的。选中这三个控件中的某行时,其它两个控件也会选中该项。右上方紫色方框内会显示选中行的信息。
- 在左下角DataGrid控件、右下角ListBox控件、右上角紫***域的某一处修改数据时,其它两处的数据也会随着修改。
下面说明如何通过数据绑定实现上面的功能
先说一下主界面的ViewModel,其中有double型属性DoubleValue,TestData型属性SelectedData,List<testdata>型属性TestDataList。这里没有使用ObservableCollection集合因为不涉及到集合内数据项的动态添加和删除。如果要使用ObservableCollection也非常简单,只需要把List<testdata>改为ObservableCollection<testdata>即可。</testdata></testdata></testdata>
功能1:Slider的Value、TextBox的Text和DockPanel的Background的Opacity都绑定了ViewModel中的DoubleValue。当拖动滑块时,Slider的Value发生了变化,会更新ViewModel中的DoubleValue,DoubleValue更新后会调用RaisePropertyChanged方法触发INotifyPropertyChanged中的PropertyChanged事件,并在事件参数中保存了发生改变的属性名称“DoubleValue”,这样View中绑定了DoubleValue的属性就要更新数据,TextBox的Text属性绑定了DoubleValue,所以TextBox更新了Text以显示最新的Slider的Value。同样主界面最顶层的布局容器DockPanel的Background中的Opacity也绑定了DoubleValue,更改后的表现就是背景色透明度发生了变化。整个过程如图中蓝色箭头所示。
功能2:DataGrid、ComboBox、ListBox的ItemsSource都绑定了ViewModel中的TestDataList,所以他们的列表项都是一样的。在DataGrid中我们显示了TestData的所有属性,ComboBox中我们只显示了StringValue属性,在ListBox中则重写了数据项模板,按照我们想要的方式显示TestData中的BoolValue、AddDateTime、和IntValue。同时这三个控件的SelectedItem也都绑定了ViewModel中的SelectedData,因此当其中一个的选中项发生改变时,其它两个控件的选中项也会相应发生改变,原理参照功能1。此外右上角紫域的几个TextBox和一个CheckBox分别绑定了SelectedData的不同属性,因此当SelectedData发生改变时,紫域内各个控件的显示内容也会发生改变。
功能3:因为TestData类也继承自NotifyObject,而且几个属性在发生改变时也会调用RaisePropertyChanged方法发出通知,因此所有绑定了这些属性的控件也会随着更新数据。表现出来就是在任何地方修改了TestData中的属性,所有绑定了这些属性的控件都会更新。
当然了,还需要在主界面的后台代码中把DataContext设置为ViewModel的一个实例。
DataContext = new MainWindowViewModel();
其实上面很多功能可以不借助ViewModel来实现的。例如功能1中把TextBox的Text直接绑定到Slider的Value上,同样功能二中ListBox的SelectedItem也可直接绑定到DataGrid的SelectedItem上,之所以使用ViewModel是为了演示MVVM模式下的数据绑定。
命令绑定要达到的效果
命令绑定要关注的核心就是两个方面的问题,命令能否执行和命令怎么执行。也就是说当View中的一个Button绑定了ViewModel中一个命令后,什么时候这个Button是可用的,按下Button后执行什么操作。解决了这两个问题基本就实现了命令绑定。另外一个问题就是执行过程中需要的数据(参数)要如何传递。本次主要探讨这几个问题。
** 命令绑定的实现**
自定义一个能够被绑定的命令需要实现ICommand接口。该接口包含:
public event EventHandler CanExecuteChanged // 在命令可执行状态发生改变时触发
public bool CanExecute(object parameter) //检查命令是否可用的方法
public void Execute(object parameter) //命令执行的方法
CanExecute和Execute方法是接口给出的,我们要做的就是新建一个类MyCommand来实现这两个方法执行的内容。可以通过在MyCommand的构造函数中传入Action<object>和Func<object,bool>,让CanExecute执行Func<object,bool>,Execute执行Action<object>。实现后MyCommand结构如下图所示,其中浅绿色背景的为ICommand接口的实现,浅蓝色背景的为MyCommand的成员。</object></object>
添加泛型支持的命令MyCommand
命令绑定时经常需要传参数,这种情况下可以给MyCommand添加泛型支持
MVVM设计模式和在WPF中的实现(四)
事件绑定
为什么要事件绑定
这个问题其实是很好理解的,因为事件是丰富多样的,单纯的命令绑定远不能覆盖所有的事件。例如Button的命令绑定能够解决Click事件的需求,但Button的MouseEnter、窗体的Loaded等大量的事件要怎么处理呢?这就用到了事件绑定。
事件绑定
要使用事件绑定需要借助System.Windows. interactivity,如果安装了Blend,里面就包含了这个dll。需要在Interaction.Triggers里面添加一个或多个EventTrigger并指定关注的的事件名称,在EventTrigger中通过InvokeCommandAction来绑定事件对应的命令。图中所示绑定了主窗口的Loaded事件,在事件触发后会调用绑定的命令对象LoadedCommand的Execute方法执行命令,当命令绑定需要参数时可以通过绑定CommandParameter实现。需要指出的是之前在实现MyCommand的Execute方法时我们加入了CanExecute的判断,因此事件触发后是否能够真正执行绑定的命令也受到绑定的LoadedCommand的CanExecute方法的影响。
带EventArgs参数的事件绑定
上面介绍的事件绑定并不足以应对所有的情况,因为很多情况下我们还需要从事件的EventArgs中获取数据,例如从MouseMove事件参数中获取鼠标位置和按键状态等。但InvokeCommandAction在未对CommandParameter绑定的情况下给Execute方法传递的参数为null。因此我们需要自己写一个类来处理事件到命令的绑定。
看一下上面我们用到的InvokeCommandAction,继承自TriggerAction<dependencyobject>,TriggerAction是一个抽象类,我们只要继承这个类并实现Invoke方法即可。TriggerAction在MSDN中的介绍如下:</dependencyobject>
MVVM模式解析和在WPF中的实现(五)
View和ViewModel的通信
消息通信的方式主要受到MVVMLight的启发,MVVMLight实现了一套略有复杂的消息通信,包含了定类型发送、分组发送、发送给包含继承类型的目标、广播等。就目前我做的几个小项目来说,View和ViewModel通信本身用的就不是那么频繁,需求也不算旺盛,所以自己实现了一套比较简易的消息通信。View在实例化的时候注册消息,通过一个列表保存注册的消息,消息在发送的时候根据条件从列表中找到相应的消息并执行操作,如下图所示:
消息发送和处理:
比较奇怪的是为什么要引入一个消息注册器,在View的后台代码中直接注册不就可以了吗?好吧,其实最初的想法确实比较强迫症,只是单纯的不想在后台中写入太多的代码(我真不是处女座),这样看上去似乎更高端。不过后来想了下,View对ViewModel(虽然不是接口)和消息注册器实际上都算是一种依赖,而且View对ViewModel和消息注册器的依赖都是唯一的,也就是说一个View只有一个ViewModel和一个消息注册器。这样可以用控制反转的方式把对ViewModel和消息注册器的依赖一起注入进来,而且在注入过程中可以顺便配置ViewModel的Dispatcher以方便跨线程修改UI,也可以给ViewModel配置单独的MessageManager让View和ViewModel的通信进入另一个次元,不受其他消息干扰。这些在讨论ViewModel依赖注入的时候将会尝试。
关于跨线程修改UI
这个顺带提一下,因为实现起来很简单。在ViewModel中有时会遇到使用其它线程修改UI的情况,我之前是通过 App.Current.MainWindow.Dispatcher来获取UI线程的调度器的。当然也可以把UI线程的调度器保存到一个静态变量中以便随时访问。不过我一直没搞明白MaiWindow的Dispatcher和非MainWindow的Dispatcher有什么区别,不过还是在ViewModel的基类中加入了Dispatcher这个属性,这样在给View注入ViewModel的时候可以把ViewModel的Dispatcher设置为绑定的View的Dispatcher,虽然并不太清楚这有什么卵用 -_-|||
MVVM模式解析和在WPF中的实现(六)
用依赖注入的方式配置ViewModel并注册消息
ViewModel和MessageManager的依赖注入
使用静态类ViewModelManager来当作IoC Container。往IoC Container里注册依赖关系一般有两种方式,一种是将依赖关系以某种形式(例如xml)保存在外部,一种是在程序中注册到一个列表里。我采取第二种做法,因为比较容易:)
程序在启动时使用ViewModelManager.Register将依赖关系注册到ViewModelManager中,View在构造函数中调用ViewModelManager.SetViewModel(this);来设置View的DataContext并通过依赖的消息注册器注册消息,消息注册器可以为空,代表View不接收消息。它们的关系如图所示:
需要说明的有两个地方:
一个是View和ViewModel的对应关系。一般来说一个View对应着一种ViewModel,这样注册起来是没问题的。但这个并不绝对,理论上来说一个View可以将DataContext设置为任意ViewModel,如果一个View可以设置多种ViewModel该如何处理呢,这时候可以在ViewModelManager注册时添加Token属性,然后用SetViewModel(this,token)的方式指定特定的ViewModel为DataContext。
另一个是消息注册的范围。因为一般来说ViewModel都是和绑定的View通信。所以默认情况下,消息注册到单独一个MessageManager中,这个MessageManager保存ViewModel中,ViewModel使用这个MessageManager发送消息,发送的消息由View接收。但如果需要和其他View通信,需要把消息注册到MessageManager.Default中,这个对象是static的,要达到这个目的只要在View设置ViewMode时这样来SetViewModel(this,isGlobalMsg:true)即可。如果ViewModel又想和绑定的View单独通信,有时候还需要和别的View通信,可以在消息注册器中注册时将需要单独通信的消息设置一个Group,ViewModel在发送消息时加一个Group过滤一下即可。一个Group可以理解为消息的单独一个通道。
顺带一提,好吧只是顺带一提的是,在给View注入ViewModel时,顺便把ViewModel的UIDispatcher属性设置为了View的Dispatcher,虽然我不知道这有什么用。但这样在ViewModel中使用UIDispatcher时即为相关的View的Dispatcher。如果要使用MainWindow的Dispatcher可以通过DispatcherHelper.Dispatcher或者App.Current.MainWindow.Dispatcher获得。
路由事件的三种方式/策略(冒泡 直接 隧道)
(1)冒泡,这种事件处理方式是从源元素向上级流过去,直到到达根节点即顶层节点,一般为最外层的控件。
(2)直接,这种处理方式是在源上处理,主要用在源元素上处理。通常setter和trigger中有所体现,我个人认为VisualState可视状态可能也是直接事件处理,也是依赖属性的状态改变。和Trigger有一定的重复,但是VisualState是通过生硬的动画间接实现依赖属性的改变。
(3)隧道,又称作Preview事件,和冒泡事件处理方式相反的。元素树的根位置调用事件处理程序,依次向下直到源元素位置。
隧道事件和冒泡事件一般成对出现。同一事件,执行时首先是隧道事件,然后
Routed Events(路由事件) 与 Commands(命令)
Event 和 Command 是程序内部通信基础,Routed Events 能够发起多重控件,并且能有序和用户沟通。
Commands是.NET Framework 提供的核心构架,来激活和去除高级别任务。
由此衍生的Animation是events的更进一步。让你能够以友好互动的方式使用Event架构,来使用多重控件。
跨线程操作(Dispatcher)
在winform中默认不允许跨线程操作控件,但可通过设置控件属性CheckForIllegalCrossThreadCalls = false。在WPF中同样也是禁止跨线程操作控件的,会提示“调用线程无法访问此对象,因为另一个线程拥有该对象。”的错误。
什么是XML扩展XAML?有何优点
1 XAML是eXtensible Application Markup Language的英文缩写,相应于中文称为可扩展应用程序标记语言,它是微软公司为构建应用程序用户界面而创建的一种新的描述性语言。
2 XAML是XML语言的一个衍生物,它的语法与XML语言完全一致,它的功能就是专门用来设计和实现程序的UI。
3 XAML一个很大的优点就是由于WPF支持WEB开发,那么WEB开发和桌面开发的转换是很简单,修改的地方很短,同时UI与逻辑完全分离,所以逻辑代码也几乎不用改动。
解释什么是依赖属性,它和以前的属性有什么不同?为什么在WPF会使用它?
1 Windows Presentation Foundation (WPF) 提供了一组服务,这些服务可用于扩展公共语言运行时 (CLR) 属性的功能,这些服务通常统称为 WPF 属性系统。由 WPF 属性系统支持的属性称为依赖项属性。
2 它和以往属性的不同之处有
(1)依赖属性是一种特定类型的属性。这种属性的特殊之处在于,其属性值受到 Windows 运行时中专用属性系统的跟踪和影响。
(2)依赖属性的用途是提供一种系统的方式,用来基于其他输入(在应用运行时其内部出现的其他属性、事件和状态)计算属性的值。
(3)依赖属性代表或支持编程模型的某种特定功能,用于定义 Windows 运行时应用,这种模型使用 XAML 编写 UI,使用 C#、Microsoft Visual Basic 或 Visual C++ 组件扩展 (C++/CX) 编写代码。
一般的属性没有这么复杂。
WPF使用它是有不少优点的
(1)优化了属性的存储,直接减少了不必要的内存使用。
(2)有属性变化通知 限制 验证等。
(3)可以储存多个值,配合Expression及Animation等,打造出更灵活的使用方法。
** 在WPF中Binding的作用及实现语法?**
典型的Binding具有四个重要组成部分:Binding目标对象(binding target object) 目标对象属性(target property) Binding数据源(binding source) Path(用于指明要从数据源中取得的值,就是我们通常写的属性名称)。
如何理解WPF体系结构?
WPF使用多层架构,类似于三层结构,最顶层部分为托管代码API,此层用于为开发人员编写WPF应用程序提供较高层次的服务,基于C#托管代码编写。转换.NET代码到DirectX的工作由中间层milcore.dll实现。中间层milcore是用非托管代码实现,因为它需要与DirectX紧密集成,对性能敏感,就是消耗的资源比较多,对性能影响较大。
WPF中什么是样式?
首先明白WPF中样式属于资源中重要的一种。
同时样式也是属性值的集合,能被应用到一个合适的元素中,或者说能将一组属性应用到多个元素。
WPF中样式可以设置任何依赖属性。
WPF中样式也支持触发器,通过属性的改变,触发一组活动,包括改变某个控件的样式。
WPF中元素只能使用一个样式。
样式有继承的特性,样式可以继承样式。
WPF中什么是模板 ?
WPF中模板是用于定义或重定义控件结构,或者说对象的外观。
WPF中模板有两类,一个是控件模板(ControlTemplate) 另一个是数据模板(DataTemplate),它们都派生自FrameworkTemplate抽象类。
总共有三大模板 ControlTemplate,ItemsPanelTemplate,DataTemplate。
1 ControlTemplate 主要用途是更改控件的外观。它有两个重要属性:VisualTree(视觉树)内容属性和Triggers触发器,对于触发器可以不用过多考虑,触发器可有可无。VisualTree就是呈现我们所画的控件。Triggers可以对我们的视觉树上的元素进行一些变化。
2 ItemsPanelTemplate 是个特殊的空间模板,主要用来标明多条目控件如何显示它所包含的多项数据。也可以说是指定用于项的额布局的面板。多用于多个内容控件的目标。多为Panel属性或者Panel结尾的属性。
3 DataTemplate 主要用于数据的呈现。也被称为显示绑定数据对象的模板。
绑定(Binding )的基础用法
WPF 里分三种:Binding,PriorityBinding,MultiBinding,这三种Binding的基类都是BindingBase,而BindingBase又继承于MarkupExtension。
常见的使用Binding方法是:
1 针对于继承于FrameworkElement控件。 SetBinding(DependencyProperty dp,String path),SetBinding(DependencyProperty dp,BindingBase binding),其中FrameworkElement中SetBinding只对DependencyProperty有效。
2 另一种是 BindingOperations.SetBinding(currentFolder,TextBlock.TextProperty,binding);
BindingOperations.SetBinding的原型是
public static BindingExpressionBase SetBinding(DependencyObject target,DependencyProperty dp,BindingBase binding)
3 清除Binding:
BindingOperations.ClearBinding(currentFolder,TextBlock.TextProperty);//删除currentFolder上的TextBlock.TextProperty绑定
BindingOperations.ClearAllBindings(currentFolder);//删除currentFolder上的所有绑定。
直接对Dependency Property赋值也可以解除binding,不过只对单向binding有效。
*解释这几个类的作用及关系: Visual, UIElement, FrameworkElement, Control *
它们四个的关系:从System.Windows.Controls命名空间中看,依次的继承关系是:
Visual继承UIElement,UIElement继承FrameworkElement,FrameworkElement继承Control。
1 Visual主要作用是为WPF提供2D呈现支持,主要包括输出显示,坐标转换,区域剪切等。
2 UIElement的主要作用是构建WPF元素和基本呈现特征的基类。例如其中定义很多与输入和焦点有关的特性,例如键盘事件,鼠标,还有一些与WPF事件模型有关的API。
3 FrameworkElement的主要作用是为定义的WPF元素添加一些功能。例如,布局定义 逻辑树 对象生命周期事件 支持数据绑定和动态资源引用 支持样式和动画。
4 Control的主要作用是为自定义应用程序控件提供基础。因为它是创建自定义应用程序控件的基类,作用就是可以重写Control类所提供的属性,方法,事件等,为自定义控件添加自定义逻辑。构建WPF应用程序页面的Window类也派生自它。
视觉树 VS 逻辑树?
1 逻辑树是视觉树的子集,也就是视觉树基本上是逻辑树的一种扩展。
2 WPF通过逻辑树来解决依赖项属性继承和资源的问题,使用视觉树来处理渲染,事件路由,资源定位等问题。
3 逻辑树可以认为是XAML所见的,而视觉树包含了XAML元素内部的结构。
4 逻辑树的查找可以通过LogicalTreeHelper辅助类,视觉树的查找可以通过VisualTreeHelper辅助类,其中需要注意的是对ContentElement元素的查找,无法直接通过VisualTreeHelper进行查找,ContentElement元素并不继承Visual,而ContentElement元素的使用时需要一个ContentElement载体FrameworkContentElement。
属性变更通知(INotifyPropertyChanged 和 ObservableCollection<t>)</t>
1 INotifyPropertyChanged向客户端发出某一属性值更改的通知。
2 ObservableCollection<t>类,它是实现 INotifyCollectionChanged 接口的数据集合的内置实现。表示一个动态数据集合,在添加项、移除项或刷新整个列表时,此集合将提供通知
*ResourceDictionary *</t>