《深入浅出WPF》学习笔记

第一部分 深入浅出XAML

WPF是什么?

WPF是Windows Presentation Foundation的简称,是用来专门编写程序表示层的技术

1.XAML概览

1.1XAML是什么

XAML(Extensible Application Markup Language):可扩展应用程序标记语言,类似于HTML+CSS+JavaScript的组合

XAML是WPF技术中专门用来设计UI的语言

XAML是一种由XML派生而来的语言,所以很多XML中的概念在XAML是通用的

XAML是一种"声明"式语言,当看到一个标签,就意味着声明了一个对象,对象之间的层级关系要么是并列、要么是包含,全都体现在标签的关系上

2.从零起步认识XAML

2.2剖析最简单的XAML

xmlns是XML-Namespace的缩写,定义名称空间的好处是,当来源不同的类同名时,可以使用名称空间加以区分。XML特征的语法格式:xmlns[:可选的映射前缀]="名称空间"

x:Class这个Attribute的作用是当XAML解析器将包含它的标签解析成C#类后,这个类的类名是什么

3.系统学习XAML语法

3.1 XAML文档的树形结构

在XAML中,每个标签对应.NET Framework类库中的一个控件类

XAML使用树形逻辑结构来描述UI

3.2 XAML中为对象属性赋值的语法

因为XAML语言不能编写程序的运行逻辑,所以一份XAML文档中除了使用标签声明对象就是初始化对象的属性

XAML中为对象属性赋值共有两种语法:

  • 使用字符串进行简单赋值
  • 使用属性元素进行复杂赋值

3.3 事件处理器与代码后置

将逻辑代码与UI代码分离、隐藏在UI代码后面的形式就叫做"代码后置"

3.4 导入程序集合引用其中的名称空间

语法:xmlns:映射名="clr-namespace:类库中名称空间的名字;assembly=类库文件名"

4.x名称空间详解

x名称空间里的成员是专门写给XAML编译器看、用来引导XAML编译器把XAML代码编译成CLR代码的

x名称空间中包含的类均与解析XAML语言相关

与C#语言一样,XAML也有自己的编译器。XAML语言会被解析并编译,最终形成微软中间语言存储程序集中。

x名称空间中包含的工具:

4.2 x名称空间中的Attribute

在使用XAML编程的时候,如果你想给它加上一些特殊的标记从而影响XAML编译器对它的解析,这时候就需要额外为它添加一些Attribute。

4.2.1 x:Class

这个Attribute的作用是告诉XAML编译器将XAML标签的编译结果与后台代码中指定的类合并。在使用x:Class时必须遵守以下要求:

  • 这个Attribute只能用于根节点
  • 使用x:Class的根节点的类型要与x:Class的值所指示的类型保持一致
  • x:Class的值所指示的类型在声明时必须使用partial关键字

4.2.2 x:ClassModifier

这个Attribute的作用是告诉XAML编译由标签编译生成的类具有怎样的访问控制级别

使用这个Attribute时需要注意:

  • 标签必须具有x:Class Attribute
  • x:ClassModifier的值必须与x:Class所指示的访问控制级别一致

4.2.3 x:Name

XAML是一种声明式语言,那么XAML标签声明的是什么呢?

XAML的标签声明的是对象,一个XAML标签对应着一个对象,这个对象一般是一个空间类的实例

x:Name的作用:

  • 告诉XAML编译器,当一个标签带有x:Name时除了为这个标签生成对应实例外还要为这个实例声明一个引用变量,变量名就是x:Name的值
  • 将XAML标签所对应对象的Name属性(如果有)也设为x:Name的值,并把这个值注册到UI树上,以方便查找

另外注意:如果标签本身有Name的属性(继承自FrameworkElement类),那么该属性和x:Name的作用是一样的,但是一个标签中只能同时用一个,为了代码可读性,后面统一使用x:Name

4.2.4 x:FiledModifier

XAML标签中的使用x:Name产生的实例的访问权限默认是Internal

x:FiledModifier是用来在XAML里改变引用变量访问级别的

4.2.5 x:Key

在WPF中,几乎每个元素都有自己的Resources属性,这个属性是个"Key-Value"式的集合,只要把元素放进这个集合,这个元素就成为资源字典中的一个条目,x:Key的作用就是为资源贴上用于检索的索引

4.2.6 x:Shared

x:Shared一定要与x:Key配合使用,如果

  • x:Shared=true,每次检索的对象都是同一个对象(默认是true)
  • x:Shared=false,每次检索对象时,得到的是这个对象的新副本

4.3 x名称空间中的标记扩展

4.3.1 x:Type

当我们在XAML中想表达某个数据类型时就需要使用x:Type标记扩展

例子:

①自定义Button:

②创建一个MyWindow1页面:

②创建一个MyWindow2页面,使用x:Type指定数据类型为MyWindow1:

4.3.2 x:Null

当一个属性具有默认值而我们又不需要这个默认值时,就需要显式地设置null值

例子:

为所有Button设置同样的样式,但是又想让个别Button不具有这个样式时,可以使用x:Null将默认样式设置为null

4.3.4 x:Array

x:Array的作用就是向它的Items属性向使用者暴露一个类型已知的ArrayList实例,ArrayList内成员的类型是通过x:Array的Type指明

4.3.5 x:Static

在XAML文档中使用数据类型的static成员

例子:

①定义一个静态字段和一个静态字段在处理逻辑的partial类中:

②在XAML中获取静态数据:

4.4 XAML指令元素

XAML一共有两个指定元素:

  • x:Code
  • x:XData

x:Code:可以在此标签中包含一些放置在后置代码中的C#代码,这样做的好处是不用把XAML代码和C#代码分置在两个文件中,但若不是遇到某些极端环境尽量不要这样做,因为这样做产生的问题是代码不好维护且不易调试

例子:

①在XAML中使用x:Code编写逻辑代码:

②之后通过XAML编译器将XAML解析成C#代码之后可以看到这块代码:

x:XData:

在x:XData元素内的元素XAML不会将其中的XML元素视为XAML命名空间或任何其他XAML命名空间的一部分,x:XData可以包含任意格式良好的XML

例子:

①定义数据源:

②使用数据源:

5.控件与布局

5.1 控件到底是什么

WPF之所以能够称得上是新一代关键在于两点:

  • 之前几代GUI方法论只能使用编程语言进行UI设计,而WPF具有专门用于UI设计的XAML
  • 前几代UI与数据的交互方面是由Windows消息到控件事件一脉相承,始终是把UI控件放在主导地位而把数据放在别动地位,用UI来驱动数据的改变,WPF在事件驱动的基础上引入了数据驱动界面的理念,让数据重归核心地位让UI回归数据表达者的位置

控件定义:能够展示数据、响应用户的操作

在WPF中谈控件,我们关注的应该是抽象的数据和行为而不是控件具体的形象

控件的派生关系:

FrameworkElement类在UIElement类的基础上添加了很多专门用于WPF开发的API,所以从这个类开始才算是进入WPF开发框架

5.2 WPF的内容模型

WPF UI元素分为如下类型:

对于内容控件,XAML标签的内容区域专门映射了控件的内容属性

①Button(Content):

②StackPanel(Children):

③ListBox(Items):

5.3 各类内容模型详解

族:符合某类内容模型的UI元素

每个族用它们共同基类来命名

5.3.1 ContentControl族

特点:

  • 均派生自ContentControl类
  • 它们都是控件
  • 内容属性的名称为Content
  • 只能由单一元素充当其内容(如Button,内容不能同时包含文本和图片)

ContentControl族包含的控件:

5.3.2 HeaderedContentControl族

特点:

  • 它们都派生自HeaderedContentControl类,HeaderedContentControl是ContentControl类的派生类
  • 它们都是控件,用于显示带标题的数据
  • 除了用于显示主体内容的区域外,控件还具有一个显示标题的区域
  • 内容属性为Content和Header
  • 无论是Content还是Header都只能容纳一个元素作为其内容

HeaderedContentControl族包含的控件:

GroupBox例子:

5.3.3 ItemsControl族

特点:

  • 均派生自ItemsControl类
  • 它们都是控件,用于显示列表化的数据
  • 内容属性为Items或ItemsSource
  • 每种ItemsControl都对应有自己的条目内容器

ItemsControl族包含的控件:

在通过XAML编写UI的时候,才发现其实我们用很简单的几个标签就可以表达一个控件,但是通过VisualTreeHelper去查找一个标签的父级标签发现有好多隐藏的标签,这可能已经被XAML编译器给自动补全了(也就是ItemsControl能够使用对应的Item Container自动包装数据),如下例子:

虽然直接在Items下加入各种Item如Button不加任何Panel、Border也说得过去,但是编译器这样优化应该是有一定的理论与逻辑支撑在的

ItemsControl对应的Item Container:

5.3.4 HeaderedItemsControl族

特点:

  • 均派生自HeaderedItemsControl类
  • 它们都是控件,用于显示列表化的数据,同时可以显示一个标题
  • 内容属性为Items、ItemsSource和Header

本族控件有:MenuItem、TreeViewItem、ToolBar

5.3.5 Decorator族

特点:

  • 均派生自Decorator类
  • 起UI装饰作用
  • 内容属性为Child
  • 只能由单一元素充当内容

Decorator族元素:

5.3.6 TextBlock和TextBox

这两个控件最主要的功能是显示文本

TextBlock

  • 只能显示文本,不能编辑,所以又称静态文本
  • 可以使用丰富的印刷级的格式控制标记显示专业的排版效果
  • 内容属性是Inlines,同时也保留了Text的属性

TextBox

  • 允许用户编辑其中内容
  • 内容属性是Text

5.3.7 Shape族元素

用来在UI上绘制图形的一类元素

特点:

  • 均派生自Shape类
  • 用于2D图形绘制
  • 无内容属性
  • 使用Fill属性设置填充,使用Stroke属性设置边线

绘制一个仿3D球体:

5.3.8 Panel族元素

作用是控制UI布局

特点:

  • 均派生自Panel抽象类
  • 内容属性为Children
  • 内容可以是多个元素,Panel元素将控制它们的布局

Panel族元素:

5.4 UI布局

5.4.2 Grid

Grid元素会以网格的形式对内容元素们进行布局

特点:

  • 可以定义任意数量的行和列,非常灵活
  • 行的高度和列的宽度可以使用绝对数值、相对比例或自动调整的方式进行精确设定,并可设置最大和最小值
  • 内部元素可以设置自己的所在的行和列,还可以设置自己纵向跨几行、横向跨几行
  • 可以设置Children元素的对齐方向

Grid中行的高度和列的宽度的单位:

对于Grid的行高和列宽,我们可以设置三类值:

  • 绝对值:double数值加单位后缀
  • 比例值:double数值加一个*
  • 自动值:Auto

Grid例子:

5.4.5 DockPanel

DockPanel内的元素会被附加上DockPanel.Dock这个属性,这个属性的数据类型为Dock枚举。Dock枚举可取Left、Top、Right和Bottom四个值。根据Dock属性值,DockPanel内的元素会向指定方向累积、切分DockPanel内部的剩余可用空间,就像船舶靠岸一样。DockPanel还有一个重要属性——bool类型的LastChildFill,它的默认值是True。当LastChildFill属性的值为True时,DockPanel内最后一个元素的DockPanel.Dock属性值会被忽略,这个元素会把DockPanel内部所有剩余空间充满。这也正好解释了为什么Dock枚举类型没有Fill这个值。

例子:

我们可以通过每次添加一个Item来看整个布局的变化,以此了解DockPanel的布局逻辑

①向DockPanel中添加第一个TextBox:

虽然设置了Dock为Top,按理说这个文本框会置顶,但是因为DockPanel有一个LastChildFill属性导致Dock属性失效,然后如果没设置Width,就默认会以整体窗体的Width大小为准

②向DockPanel中添加第二个TextBox:

当添加第二个TextBox之后,第一个TextBox就置顶了,那么第二个TextBox其实又从剩下的位置(除了第一个TextBox的位置)来设置布局,同理,第二个TextBox的Dock的Left失效

③向DockPanel中添加第三个TextBox:

把LastChildFill设为false,最后一个将不会填满剩下的窗体位置:

可以看到,最后一个TextBox停靠在剩余的窗体左边,这里我没有设置TextBox的Height和Width,不清楚这个默认的长和宽是怎么来的

5.4.6 WrapPanel

WrapPanel内部采用的是流式布局。WrapPanel使用Orientation属性来控制流延伸的方向,使用HorizontalAlignment和VerticalAlignment两个属性控制内部控件的对齐。在流延伸的方向上,WrapPanel会排列尽可能多的控件,排不下的控件将会新起一行或一列继续排列。

例子:

第六个Button在第一行的宽度已经不够了,所以他们流向第二行

启动程序之后,改变窗体的宽度,可以看到Button按钮会自适应的满足一行最多可以放几个Button,多余的会继续向下一行流

第二部分 游历WPF内部世界

6.深入浅出Binding

UI驱动程序:程序是被来自UI的事件(即封装过的消息)驱使向前的,简称“消息驱动”或“事件驱动”

6.2 Binding基础

Binding一词更注重表达它是一种像桥梁一样的关联关系,WPF中,正是在这段桥梁上我们有机会为往来流通的数据做很多事情

对于绑定的解释,此书太清晰了:如果把Binding比作数据的桥梁,那么它的两端分别是Binding的源(Source)和目标(Target)。数据从哪里来哪里就是源,Binding是架在中间的桥梁,Binding目标是数据要往哪儿去(所以我们就要把桥架向哪里)。一般情况下,Binding源是逻辑层的对象,Binding目标是UI层的控件对象,这样,数据就会源源不断通过Binding送达UI层、被UI层展现,也就完成了数据驱动UI的过程。“一桥飞架南北,天堑变通途”,我们可以想象Binding这座桥梁上铺设了高速公路,我们不但可以控制公路是在源与目标之间双向通行还是某个方向的单行道,还可以控制对数据放行的时机,甚至可以在桥上架设一些“关卡”用来转换数据类型或者检验数据的正确性。

例子:

①增加一个数据源

②这里要说一个概念,Binding的路径(Path),这个就是UI上的元素关心的那个属性值的变化。那么我们在数据源中做的就是当值变化后属性要有能力通知Binding,让Binding把变化传递给UI元素,方法就是实现INotifyPropertyChanged接口然后在属性的set语句中激发一个PropertyChanged事件,当为Binding设置了数据源后,Binding就会自动侦听来自这个接口的PropertyChanged事件

③在窗体逻辑类中加入Binding代码:

简易写法是因为各个控件都继承了FrameworkElement基类,这个基类有个binding方法

其实这里控件能及时取得更新的数据还是依靠了委托做回调

Binding模型

6.3 Binding的源与路径

Binding源:是一个对象并且通过属性公开自己的数据

6.3.1 把控件作为Binding源与Binding标记扩展

有时候为了让UI元素产生一些联动效果也会使用Binding在控件间建立关联

例子:

这里使用了Binding的标记扩展语法 {Binding Value }的写法和{Binding Path=Value}是一样的,因为Path是Binding对象中构造函数的形参所以形参名可忽略不写,ElementName是Binding对象的一个属性,保存的是控件的Name

6.3.2 控制Binding的方向及数据更新

Binding.Mode,这是个枚举对象,用于确定数据流向,有5个枚举值:

  • TwoWay 双向
  • OneWay 从Source -> Target
  • OnTime 只绑定一次,也就是后续任何一方数据改动不影响另一方
  • OneWayToSource 从Target -> Source
  • Default 视控件类型而定,如TextBox,双向;TextBlack,单向

Binding.UpdateSourceTrigger,这也是个枚举,什么时刻数据开始流动,有4个枚举值:

  • PropertyChanged 属性值改变那一刻
  • LostFocus 失去焦点那一刻
  • Explicit 调用BindingExpression.UpdateSource这个方法之后
  • Default 视控件类型而定

例子:

在文本框中写成80,焦点还在文本框中,滑动块也还在初始位置

移除焦点,滑动块移动到指定位置

6.3.3 Binding的路径(Path)

Binding的多级路径:

就是不仅可以将一个TextBox对象的Text作为path还可以将Text.Length作为Path,但要注意,Length是string下面的只读属性,如果现在有从textBox2.length流向textBox1.length的逻辑的时候,就会抛出异常,所以需要使用Binding.Mode来控制数据流向

奇怪的Path:

感觉Path的语法好奇怪呀,没有太get到逻辑在哪里,以一个List为source为例:

path如果是"/"那么取的就是list中的第一个值,为什么??

6.3.4 "没有Path"的Binding

Binding源本身就是数据且不需要Path来指明。典型的,string、int等基本类型就是这样,这时我们只需将Path的值设置为“.”就可以.在XAML代码里这个“.”可以省略不写,但在C#代码里却不能省略。

例子:

XAML:

点(.)也可以直接省去

C#:

6.3.5 为Binding指定源(Source)的几种方法:

把依赖对象(Dependency Object)指定为Source:依赖对象不仅可以作为Binding的目标,同时也可以作为Binding的源。这样就有可能形成Binding链。依赖对象中的依赖属性可以作为Binding的Path。

上面的Binding链举例子就是:

Student.Name改变导致textBox1.TextProperty改变,textBox1的TextProperty改变,导致TextBox2.TextProperty改变...马上,依赖对象在这些控件类中作为静态属性的存在,互相绑定有意义吗..自己绑定自己?

其实目标 <- 目标(源) <- 目标,这样的形式也可以制造Binding链,只要有属性即作为源又作为目标即可

6.3.6 没有Source的Binding-使用DataContext作为Binding的源

首先,DataContext属性是FrameworkElement的属性,而FrameworkElement是控件的基类,所以每个控件都有这个属性,而又因为这个属性是依赖属性、xaml的UI同时也是一个控件树的结构,所以,如果上层的树(控件)节点有对这个DataContext有赋值的话,那么它会判断它的子节点(控件)有没有对这个值赋值,如果没有赋值,就会给这个节点赋上值,这是一种依赖属性的值传递行为,而又因为使用xaml做Binding时,如果不设定源的话,那么xaml就会去找DataContext属性作为Source

例子:

①创建Student类 :

②为外层的StackPanel对象的DataContext属性赋Student对象值,然后为内部的TextBlock控件的Text属性做一个没有Source的Binding:

可以看到,Student的属性正常显示在窗体上

那么我们把StackPanel.DataContext赋于string、int等基础类型的值,Binding可以怎么设置呢?

我们就可以使用{Binding}这种奇怪的语法了

DataContext的用法:

  • 当UI上的多个控件都使用Binding关注同一个对象时,不妨使用DataContext
  • 当作为Source的对象不能被直接访问的时候——比如B窗体内的控件想把A窗体内的控件当作自己的Binding源时,但A窗体内的控件是private访问级别,这时候就可以把这个控件(或者控件的值)作为窗体A的DataContext(这个属性是public访问级别的)从而暴露数据

6.3.7 使用集合对象作为列表控件的ItemsSource:

①为ListBox指定一个集合,并选择集合中的某一个元素显示:

这里重点注意DisplayMemberPath这个属性 ,ListBox会用这个属性作为Binding的Path,然后Binding的目标是ListBoxItem的内容插件(TextBox)

在使用集合类型作为列表控件的ItemsSource时一般会考虑使用ObservableCollection<T>代替List<T>,因为ObservableCollection<T>类实现了INotifyCollectionChanged和INotifyPropertyChanged接口,能把集合的变化立刻通知显示它的列表控件,改变会立刻显现出来

6.3.9 使用XML数据作为Binding的源

XML文本是树形结构的,所以XML可以方便地用于表示线性集合和树形结构数据

当使用XML数据作为Binding的Source时我们将使用XPath属性而不是Path属性来指定数据的来源

例子:

①一个包含学生列表的XML文件:

②在XAML中为列表控件ListView的内容指定Binding的Path

③为Binding指定Source和Path,并将ListView.ItemsSourceProperty作为Binding的target

我们还可以直接将XML内容写在XAML中,只需要使用x:XData指令元素即可:

效果:

6.3.10 使用LINQ检索结果作为Binding的源

使用LINQ的目的是用于对集合数据做一下处理而已,其实和Binding本身关系并不大,那么这里只要看一下如何使用Linq做数据处理就好了

使用Linq处理集合:

目前习惯使用lambda的方式来使用Linq,用起来也非常爽,原生的这种类似sql的语法结构就不太懂了

使用Linq处理XML集合,这里因为XAML binding的是Student对象的Id和Name所以要从XML对象中取出相应的值到Student对象中:

这里顺便梳理了一下Linq的Where、Foreach和Select:

  • Foreach:遍历
  • Where:筛选
  • Select:转换

6.3.11 使用ObjectDataProvider对象作为Binding的Source

ObjectDataProvider对应与被包装对象的关系:

一个例子:三个TextBox框,对上面两个框输入值之后,两个框的值的和会立马体现在第三个框上:

①定义一个Calculator类,内部有一个Add方法:

②使用XAML画出三个TextBox文本框:

③对三个TextBox文本框设置Binding:

前两个Binding的数据流向主要是从TextBox.Text流向Source,这里主要是注意Binding的UpdateSourceTrigger属性,这个属性是个枚举值,意思是数据更新到Source的时机,对于TextBox来说,默认是输入值到TextBox释放焦点的时候,但是通过为这个值设置成PropertyChanged之后,就是输入值那一刻就会更新Source,这样两个文本框的值的和就会立刻回显到第三个TextBox文本框中;第三个Binding的path是点(.),点的意思是Source本身就是数据

结果:

上面的前面两个Binding很明显,ObjectDataProvider看上去更像Binding的Target,因为数据总是从TextBox -> ObjectDataProvider,但是不将ObjectDataProvider设置为Binding的Target有两个原因:

  • ObjectDataProvider的MethodParameters不是依赖属性,不能作为Binding的目标
  • 数据驱动UI的理念要求尽可能地使用数据对象作为Binding的Source而把UI元素当作Binding的Target

6.3.12 使用Binding的RelativeSource:

有些时候我们不能确定作为Source的对象叫什么名字,但知道它与作为Binding目标的对象在UI布局上有相对关系,比如控件自己关联自己的某个数据、关联自己某级容器的数据。这时候我们就要使用Binding的RelativeSource属性

画一个被很多布局嵌套的TextBox,然后为这个TextBox的Text属性使用Binding语法赋值:

RelativeSource的构造函数接受一个RelativeSourceMode参数,这个参数定义Binding的target(在这是TextBox)和Binding的Source(这里是Grid)的相对位置,AncestorType说明Binding的是什么类型的控件,AncestorLevel属性指的是以Binding目标控件为起点的层级偏移量

使用代码做binding(or 上面直接在XAML做Binding):

如果想Binding自身属性,可以使用RelativeSourceMode.Self

6.4 Binding对数据的转换与校验

6.4.1 Binding的数据校验

Binding的ValidationRules属性是Collection<ValidationRule>,它可以为每个Binding设置多个数据校验条件,每个条件是一个ValidationRule类型对象。ValidationRule类是个抽象类,在使用的时候我们需要创建它的派生类并实现它的Validate方法。Validate方法的返回值是ValidationResult类型对象,如果校验通过,就把ValidationResult对象的IsValid属性设为true,反之,需要把IsValid属性设为false并为其ErrorContent属性设置一个合适的消息内容(一般是个字符串)

例子:

①实现一个ValidationRule的派生类:

②界面两个控件,一个TextBox一个Slider:

③将Slider的Value值Binding到TextBox的Text属性,并将RangeValidationRule加入到Binding.ValidationRules中:

④运行程序:

当输入1000后,文本框显示红色边框,提醒用户输入有误。通过调试,发现这个ValidationRule是从target -> source的一个校验,当然,ValidationRule类的Summary也是这么说的:User input

6.4.2 Binding的数据转换

当某些数据在从Binding的Target到Source或Source到Target数据类型不一致并且wpf类库不能做到自动帮我们转型时,那么我们需要手动做转型,这就会用到一个接口IValueConverter,里面有两个方法,一个是Convert,一个是ConvertBack,从Source到Target是实现Convert方法,从Target到Source是实现ConvertBack方法

例子:在一个列表中向用户显示飞机的状态:

①定义数据类型:

②加入两个飞机图片资源

③实现转换接口的派生类:

④界面:

⑤后台代码:

⑥运行程序:

6.5 MultiBinding(多路Binding)

例子:比较常见的一个场景,就是有些Button需要我们输入多个文本框且文本框的内容相同时Button才可点

①需要一个转换器,将输入的多个结果转换为一个bool值:

4个TextBox,一个Button,当第一个的TextBox和第二个的Text并且第三个TextBox和第四个的Text一样时,Button可点,否则置灰状态(Button.IsChecked=false)

②界面:

③代码:

④运行程序:

7.深入浅出属性

7.2 依赖属性

依赖属性就是一种可以自己没有值,并能通过使用Binding从数据源获得值(依赖在别人身上)的属性

拥有依赖属性的对象被称为依赖对象

依赖属性的好处:

  • 节省实例对内存的开销
  • 属性值可以通过Binding依赖在其他对象上

7.2.1 依赖对内存的使用方式

传统的.NET开发中,一个对象所占用的内存空间在调用new操作符进行实例化的时候就已经决定了,而WPF允许对象在被创建的时候并不包含用于存储数据的空间(即字段所占用的空间)、只保留在需要用到数据时能够获得默认值、借用其他对象数据或实时分配空间的能力——这种对象就称为依赖对象(Dependency Object)而它这种实时获取数据的能力则依靠依赖属性(Dependency Property)来实现。WPF开发中,必须使用依赖对象作为依赖属性的宿主,使二者结合起来,才能形成完整的Binding目标被数据所驱动

在WPF系统中,依赖对象的概念被DepencyObject类实现(控件都隐式实现),依赖属性则由DepencyProperty类所实现

7.2  依赖属性

DependencyProperty必须以DependencyObject为宿主、借助它的SetValue和GetValue方法进行写入与读取,因此想要自定义DependencyProperty,宿主一定是DependencyObject的派生类

例子:

修饰符的约定是public static readonly

获取实例是使用DependencyProperty.Register方法,方法的第一个参数是来指明以哪个CLR属性作为这个依赖属性的包装器

包装器(CLR属性)的作用:

以实例属性的形式向外界暴露依赖属性,这样一个依赖属性才能成为数据源的一个Path

使用一个包装器(CLR属性)去包装NameProperty:

这就是为什么我们在Binding的时候:

被要求target是DependencyObject的,属性是DenpendencyProperty,而我们在XAML中又使用的是CLR属性,因为CLR属性就是对依赖属性做了一层封装

使用propdp + 2次tab可以快速声明一个依赖属性

7.2.3 依赖属性值存取的秘密

在程序中为一个依赖属性获取实例是使用的DenpendencyProperty.Register方法,那么可以从这个Register方法内部了解到更多:

public static DependencyProperty Register(string name, Type propertyType, Type ownerType)
{
    return RegisterCommon(name, propertyType, ownerType);
}

private static DependencyProperty RegisterCommon(string name, Type propertyType, Type ownerType)
{
    FromNameKey key = new FromNameKey(name, ownerType);
    //FromNameKey的构造器
    //public FromNameKey(string name,Type ownerType)
    //{
    //    _name = name;
    //    _ownerType = ownerType;
    //    _hashCode = _name.GetHashCode() ^ _ownerType.GetHashCode();
    //}
    if (PropertyFromName.Contains(key))
    {
        //throw new ....
    }
    DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultMetadata, validateValueCallback);
    PropertyFromName[key] = dp;
}
public override int GetHashCode()
{
    return _hashCode;
}

 一句话概括DependencyProperty对象的创建与注册,那就是:创建一个DependencyProperty实例并用它的CLR属性名和宿主类型名生成hash code,最后把hashcode和DependencyProperty实例作为Key-Value对存入全局的、名为PropertyFromName的Hashtable中。这样,WFP属性系统通过CLR属性名和宿主类型名就可以从这个全局的Hashtable中检索出对应的DependencyProperty实例

可以通过DependencyObject.GetValue来了解一下依赖属性的作用:

public object GetValue(DependencyProperty dp)
{
    ......
    return GetValueEntry(LookupEntry(dp.GlobalIndex),dp,null,RequestFlags.FullyResolved)
            .value;
    //上面的展开:
    EntryIndex entryIndex = LookupEntry(dp.GlobalIndex);
    EfffectiveValueEntry valueEntry = GetValueEntry(entryIndex,dp,null....)
    return valueEntry.Value;
}

public EffectiveValyeEntry GetValueEntry(...)
{
    ...
    return EffectiveValueEntry[dp.GlobalIndex]; 
}

 在DependencyObject有这样一个成员变量:

private EffectiveValueEntry[] _effectiveValues;

这个数组就存的是每个依赖属性的值,然后取的话就通过DependencyProperty.GlobaIndex(本质是hash code,而hash code又由其CLR包装器名和宿主类型名共同决定)去数组中索引找到值

所以,依赖属性的static关键字所修饰的依赖属性对象其作用是用来检索真正的属性值,而不是用来存储值,然后为了保证GlobalIndex属性值的稳定性,又对依赖属性使用了readonly修饰符(想一想,如果不加readonly,那么就可以在程序中随便修改某个依赖属性的实例,那么它的GlobalIndex就会改变,GlobalIndex改变,hashcode就会改变,随之后续EffectiveValueEntry[]对于同一个依赖属性就会有多个实例)

DependencyObject.SetValue基本上就是往DependencyObject._effectiveValues数组中写数据了

WPF利用依赖属性其实是一种空间换时间的做法,虽然减少属性消耗的内存,但增加了属性存值取值时的算法时间,而本书作者也说了WPF在性能上不尽人意,微软也在不断完善这个机制

7.3 附加属性

附加属性是说一个属性本来不属于某个对象,但由于某种需求而被后来附加上。也就是把对象放入一个特定环境后对象才具有的属性(表现出来就是被环境赋予的属性)就称为附加属性(Attached Properties)

附加属性的作用就是将属性与数据类型(宿主)解耦

附加属性的本质是依赖属性

快速创建一个附加属性:propa+2次tab

附加属性的例子:

可以看到,附件属性在代码中的写法和依赖属性基本相同,不同点在于:

  • 附件属性是使用DependencyProperty.RegisterAttached注册
  • 附件属性的两个包装器是方法

因为附加属性也是DependencyProperty的一个实例,所以也可以作为Binding的对象,下面是用附加属性来做Binding的例子:

可以在XAML以Canvas.Top和Canvas.Left为Target做Binding

也可以用C#代码做Binding:

这种代码与在XAML中写起来同样奇怪,谁叫它是附加属性..

附加属性工作流程(我的理解):当我们在XAML像Canvas.Left=3这样复制之后,Canvas的实例(以上面的XAML为例就是包裹了两个Slider和Rectangle的Canvas)下的一个数组就会存一个3的值,而键就是附加到那个控件对象的hashcode值,这样Canvas就在布局的时候就可以通过这个数组以键和值的方式确定它所包含的所有控件的布局位置

8.深入浅出事件

事件系统在WPF中也有升级,进化为路由事件,并在其基础上衍生出命令传递机制

好处:使程序的设计和实现更加灵活,模块之间的耦合度也进一步降低

8.1近观WPF的树形结构

路由(Route):起点与终点间有若干个中转站,从起点出发后经过每个中转站时要做出选择,最终以正确(比如最短或者最快)的路径到达终点

(Windows本身就是一种消息驱动的操作系统)

WPF中有两种“树”:一种叫逻辑树(Logical Tree);一种叫可视元素树(Visual Tree):

  • Logical Tree最显著的特点就是它完全由布局组件和控件构成(包括列表类控件中的条目元素)
  • Visual Tree:每个WPF控件本身也是由更细微级别的组件(它们不是控件,而是一些可视化组件,派生自Visual类)组成的树

Template可以理解为控件的骨架,可以保证控件功能不丢失的情况下为控件换一副新骨架,让它更漂亮

8.2事件的来龙去脉

事件模型:

参考我的另一篇事件学习文章:C#事件_CookieYangK的博客-CSDN博客

在这种模型里,事件的响应者通过订阅关系直接关联在事件拥有者的事件上,为了与WPF的路由事件模型区分开,我把这种事件模型称为直接事件模型或者CLR事件模型。因为CLR事件本质上是一个用event关键字修饰的委托实例

8.3 深入浅出路由事件

8.3.1 使用WPF内置路由事件

①画一个多层布局,内部含有2个Button的界面:

②在代码中为gridRoot绑定Button.Click路由事件(或者在XAML中绑定,如上):

当像这样做了绑定之后,那么gridRoot布局内部的button的所有click事件都会被它接受并触发它的事件响应函数

8.3.2 自定义路由事件

创建自定义路由事件大体可分为三步:

  1. 声明注册路由事件
  2. 为路由事件添加CLR事件包装
  3. 创建可以激发路由事件的方法

ButtonBase类中Click事件的三个步骤的体现:

为路由事件添加CLR事件包装是为了把路由事件暴露得像一个传统的直接事件,所以平时在使用路由事件的时候同样可以使用与直接事件相同的语法+=与-=

自定义一个可以报告事件的Button路由事件:

①继承Button类,创建一个ReportTime事件:

开始没有明白为什么点击Button就会触发ReportTime事件,其实应该就是点击Button之后会去调用OnClick方法,方法内部定义一些事件调用即可

②界面:

③事件响应方法:

④效果:

另外可以看到,当事件路由到grid_2这个对象之后就没有再继续往上走了,这是因为我在③中的事件响应方法中做了判断,如果事件通知到grid_2,那么e.Handled = true,则事件通知停止

9.深入浅出命令

9.1 命令系统的基本元素与关系

 9.1.1 命令系统的基本元素

WPF的命令系统由几个基本要素构成,它们是:

  • 命令(Command):WPF的命令实际上就是实现了ICommand接口的类,平时使用最多的是RoutedCommand类。我们还会学习使用自定义命令
  • 命令源(Command Source):即命令的发送者,是实现了ICommandSource接口的类。很多界面元素都实现了这个接口,其中包括Button、MenuItem、ListBoxItem等
  • 命令目标(Command Target):即命令将发送给谁,或者说命令将作用在谁身上。命令目标必须是实现了IInputElement接口的类
  • 命令关联(Command Binding):负责把一些外围逻辑与命令关联起来,比如执行之前对命令是否可以执行进行判断、命令执行之后还有哪些后续工作等

命令基本元素的关系图

暂时跳过命令的学习,因为感觉之前的例子和场景都没有用到命令这个概念,所以先学习重要的知识点...

10 深入浅出资源

为了避免丢失或损坏,编译器允许我们把外部文件编译进程序主体、成为程序主体不可分割的一部分,这是传统的程序资源

每个界面元素都可以携带自己的资源并可被自己的子级元素共享,这是对象级资源

10.2 且"静"且"动"用资源

静态资源使用(StaticResource)指的是在程序载入内存时对资源的一次性使用,之后就不再去访问这个资源了

动态资源使用(DynamicResource)指的是在程序运行过程中仍然会去访问资源

所以对于StaticResource访问资源来说,后续即使资源被更新,使用者也不会同步;而使用DynamicResource访问资源,后续资源被更新,使用者也会感知资源更新并且同步更新

例子:

①定义两个资源,一个使用静态资源访问,一个使用动态资源访问

②Button.Click事件导致资源被更新:

③效果:

触发一次Click事件,DynamicResource访问的资源被同步更新,而StaticResource访问的资源保持不变

10.3 向程序添加二进制文件

如果要添加的资源是字符串而非文件,我们可以使用应用程序Properties名称空间中的Resources.resx资源文件

如图:

①向Resources.resx中添加一个Username-name的键值对:

注意要将访问权限设置为Public,否则访问该资源会抛出异常

③效果:

11 深入浅出模板

新一代的设计理念:模板

11.1 模板的内涵

在WPF中,通过引入模板微软将数据和算法的“内容”与“形式”解耦

  • 6
    点赞
  • 67
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
深入浅出WPF(Windows Presentation Foundation)是一本介绍WPF的书籍,适合初学者。本书共分为以下几章: 第一章:WPF简介 这一章主要介绍WPF的背景和优势,以及与传统的Windows Forms应用程序开发的对比。 第二章:XAML基础 XAML(可扩展应用程序标记语言)是WPF的核心,本章通过一些简单的示例介绍XAML的基本概念和语法。 第三章:WPF控件 WPF提供了丰富的控件,本章依次介绍了常用的Button、TextBox、ComboBox和ListBox等控件的用法和属性。 第四章:布局和容器 WPF的布局和容器可以帮助我们更好地管理和组织控件,本章详细介绍了Grid、StackPanel和WrapPanel等布局和容器的使用。 第五章:数据绑定 数据绑定是WPF的重要特性之一,可以实现数据和UI之间的自动同步,本章通过示例演示了常见的数据绑定方式。 第六章:样式和模板 WPF的样式和模板可以帮助我们更好地定制和美化应用程序的外观,本章介绍了如何定义和应用样式和模板。 第七章:命令和事件 WPF的命令和事件机制是实现交互的重要手段,本章介绍了如何定义和使用命令,以及如何处理事件。 第八章:动画和效果 WPF提供了强大的动画和效果功能,可以使应用程序更加生动和吸引人,本章介绍了常用的动画和效果的实现方式。 第九章:MVVM架构 MVVM(Model-View-ViewModel)是一种经典的软件架构模式,在WPF开发中被广泛应用,本章介绍了MVVM的基本原理和实现方式。 第十章:高级主题 本章涵盖了一些高级的WPF主题,如自定义控件、多文档界面和异步编程等。 通过学习本书,读者可以全面了解WPF的基础知识和常用技术,能够使用WPF开发出功能强大、界面美观的应用程序。无论是从零开始学习WPF,还是希望系统地复习和巩固WPF知识的读者,本书都是一本不可或缺的参考资料。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值