DeepInWPF

深入浅出WPF知识点小结


在XAML中为对象属性赋值的三种形式

  1. Attribute=Value形式
  2. 属性标签
  3. 标签扩展

TypeConverter

其实wpf中xaml窗口中的属性定义的Value很多都是字符串,但是仍然可以转换为目标对象类型。这就和TypeConverter脱不了干系。我们自己实现一次对此有更深入的理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void button_Click(object sender, RoutedEventArgs e)
{
Human h = (Human)this.FindResource("human");
MessageBox.Show(h.Child.Name);
}
}
[TypeConverterAttribute(typeof(StringToHumanTypeConverter))]
public class Human
{
public string Name { get; set; }
public Human Child { get; set; }
}
public class StringToHumanTypeConverter:TypeConverter
{
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string )
{
Human h = new Human();
h.Name = value as string;
return h;
}
return base.ConvertFrom(context, culture, value);
}
}
1
2
3
<Window.Resources>
<local:Human x:Key="human" Child="ABC"/>
</Window.Resources>

属性标签

所谓属性标签,居然就是简单的.+扩展的属性.

1
2
3
4
5
<Button Width="30" Height="30" Button.Click="ButtonClick" >
<Button.Content>
<Rectangle Width="30" Height="30" Stroke="Black" Fill="red"/>
</Button.Content>
</Button>

其中Button.Content即为属性标签的用法。

xmal是生命性语言,也就是说声明Button.Content属性对象中要增加的东西。

下面看LinearGradientBrush的完全体,这个纯属个人需求,读者可以掠过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Rectangle x:Name="rectangle" Width="200" Height="120">
<Rectangle.Fill>
<LinearGradientBrush>
<LinearGradientBrush.StartPoint>
<Point X="0" Y="0"></Point>
</LinearGradientBrush.StartPoint>
<LinearGradientBrush.EndPoint>
<Point X="1" Y="1"/>
</LinearGradientBrush.EndPoint>
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0.2" Color="LightBlue"/>
<GradientStop Offset="0.5" Color="Blue"/>
<GradientStop Offset="0.8" Color="DarkBlue"/>
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>

标签扩展

就是花括号的形式。。。

1
2
3
4
5
6
7
<Window.Resources>
<sys:String x:Key="stringHello">Hello Her!</sys:String>
</Window.Resources>
<Grid>
<TextBlock Height="24" Width="120" Background="LightBlue"
Text="{StaticResource ResourceKey=stringHello}"/>
</Grid>
  • {StaticResource}
  • {DynamicResource}
  • {Binding }

内容分布

  1. 事件处理器与代码后置
  2. 导入程序集和引用其中的命名空间
  3. XAML的注释

事件处理器与代码后置

事件响应者:事件处理器->订阅事件->事件的拥有者:事件。

可以用简单的Button Click事件来理解。除了xaml方式添加事件订阅 ,也可以用C#代码的方式。

1
this.button.Click += Button_Click1;

导入程序集和引用其中的命名空间

同一sln中引用自己开发的自定义控件时,也需要先添加引用add Reference.

然后在要用的地方,如Window 中

xmlns:controls="clr-namespace:HerLib;assembly=HerLib"

引用其他公司的程序集,也是先添加引用。

然后如果有https方式就用,没有就照常引用。

XAML的注释

和C#注释一样,两组快捷键Ctrl+EC EU Ctrl+KC KU


厉害的x名称空间

  1. x名称空间的由来和作用
  2. x名称空间里都有什么
  3. x:Class
  4. x:ClassModifier
  5. x:Name
  6. x:FieldModifier

名称空间的由来和作用

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

专门用来解析和分析xaml文档的命名空间,大概可以理解为包含了很多”using”.就是“XAML名称空间”

x名称空间里都有什么

Tools:

  • x:Array
  • x:Class
  • x:ClassModifier
  • x:Code
  • x:FieldModifier
  • x:Key
  • x:Name
  • x:Null
  • x:Shared
  • x:Static
  • x:Subclass
  • x:Type
  • x:TypeArguments
  • x:Uid
  • x:XData

x:Class

x:Class="V3.引用"

实际也是声明了一个partial class。即WPF设计是想要把页面设置和后台代码分开。XAML里的partial class专注于设计,代码的专注于逻辑。

x:ClassModifier

<Window x:Class="V3.引用" x:ClassModifier="internal"...>

只是改了xaml还不够,上面可以知道还得改C#

internal partial class 引用 : Window

x:Name

为对象准备一个引用变量方便在C#中直接访问。类似搞个指针?

对应到Unity,应该就是public一个变量命名并且拖过来。

声明一个引用变量,并且检查改对象是否有Name属性,没有就给他赋值,把name注册到WPF树里。

对于派生自FrameworkElement类的元素,都是有Name属性的,对于他们Name 和x:Name是同样的。但是其他就不同了

x:FieldModifier

1
2
3
<Grid>
<TextBox x:FieldModifier="private"></TextBox>
</Grid>

意思说xaml 的partial 类里定义的基本都是public ,这个x:FieldModifier就是给它设置权限的。


控件与布局

常用控件:

  1. 布局控件: Grid StackPanel
  2. 内容控件: Window Button
  3. 带标题内容控件:GroupBox TabItem
  4. 条目控件:ListBox ComboBox
  5. 带标题条目控件:TreeViewItem MenuItem
  6. 特殊内容控件:Image TextBox

Binding

Data Binding在WPF中的地位

它说程序的本质是数据加算法。数据在存储/逻辑/展示3个层流通。但是算法一般分布在这几处:

  1. 数据库内部
  2. 读取与写回数据
  3. 业务逻辑
  4. 数据展示
  5. 界面与逻辑的交互

理解WPF的数据驱动UI

Binding通过Path来指定关心的哪个属性,当值变化后属性还要有通知Binding的能力:一般在Set中激发PropertyChanged事件。实现System.ComponentModel名称空间中的InotifyPropertyChanged接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public partial class BindingBasic : Window
{
Student stu;
public BindingBasic()
{
InitializeComponent();
stu = new Student();
Binding binding = new Binding();
binding.Source = stu;
binding.Path = new PropertyPath("Name");
BindingOperations.SetBinding(this.textBox, TextBox.TextProperty, binding);
}

private void ButtonClick(object sender, RoutedEventArgs e)
{
stu.Name += "Name";
}
}
public class Student:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string name;
public string Name
{
get { return name; }
set
{
name = value;
if (this.PropertyChanged!=null)
{
PropertyChanged.Invoke(this,new PropertyChangedEventArgs("Name"));
}
}
}
}

BindingOperations.SetBinding(this.textBox, TextBox.TextProperty, binding);

public class Student:INotifyPropertyChanged

比较重要的就是上面两条。

实际工作中有简化。因为FrameworkElement对BindingOperations.SetBinding()方法进行了封装。

1
2
3
4
public BindingExpressionBase SetBinding(DependencyProperty dp,BindingBase binding)
{
return BindingOperations.SetBinding(this,dp,binding);
}

有经验就继续

1
2
3
4
5
public Window1()
{
InitializeComponent();
this.textBox.SetBinding(TextBox.TextProperty,new Binding("Name"){Source=stu=new Student()});
}

控制Binding的方向及数据更新

控制Binding数据流向的属性是Mode,它的类型是BindingMode枚举。取值为TwoWay,OneWay,OnTime,OneWayToSource,Default.

控制更新的属性UpdateSourceTrigger.枚举为PropertyChanged,LostFocus,Explicit,Default.

没有Path的binding

如果Binding源本身就是数据且不需要path来指明,如string,int等基本类型。那么就可以把Path设定为. xaml中可以不写. C#中必须写. 0

Text="{Binding Source={StaticResource ResourceKey=myString}}"

Text="{Binding ., Source={StaticResource ResourceKey=myString}}

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

常见的办法有

  • 把普通CLR单个对象指定为Source
  • 把普通CLR集合类型对象指定为Source,包括数组、List、ObservableCollection<> 。
  • ADO.NET数据对象指定为Source:DataTable和DataView等对象
  • 用XmlDataProvider把XML数据指定为Source :
  • 把依赖对象(Dependency Object)指定为Source:即可以成目标也可以成源。可以形成Binding链。
  • 把容器的DataContext指定为Source,设置Path而不设置Source,让Binding自己去找Source。
  • 通过ElementName指定Source:
  • 通过Binding的RelativeSource属性相对地指定Source:控件关注自己的和内部元素的某个值。
  • 把ObjectDataProvider对象指定为Source:
  • 把LINQ检索得到的数据对象作为Bingding的源。

没有Source的Binding__使用DataContext作为Binding源

1
2
3
4
5
6
public class Student
{
public int Id{get;set;}
public String Name{get;set;}
public int Age{get;set;}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<StackPanel Background="LightBlue">
<StackPanel.DataContext>
<local:Student Id="6" Age="29" Name="Tim"></local:Student>
</StackPanel.DataContext>
<Grid>
<StackPanel>
<TextBox Text="{Binding Path=Id}"></TextBox>
<TextBox Text="{Binding Path=Name}"></TextBox>
<TextBox Text="{Binding Path=Age}"></TextBox>
<TextBox Text="{Binding Id}"></TextBox>
<TextBox Text="{Binding Name}"></TextBox>
<TextBox Text="{Binding Age}"></TextBox>
</StackPanel>
</Grid>
</StackPanel>

结合前面的Source本身为数据时,path可以写成”.”。

1
2
3
4
5
6
7
8
9
10
<StackPanel Background="LightBlue">
<StackPanel.DataContext>
<sys:String>Hello HerWorld!</sys:String>
</StackPanel.DataContext>
<Grid>
<StackPanel>
<TextBox Text="{Binding .}" Margin="5"></TextBox>
</StackPanel>
</Grid>
</StackPanel>

因为DataContext是依赖属性,当你没有为控件的某个依赖属性赋值时,该控件会把自己容器的属性值“借过来”当作自己的,也就是属性值可以沿某些UI元素树向下传递。

实际工作用法

  1. 当UI上的多个控件都使用Binding关注同一个对象时
  2. 当作为Source的对象不能被直接访问的时候–

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

1
2
3
4
5
6
<StackPanel x:Name="StackPanel" Background="LightBlue">
<TextBlock Text="Student ID:" FontWeight="Bold" Margin="5"></TextBlock>
<TextBox x:Name="TextBlock" Margin="5"></TextBox>
<TextBlock Text="Student List" FontWeight="Bold" Margin="5"></TextBlock>
<ListBox x:Name="ListBoxStudent" Height="110" Margin="5"></ListBox>
</StackPanel>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public List()
{
InitializeComponent();
List<Student> stuList = new List<Student>()
{
new Student(){Id=0,Name = "Tim",Age=29},
new Student(){Id=1,Name ="Tom",Age=28 },
new Student(){Id=2,Name = "Kyle",Age=27},
new Student(){Id=3,Name = "Tony",Age=26},
new Student(){Id=4,Name = "Vina",Age=25},
new Student(){Id=5,Name = "Mike",Age=24},
};
ListBoxStudent.ItemsSource = stuList;
ListBoxStudent.DisplayMemberPath = "Name";

Binding binding = new Binding("SelectedItem.Id"){Source=this.ListBoxStudent};
TextBlock.SetBinding(TextBox.TextProperty, binding);
}

this.ListBoxStudent.DisplayMemberPath="Name"赋值后,ListBox在获得ItemsSource的时候就会创建等量的ListBoxItem 并且以DisplayMemberPath的属性值Path创建Binding,Binding的目标是ListBoxItem的内容插件

这个创建Binding的过程是在DisplayerMemberTemplateSelector类的SelectTemplate方法里完成的。这个方法的定义格式如下

public override DataTemplate SelectTemplate(object item,DependencyObject container){}

1
2
3
4
Binding binding = new Binding();
binding.Path = new PropertyPath(_displayMemberPath);
bingding.StringFormat=_stringFormat;
text.SetBinding(TextBlock.TextProperty,binding);

显式设置DataTemplate的例子。

1
2
3
4
5
6
7
8
9
10
11
<ListBox x:Name="ListBoxStudent" Height="110" Margin="5">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Id}" Width="30"/>
<TextBlock Text="{Binding Path=Name}" Width="60"/>
<TextBlock Text="{Binding Path=Age}" Width="30"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

Note:使用集合时一般考虑用ObservableCollection<>,它实现了InotifyCollectionChanged和INotifyPropertyChange接口,能把集合的变化立即通知显示它的列表控件。

使用ADO.NET对象作为Binding源

DataTable。。。Skip


使用XML数据作为Binding的源

.NET Framework提供了两套处理XML数据的类库:

  • 符合DOM(Document Object Modle,文档对象模型)标准的类库:包括XmlDocument、XmlElement、XmlAttribute等类。中规中矩。
  • 以LINQ(Language-Intergrated Query,语言集成查询)为基础的类库:包括XDocument、XElement、XNode、XAttribute等类。方便

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

1
2
3
4
5
6
7
8
9
10
11
12
private void ButtonClick(object sender, RoutedEventArgs e)
{
XmlDocument doc= new XmlDocument();
doc.Load(@"C:\C\xml\RawData.xml");
XmlDataProvider xdp = new XmlDataProvider();
xdp.Document = doc;
//XmlDataProvider xdp1= new XmlDataProvider();
//xdp1.Source= new Uri(@"C:\C\xml\RawData.xml");
xdp.XPath = @"/StudentList/Student";
this.listViewStudents.DataContext = xdp;
listViewStudents.SetBinding(ListView.ItemsSourceProperty, new Binding());
}

使用@的是元素的Attribute,不加的是子集元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<Window.Resources>
<XmlDataProvider x:Key="xdp" XPath="FileSystem/Folder">
<x:XData>
<FileSystem xmlns="">
<Folder Name="Books">
<Folder Name="Programing">
<Folder Name="Windows">
<Folder Name="WPF"></Folder>
<Folder Name="MFC"/>
<Folder Name="Delphi"/>
</Folder>
</Folder>
<Folder Name="Tools">
<Folder Name="Development"/>
<Folder Name="Designment"/>
<Folder Name="Players"/>
</Folder>
</Folder>
</FileSystem>
</x:XData>
</XmlDataProvider>

</Window.Resources>
<Grid>
<TreeView ItemsSource="{Binding Source={StaticResource xdp}}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding XPath=Folder}">
<TextBlock Text="{Binding XPath=@Name}"></TextBlock>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>

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

1
2
3
4
5
6
7
8
9
<ListView x:Name="listViewStudents" Height="143" Margin="3">
<ListView.View>
<GridView>
<GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding Id}"></GridViewColumn>
<GridViewColumn Header="Name" Width="100" DisplayMemberBinding="{Binding Name}"></GridViewColumn>
<GridViewColumn Header="Age" Width="80" DisplayMemberBinding="{Binding Age}"></GridViewColumn>
</GridView>
</ListView.View>
</ListView>
1
2
3
4
5
6
7
8
9
10
11
12
13
private void ButtonClick(object sender, RoutedEventArgs e)
{
List<Student> stuList = new List<Student>()
{
new Student(){Id=0,Name = "Tim",Age=29},
new Student(){Id=1,Name ="Tom",Age=28 },
new Student(){Id=2,Name = "Kyle",Age=27},
new Student(){Id=3,Name = "Tony",Age=26},
new Student(){Id=4,Name = "Vina",Age=25},
new Student(){Id=5,Name = "Mike",Age=24},
};
this.listViewStudents.ItemsSource = from stu in stuList where stu.Name.StartsWith("T") select stu;
}

如果是xml

1
2
3
4
5
6
7
8
9
XDocument xdoc= XDocument.Load(@"C:\C\xml\RawData01.xml");
listViewStudents.ItemsSource = from element in xdoc.Descendants("Student")
where element.Attribute("Name").Value.StartsWith("T")
select new Student()
{
Id = int.Parse(element.Attribute("Id").Value),
Name = element.Attribute("Name").Value,
Age = int.Parse(element.Attribute("Age").Value)
};

使用ObjectDataProvider对象作为Source

1
2
3
4
5
<StackPanel Background="Gray">
<TextBox x:Name="textBoxArg1" Margin="5"></TextBox>
<TextBox x:Name="textBoxArg2" Margin="5"></TextBox>
<TextBox x:Name="textBoxResult" Margin="5"></TextBox>
</StackPanel>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public ODPBook()
{
InitializeComponent();
SetBinding();
}

private void SetBinding()
{
ObjectDataProvider odp= new ObjectDataProvider();
odp.ObjectInstance= new Calculator();
odp.MethodName = "Add";
odp.MethodParameters.Add("0");
odp.MethodParameters.Add("0");

Binding bindingToArg1 = new Binding("MethodParameters[0]")
{
Source = odp,
BindsDirectlyToSource = true,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};

Binding bindingToArg2 = new Binding("MethodParameters[1]")
{
Source = odp,
BindsDirectlyToSource = true,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};

Binding bindingToResult = new Binding("."){Source = odp};

textBoxArg1.SetBinding(TextBox.TextProperty, bindingToArg1);
textBoxArg2.SetBinding(TextBox.TextProperty, bindingToArg2);
textBoxResult.SetBinding(TextBox.TextProperty, bindingToResult);
}

Something Skip

6.3.12 使用Binding的RelativeSource

1
2
3
4
5
6
7
8
9
<Grid x:Name="g1" Background="Red" Margin="10">
<DockPanel x:Name="d1" Background="Orange" Margin="10">
<Grid x:Name="g2" Background="Yellow" Margin="10">
<DockPanel x:Name="d2" Background="LawnGreen" Margin="10">
<TextBox x:Name="TextBox" FontSize="24" Margin="10"></TextBox>
</DockPanel>
</Grid>
</DockPanel>
</Grid>

把TextBox的Text属性关联到外层容器的Name属性上。

1
2
3
4
5
6
7
8
9
public RelativeSourceHer()
{
InitializeComponent();
RelativeSource rs = new RelativeSource(RelativeSourceMode.FindAncestor);
rs.AncestorLevel = 1;
rs.AncestorType = typeof(Grid);
Binding binding = new Binding("Name"){RelativeSource = rs};
TextBox.SetBinding(TextBox.TextProperty, binding);
}

或者XAML中插入等效代码

1
2
Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=
{x:Type Grid},AncestorLevel=1},Path=Name}"

其中AncestorLevel属性指的是以Binding目标控件为起点的层级偏移量–d2的偏移量是1,g2的偏移量是2,依次类推。AncestorType属性告诉Binding寻找哪个类型的对象作为自己的源,不是这个类型的对象会被跳过。同理修改

1
2
Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=
{x:Type DockPanel},AncestorLevel=2},Path=Name
1
Text="{Binding RelativeSource={RelativeSource Self},Path=Name}

RelativeSource类的Mode属性的类型是RelativeSourceMode枚举,它的取值有:PreviousData、TemplateParent、Self和FindAncestor。RelativeSource类还有3个静态属性PreviousData、TemplateParent、Self。实际上这三静态属性就是创建一个RelativeSource实例、把实例的Mode属性设置为相应的值,然后返回这个实例。

例如

1
2
3
4
5
6
7
8
9
public static RelativeSource Self
{
get
{
if(s_self==null)
{ s_self = new RelativeSource(RelativeSourceMode.Self);}
return s_self;
}
}

常用于 DataTemplate

  • PreviousData
  • TemplateParent
  • Self

Binding对数据的转换与校验

Source与Target之间的桥梁,可以设置安检。而且还可以提供变脸器(转换器Converter)让你通过。

Binding的数据校验

Binding的ValidationRules属性类型是Collection,从它的名称和数据类型可知可以设置多个数据校验条件,每个条件是一个ValidationRule类型对象。ValidationRule类是个抽象类,在使用的时候需要创建它的派生类并且实现它的Validate方法。该方法的返回值是ValidationResult类型对象,如果校验通过,就把ValidationResult对象的IsValid属性设为True,反之,则设置为false并为其ErrorContent属性设置一个合适的消息内容。

Slider:0~100

xaml

1
2
3
4
<StackPanel>
<TextBox x:Name="textBox" Margin="5"></TextBox>
<Slider x:Name="slider" Minimum="-10" Maximum="110" Margin="5" ></Slider>
</StackPanel>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public SliderCheck()
{
InitializeComponent();
Binding binding = new Binding("Value"){Source = this.slider};
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
RangeValidationRule rvr = new RangeValidationRule();
binding.ValidationRules.Add(rvr);
textBox.SetBinding(TextBox.TextProperty, binding);
}
public class RangeValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
double d = 0;
if (double.TryParse(value.ToString(), out d))
{
if (d >= 0 && d <= 100)
{
return new ValidationResult(true, null);
}
}
return new ValidationResult(false, "Validation Failed!");
}
}

Binding校验默认来自Source的数据总是正确的,例如在Target Slider里设置-10,上述方法不会报错。

所以当Source的数据有可能出现问题时,将校验条件的ValidatesOnTargetUpdated属性设置为True。

rvr.ValidatesOnTargetUpdated = true;

Route Event提示ToolTip失败提示

1
2
3
4
5
6
RangeValidationRule rvr = new RangeValidationRule();
rvr.ValidatesOnTargetUpdated = true;
binding.ValidationRules.Add(rvr);
binding.NotifyOnValidationError = true;
textBox.SetBinding(TextBox.TextProperty, binding);
textBox.AddHandler(Validation.ErrorEvent,new RoutedEventHandler(this.ValidationError));

事件处理器

1
2
3
4
5
6
7
void ValidationError(object sender, RoutedEventArgs e)
{
if (Validation.GetErrors(textBox).Count>0)
{
this.textBox.ToolTip = Validation.GetErrors(textBox)[0].ErrorContent.ToString();
}
}

Binding的数据转换

之前的Double和String 居然可以在C#这种强类型语言中转换自如。

那是因为Binding的另外一个机制 Data Convert,当Source端Path所关联的数据与Target端目标属性类型不一样的时候,可以添加转换器。上面提到的自动做了,但是有时需要自己做:

  • Source里数据是Y、N和X三个值,UI对应CheckBox控件,需要把这三个值映射为它的IsChecked属性。
  • 当TextBox里已经输入了文字时用于登陆的Button才出现,String类型与Visibility或者bool之间的转换
  • Source是Male或者Female(string或者枚举),UI上对应的是Image控件,需要转成Uri

自己动手写Converter,创建一个类实现IValueConverter接口。定义如下:

1
2
3
4
5
public interface IValueConverter
{
object Convert(object value,Type targetType,object parameter,CultureInfo culture);
object ConvertBack(object value,Type targetType,object parameter,CultureInfo culture);
}

数据从Source流向Target时,Convert被调用,反之调用ConvertBack。第二个参数可以命名为outputType,确定方法的返回类型,第三个参数把额外的信息传入方法,如果需要多个信息可以把信息放入一个集合。Binding的Mode属性 OneWay 和TwoWay 影响到ConvertBack是否会被调用。

Converter实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Window.Resources>
<local:CategoryToSourceConverter x:Key="cts"></local:CategoryToSourceConverter>
<local:StateToNullableBoolConverter x:Key="stnb"></local:StateToNullableBoolConverter>
</Window.Resources>
<StackPanel Background="LightBlue">
<ListBox x:Name="listBoxPlane" Height="160" Margin="5">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Width="20" Height="20" Source="{Binding Path=Category,Converter={StaticResource cts}}"/>
<TextBlock Text="{Binding Path=Name}" Width="60" Margin="80,0"/>
<CheckBox IsThreeState="True" IsChecked="{Binding Path=State,Converter={StaticResource stnb}}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button x:Name="buttonLoad" Content="Load" Height="25" Margin="5,0" Click="buttonLoad_Click"></Button>
<Button x:Name="buttonSave" Content="Save" Height="25" Margin="5,0" Click="buttonSave_Click"></Button>
</StackPanel>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
public partial class ConvertHer : Window
{
public ConvertHer()
{
InitializeComponent();
}

private void buttonLoad_Click(object sender, RoutedEventArgs e)
{
List<Plane> planeList = new List<Plane>()
{
new Plane(){Category =Category.Boomer,Name = "B-1",State = State.Unknown},
new Plane(){Category =Category.Boomer,Name = "B-2",State = State.Unknown},
new Plane(){Category =Category.Fighter,Name = "F-22",State = State.Unknown},
new Plane(){Category =Category.Fighter,Name = "Su-47",State = State.Unknown},
new Plane(){Category =Category.Boomer,Name = "B-52",State = State.Unknown},
new Plane(){Category =Category.Fighter,Name = "J-10",State = State.Unknown},
};
this.listBoxPlane.ItemsSource = planeList;
}

private void buttonSave_Click(object sender, RoutedEventArgs e)
{
StringBuilder sb = new StringBuilder();
foreach (Plane plane in listBoxPlane.Items)
{
sb.AppendLine(string.Format("Category={0},Name={1},State={2}", plane.Category, plane.Name,
plane.State));
}
File.WriteAllText(@"C:\C\xml\PlaneLIst.txt", sb.ToString());
}
}

public class CategoryToSourceConverter : IValueConverter
{
public object Convert(object value, Type outputType, object parameter, CultureInfo culture)
{
Category c = (Category) value;
switch (c)
{
case Category.Boomer:
return @"\Icons\boomer.png";
case Category.Fighter:
return @"\Icons\fighter.png";
default:
return null;

}
}

public object ConvertBack(object value, Type outputType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

public class StateToNullableBoolConverter : IValueConverter
{
public object Convert(object value, Type outputType, object parameter, CultureInfo culture)
{
State s = (State) value;
switch (s)
{
case State.Locked:
return false;
case State.Available:
return true;
case State.Unknown:
default:
return null;

}
}

public object ConvertBack(object value, Type outputType, object parameter, CultureInfo culture)
{
bool? nb = (bool?) value;
switch (nb)
{
case true:
return State.Available;
case false:
return State.Locked;
case null:
return State.Unknown;
default:
return State.Unknown;
}
}

}

public enum Category
{
Boomer,
Fighter
}

public enum State
{
Available,
Locked,
Unknown
}

public class Plane
{
public Category Category { get; set; }
public string Name { get; set; }
public State State { get; set; }
}

MultiBinding

multiBinding与Binding一样均以BindingBase为基类,凡是能使用Binding对象的场合就可以使用MultiBinding。它具有一个名为Binding的属性,其类型是Collection.

考虑这么一个需求,有一个用于新用户注册的UI,包含四个TextBox和一个Button。

  • 第一 二个TextBox输入用户名,要求内容一致
  • 第三 四个TextBox输入E-Mail,要求内容一致
  • 上述符合时Button可用
1
2
3
4
5
6
7
8
<StackPanel Background="LightBlue">
<TextBox x:Name="textBox1" Height="23" Margin="5"/>
<TextBox x:Name="textBox2" Height="23" Margin="5,0"/>
<TextBox x:Name="textBox3" Height="23" Margin="5"/>
<TextBox x:Name="textBox4" Height="23" Margin="5,0"/>
<Button x:Name="button1" Content="Submit" Width="80" Margin="5"/>
<Button x:Name="button2" Content="Submit" Width="80" Margin="5"/>
</StackPanel>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public MultiBindingHer()
{
InitializeComponent();
this.SetMultiBinding();
}

void SetMultiBinding()
{
Binding b1= new Binding("Text"){Source = textBox1};
Binding b2 = new Binding("Text") { Source = textBox2 };
Binding b3 = new Binding("Text") { Source = textBox3 };
Binding b4 = new Binding("Text") { Source = textBox4 };

MultiBinding mb = new MultiBinding(){Mode=BindingMode.OneWay};
mb.Bindings.Add(b1);
mb.Bindings.Add(b2);
mb.Bindings.Add(b3);
mb.Bindings.Add(b4);
mb.Converter = new LogonMultiBindingConverter();
button1.SetBinding(Button.IsEnabledProperty,mb);
}

public class LogonMultiBindingConverter : IMultiValueConverter
{
public object Convert(object[] values, Type outputType, object parameter, CultureInfo culture)
{
if (!values.Cast<String>().Any(text=>string.IsNullOrEmpty(text))
&&values[0].ToString()==values[1].ToString()
&&values[2].ToString()==values[3].ToString())
{
return true;
}
return false;
}

public object[] ConvertBack(object value, Type[] outputTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}

}

总结:SetMultiBinding() 实现MutiBindingConverter()

Attributes 讲属性

7.1 属性的来龙去脉

C#规定 对类有意义的字段和方法使用static关键字修饰,称为静态成员,通过.访问

对类的实例有意义的字段和方法不加static,称为非静态成员或实例成员。

直接写字段暴露在外太不安全

1
public class Human{public int age;}

于是聪明的程序源改写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Human
{
private int age;

public void SetAge(int value)
{
if (value>=0 && value<=100)
{
age = value;
}
else
{
throw new OverflowException("Age overflow");
}
}
public int GetAge()
{
return age;
}
}

.NET Framework推出时,微软更进一步把 GEt/Set合并成了属性。简短了代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Human
{
private int age;
public int Age
{
get { return age; }
set
{
if (value>=0&&value<=100)
{
this.age = value;
}
else
{
throw new OverflowException("Age overflow");
}
}
}
}

.NET Framework推出时,微软更进一步把 GEt/Set合并成了属性。简短了代码,不失阅读性。这种属性又称为CLR属性(Common Language Runtime)。可以说CLR属性是private字段的安全访问包装,也可以说一个private字段在后台支持(back)一个CLR属性。

Note:CLR属性会占更多内存吗?属性的编译结果是两个方法!再多实例方法也只有一个Copy。

依赖属性

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

7.2.1 依赖属性对内存的使用方式

思考,TextBox有138个属性,假设每个CLR属性包装一个4字节的字段,如果程序运行创建了一个10列1000行的TextBox列表,那么将占用413810*1000=5.26M内存,怎么避免呢

用得着就带着,用不着就不带

WPF允许对象在被创建时不包含用于存储数据的空间(字段占用的空间)、只保留在需要用到数据时获取默认值、借用其他对象数据或实时分配空间的能力–这种对象就是依赖对象(Dependency Object),这种能力依靠依赖属性来实现。

依赖对象的概念被DependencyObject类实现。依赖属性的概念由DependencyProperty类实现。DependencyObject具有GetValue和SetValue两个方法。

DependencyProperty必须以DependencObject为宿主,因此,想自定义,宿主一定是DependencyObject的派生类。DependecyProperty实例的声明特点很鲜明–引用变量由public static readonly三个修饰符修饰,实例并非使用new操作符得到而是使用DependencyProperty.Register方法生成。代码:

1
2
3
4
5
public class Student:DependencyObject
{
public static readonly DependencyProperty NameProperty=
DependencyProperty.Register("Name",typeof(string),typeof(Student));
}

3个参数

  1. 此依赖属性back哪个CLR属性
  2. 此依赖属性存储什么类型的值,依赖属性的注册属性
  3. 依赖属性宿主类型,注册关联到哪个类型上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private Student stu;
public Dp()
{
InitializeComponent();
stu=new Student();
Binding binding= new Binding("Text"){Source = textBox1};
BindingOperations.SetBinding(stu, Student.NameProperty, binding);
}

private void ButtonClick(object sender, RoutedEventArgs e)
{
Student stu= new Student();
stu.SetValue(Student.NameProperty,textBox1.Text);
textBox2.Text = (string) stu.GetValue(Student.NameProperty);
}

public class Student : DependencyObject
{
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register("Name", typeof(string), typeof(Student));
}

private void ButtonClick1(object sender, RoutedEventArgs e)
{
MessageBox.Show(stu.GetValue(Student.NameProperty).ToString());
}

FrameworkElement类的SetBinding方法并不神秘,仅仅对BindingOperations的SetBinding方法做了一个简单的封装,代码如下:

1
2
3
4
5
6
7
public clas FrameworkElemnt:UIElement
{
public BindingExpressionBase SetBinding(DependencyProperty dp,BindingBase binding)
{
return BindingOperations.SetBinding(this,dp,binding);
}
}

Get,Set显得太过麻烦,于是添加一个CLR属性包装器:

1
2
3
4
5
6
7
8
9
10
11
public class Student : DependencyObject
{
public String Name
{
get { return (string) GetValue(NameProperty); }
set { SetValue(NameProperty,value);}
}
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register("Name", typeof(string), typeof(Student));

}

有了它就可以像普通属性一样访问依赖属性了

1
2
3
4
5
6
private void Button_Click(object sender,RoutedEventArgs e)
{
Student stu = new Student();
stu.Name=this.textBox1.Text;
this.textBox2.Text=stu.Name;
}

Note:propdp可以自动生成依赖属性

第四个参数:DefaultMetadata的作用是向依赖属性的调用者提供一些基本信息,包括:

  • CoerceValueCallback 依赖属性值被强制改变时此委托会被调用,此委托可以关联一个影响函数
  • DefaultValue:依赖属性未被显式赋值时,
  • IsSealed:控制PropertyMetadata属性值是否可以更改
  • PropertyChangeCallback:依赖属性值被改变之后此委托调用,可以关联一个影响函数

Note:依赖属性的DefaultMetadata只能通过Register方法的第四个参数进行赋值,而且一旦赋值就不能改变。如果想用新的替换,需要使用DependencyProperty.OverrideMetadata方法。

DependencyProperty被注册到了哪儿

private static Hashtable PropertyFromName= new Hashtable();

创建一个DependencyProperty实例并且用它的CLR属性名和宿主类型名生成hashcode,最后把hashcode和DependencyProperty实例作为Key—Value对存入全局的、名为PropertyFromName的Hashtable中。最后生成的DependencyProperty实例被当作返回值交还:

return dp;

并且DependencyProperty的GetHashCode方法被重写:

1
2
3
4
public override int GetHashCode()
{
return GlobalIndex;
}

WPF对依赖属性的读取优先级

  1. WPF属性系统强制值
  2. 由动画过程控制的值
  3. 本地变量值
  4. 上级元素Template设置的值
  5. 11
  6. 11
  7. 11
  8. 11
  9. 11

7.3 附加属性

附加属性的作用是让属性与数据类型解耦,让数据类型的设计更加灵活。

比如<Button Content="OK" Grid.Column="1" Grid.Row="1"

说事件

事件系统在WPF中升级为了Routed Event,衍生出了命令传递机制。降低了耦合度。

近观树形结构

Route :起点和终点间有若干个中转站,从起点出发后经过每个中转站时要做出选择,最终以正确的路径到达终点。

你可以把WPF的路由事件看成是一只小蚂蚁,它可以从树的基部向顶部(或者反向)目标爬行,每如果一个树枝的分岔点就会把消息带给这个分岔点。

WPF树:

Logical Tree:完全由布局组件和控件构成,查找用LogicalTreeHelper类的static方法,详见msdn

Visual Tree:如果把树叶在放大镜下看,也像树一样,控件本身也是由Visual类的更细微的可视化组件组成的树。用VisualTreeHelper类的static方法。

Routed Event是沿着Visual Tree传递的。

事件的来龙去脉

事件的前身是Message。Windows是消息驱动的操作系统,程序也遵照这个机制运行。消息本身就是一条数据,这条数据里面记载着消息的类别,必要的时候还记载一些消息参数。比如,当你在窗体上按下鼠标左键,一条WM_LBUTTONDOWN的消息就被生成并且加入到Windows待处理的消息队列中–大部分情况下WIndows的消息队列里不会有太多的消息在排队、消息会立刻被处理。如果你的计算机很慢并且很忙,那么这条消息就要等会才被处理,此为操作系统延迟。然后窗体用自己的算法进行处理。

此为消息出发算法逻辑的过程。

时间过去,微软把消息机制封装成了更容易让人理解的事件模型。3个关键点

  • 事件的拥有者:消息的发送者。事件的宿主可以在默写条件下激发它拥有哦的事件,即事件被触发。事件被触发则消息被发送。
  • 事件的响应者:即消息的接收者、处理者。使用事件处理器Event Handler对事件做出响应。
  • 事件的订阅关系:事件的拥有者可以随时激发事件,但是事件发生后会不会得到响应要看有没有事件的响应者,或者说要看这个事件是否被关注。如果对象A关注对象B的某个事件是否发生,则称A订阅了B的事件。更进一步讲,事件实际上是一个使用event 关键字修饰的委托Delegate类型成员变量,事件处理器则是一个函数。说A订阅了B的事件,本质上是让B.Event与A.EventHandler关联起来。所谓事件激发就是B.Event被调用,这时,与其关联的A.EventHandler就被调用。

001

这种传统的CLR事件模型,因为CLR事件本质上是一个用event关键字修饰的委托实例,我们暂且模仿CLR属性

的说法,把CLR事件定义为一个委托类型实例的包装器或者说有一个委托类型实例在backing一个CLR事件。

Windows Form项目中的一个button,可以认识事件模型的关键部分:

  • 事件的拥有者:myButton

  • 事件:myButton.Click

  • 事件的响应者:窗体本身

  • 事件处理器:this.myButton_Click

  • 订阅关系:

    this.myButton.Click+= new System.EventHandler(this.myButton_Click);

    如果实现如下方法:

    1
    2
    3
    4
    5
    6
    7
    private void myButton_Click(object sender,EventArgs e)
    {
    if(sender is Button)
    {
    MessageBox.Show((sender as Button).Name);
    }
    }

运行后会显示myButton。说明在CLR事件模型中,事件的拥有者就是消息的发送者sender。

弊端:

  • 每对消息是“发送-响应”关系,必须是建立显式的点对点订阅关系
  • 事件的宿主必须能够直接访问事件的响应者,不然无法建立订阅关系。

路由事件就很好的解决了上述问题。

路由事件Routed Event

直接事件激发:发送者直接将消息通过事件订阅交给事件响应者,事件响应者使用其事件处理器方法对事件的发生做出响应、驱动程序逻辑按客户需求运行。

路由事件激发:事件的拥有者和事件响应者之间没有直接显式的订阅关系,事件的拥有者只负责激发事件,事件的响应者则安装有事件侦听器,当它被单击后大喊一声我被单击了,这样一个Button.Click事件就开始在Visual Tree中传播,当事件经过某个节点时如果这个节点没有安装用于侦听Button.Click事件的耳朵,那么它会无视这个事件,让它继续传播,如果安装了,它的事件处理器就会被调用(任何一个传来的Button.Click事件都会被侦听到,例如在StackPanel上的Button.Click 能侦听其内所有button的事件),在事件处理器内程序员可以查看路由事件原始的出发点是哪个控件、上一站是哪里,还可以决定事件传递到此还是可以继续传递,像烽火台一样的传递策略。

传统的事件在WPF也是可以用的。

8.3.1 内置路由事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Grid x:Name="gridRoot" Background="Lime">
<Grid x:Name="gridA" Margin="10" Background="Blue">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Canvas x:Name="canvasLeft" Grid.Column="0" Background="Red" Margin="10">
<Button x:Name="buttonLeft" Content="Left" Width="40" Height="100" Margin="10"
></Button>
</Canvas>
<Canvas x:Name="canvasRight" Grid.Column="1" Background="Yellow" Margin="10">
<Button x:Name="buttonRight" Content="Right" Width="40" Height="100" Margin="10"></Button>
</Canvas>
</Grid>
</Grid>

单击buttonLeft时,事件沿着buttonLeft->canvasLeft->GridA->gridRoot->Window向上传递。

由于没有安装侦听器,所以无响应

1
2
3
4
5
public RoutedLogicalTree()
{
InitializeComponent();
this.gridRoot.AddHandler(Button.ClickEvent,new RoutedEventHandler(this.ButtonClicked));
}

可以如上添加。

AddHandler方法源自UIElement类,也就是说,所有UI控件都具有这个方法。AddHandler方法第一个参数是Button.ClickEvent而不是Button.Click。原来,WPF事件系统也使用了与属性系统类似的“静态字段->包装器(Wrapper)”的策略。

路由事件本身是一个RoutedEvent类型的静态成员变量(Button.ClickEvent),Button还有一个与之对应的Click事件(CLR包装)专门用于对外界暴露这个事件。

1
2
3
4
private void ButtonClicked(object sender, RoutedEventArgs e)
{
MessageBox.Show((e.OriginalSource as FrameworkElement).Name);
}

Note:因为路由事件的消息是从内部一层一层传递出来并且由gridRoot元素将事件消息交给ButtonClicked方法来处理,所以传入ButtonClicked方法的参数sender实际上是gridRoot而不是被单击的Button,解决方法是使用e.OriginalSource,再使用as/is操作符或者强制类型转换。

或者可以在grid上添加

1
2
3
<Grid x:Name="gridRoot" Background="Lime" Button.Click="ButtonClicked">
...
</Grid>

但是这里编辑器不会有提示,只能自己认真小心写啦

但是如果你是用ButtonBase.Click就可以有提示。 道理很简单,因为ClickEvent这个路由事件是ButtonBase类的静态成员变量(Button类继承获得它),而XAML编辑器只认得包含ClickEvent字段定义的类。

8.3.2 自定义Routed Event

步骤:

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

定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract clas ButtonBase:ContentControl,ICommandSource
{
//声明并注册路由事件
public static readonly RoutedEvent ClickEvent=/*注册路由事件*/
//为路由事件添加CLR事件包装器
public event RoutedEventHandler ClickEvent
{
add{this.AddHandler(ClickEvent,value);}
remove{this.RemoveHandler(ClickEvent,value);}
}
//激发路由事件的方法。此方法在用户单击鼠标时会被Windows系统调用
protected virtual void OnClick()
{
RoutedEventArgs newEvent = new RoutedEventArgs(ButtonBase.ClickEvent,this);
this.RaiseEvent(newEvent);
}
//...

}

CLR事件包装器让路由事件暴露得像一个传统的直接事件,可以用+= 和-=,代码格式换成了add和remove。

激发路由事件,首先创建需要让事件携带的消息(RoutedEventArgs类的实例)并且让它与路由事件关联,然后调用元素的RaiseEvent方法(继承自UIElement类)把事件发送出去。这与传统事件的方法不同,传统事件激发是通过调用CLR的Invoke方法实现的。

1
2
public static readonly RoutedEvent ClickEvent= EventManager.RegisterRoutedEvent
("Click",RoutingStrategy.Bubble,typeof(RoutedEventHandler),typeof(ButtonBase));

参数说明:

  1. string,路由事件的名称,尽量和RoutedEvent变量的前缀一致
  2. 路由策略
    • Buttle,冒泡式
    • Tunnel,隧道式
    • Direct,直达式,模仿CLR直接事件
  3. 指定事件处理器的类型
  4. 指定路由事件的宿主类型。

下面动手创建一个路由事件,包含事件发生的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

public class ReportTimeEventArgs : RoutedEventArgs
{
public ReportTimeEventArgs(RoutedEvent routedEvent, object source)
: base(routedEvent, source) { }
public DateTime ClickTime { get; set; }
}

public class TimeButton : Button
{
public static readonly RoutedEvent ReportTimeEvent = EventManager.RegisterRoutedEvent(
"ReportTime", RoutingStrategy.Bubble, typeof(EventHandler<ReportTimeEventArgs>), typeof(TimeButton));
//CLR事件包装器
public event RoutedEventHandler ReportTime
{
add { this.AddHandler(ReportTimeEvent, value); }
remove { this.RemoveHandler(ReportTimeEvent, value); }
}

protected override void OnClick()
{
base.OnClick();

ReportTimeEventArgs args = new ReportTimeEventArgs(ReportTimeEvent, this);
args.ClickTime = DateTime.Now;
this.RaiseEvent(args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  Title="HerCustomRoutedEvent" x:Name="window1" Height="350" Width="300"
local:TimeButton.ReportTime="ReportTimeHandler">
<Grid x:Name="gird_1" local:TimeButton.ReportTime="ReportTimeHandler" >
<Grid x:Name="grid_2" local:TimeButton.ReportTime="ReportTimeHandler">
<Grid x:Name="grid_3" local:TimeButton.ReportTime="ReportTimeHandler">
<StackPanel x:Name="stackPanel_1" local:TimeButton.ReportTime="ReportTimeHandler">
<ListBox x:Name="listBox"/>
<local:TimeButton x:Name="timeButton" Width="80" Height="80" Content="SeeTime"
local:TimeButton.ReportTime="ReportTimeHandler"></local:TimeButton>
</StackPanel>
</Grid>
</Grid>

</Grid>

方法

1
2
3
4
5
6
7
private void ReportTimeHandler(object sender, ReportTimeEventArgs e)
{
FrameworkElement element= sender as FrameworkElement;
string timeStr = e.ClickTime.ToLongTimeString();
string content = string.Format("{0} To {1} ", timeStr, element.Name);
this.listBox.Items.Add(content);
}

如果在方法中加入

1
2
3
4
if (element==this.grid_2)
{
e.Handled = true;
}

那么就会在grid_2断掉

Note:路由事件将程序中的组件进一步解耦

  • 很多类的事件都是路由事件,如TextBox类的TextChanged事件,Binding类的SourceUpdate事件等,所以在用到这些类时要发挥想象力
  • 也不要滥用路由事件,事件该结束就Handled了,不要惹麻烦

8.3.3 RoutedEventArgs的Source与OriginalSource

说路由事件在VisualTree上传递,本意是说”路由事件的消息在VisualTree上传递”,而路由事件的消息则包含在RoutedEventArgs实例中。

RoutedEventArgs两个属性

  • Source:LogicalTree上的消息源头
  • OriginalSource:VisualTree上的消息源头

先创建一个UserControl,

1
2
3
4
5
6
7
8
9
10
11
12
<UserControl x:Class="RoutedEventH.HerUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:RoutedEventH"
mc:Ignorable="d"
d:DesignHeight="90" d:DesignWidth="90">
<Border BorderBrush="Orange" BorderThickness="3" CornerRadius="5">
<Button x:Name="innerButton" Width="80" Height="80" Content="OK"></Button>
</Border>
</UserControl>

然后添加到主窗体中

1
2
3
<Grid>
<local:HerUserControl x:Name="herUserControl" Margin="10"></local:HerUserControl>
</Grid>

后台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public SourceAndOriginalSource()
{
InitializeComponent();
this.AddHandler(Button.ClickEvent,new RoutedEventHandler(this.Button_Click));
}

private void Button_Click(object sender, RoutedEventArgs e)
{
string strOriginSource = string.Format("VisualTree start point: {0}, type is {1} ",
(e.OriginalSource as FrameworkElement).Name, e.OriginalSource.GetType().Name);
string strSource = string.Format("LogicalTree start point:{0},tpye is {1} ",
(e.Source as FrameworkElement).Name, e.Source.GetType().Name);
MessageBox.Show(strOriginSource + "\r\n" + strSource);
}

8.3.4 附加事件Attached Event

哪些类有附加事件

  • Binding类:SourceUpdated事件、TargetUpdated事件
  • Mouse类:MouseEnter、MouseLeave、MouseDown、MouseUp
  • Keyboard类:KeyDown、KeyUp

对比一下那些拥有路由事件的类,如Button、Slider、TextBox…发现什么问题了吗

路由事件的宿主都是拥有可视化实体的界面元素

附加事件不具有显示在用户界面上的能力

设想实现:

设计一个Student类,如果Student实例的Name属性值发生了变化,就激发一个路由事件。

1
2
3
<Grid x:Name="gridMain" >
<Button x:Name="button1" Content="OK" Width="80" Height="80" Click="Button_Click"/>
</Grid>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public partial class AttachedEventH : Window
{
public AttachedEventH()
{
InitializeComponent();
//this.gridMain.AddHandler(
// Student.NameChangedEvent, new RoutedEventHandler(this.StudentNameChangeHandler));
Student.AddNameChangedHandler(
this.gridMain,
new RoutedEventHandler(this.StudentNameChangeHandler)
);
}


public class Student
{
public static readonly RoutedEvent NameChangedEvent = EventManager.RegisterRoutedEvent
("NameChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Student));

public static void AddNameChangedHandler(DependencyObject d, RoutedEventHandler h)
{
UIElement e = d as UIElement;
if (e!=null)
{
e.AddHandler(Student.NameChangedEvent, h);
}
}

public static void RemoveNameChangedHandler(DependencyObject d, RoutedEventHandler h)
{
UIElement e = d as UIElement;
if (e!=null)
{
e.RemoveHandler(Student.NameChangedEvent, h);
}
}

public int Id { get; set; }
public string Name { get; set; }
}

private void Button_Click(object sender, RoutedEventArgs e)
{
Student stu = new Student() {Id = 101, Name = "Tim"};
stu.Name = "Tom";
RoutedEventArgs arg = new RoutedEventArgs(Student.NameChangedEvent, stu);
this.button1.RaiseEvent(arg);
}

private void StudentNameChangeHandler(object sender, RoutedEventArgs e)
{
MessageBox.Show((e.OriginalSource as Student).Id.ToString());
}
}

自定义的类并非派生自UIElement,因此不具备AddHandler和RemoveHandler这两个方法,所以不能使用CLR属性作为包装器,规定:

  • 为目标UI元素添加附加事件侦听器的包装器是一个名为Add*Handler的public static 方法,星号代表事件名称。此方法接收两个参数,一个参数是事件的侦听者,类型为DependencyObject,另外一个是事件的处理器(RoutedEventHandler委托类型)
  • 解除UI元素对附加事件侦听的包装器是名为Remove*Handler的public static 方法。

Note:

第一,像Button.Click这些路由事件,因为事件的宿主是界面元素、本身就是UI树上的一个节点,所以路由事件路由时的第一站就是事件的激发者。附加事件宿主不是UIElement的派生类,所以不能出现在UI树上的节点,而且附加事件的激发是借助UI元素实现的,因此,附加事件路由的第一站是激发它的元素。

第二,实际上很少把附加事件定义在Student这种与业务逻辑相关的类中,一般都是定义在像Binding、Mouse、Keyboard这种全局的Helper类中。如果需要业务逻辑对象能发出路由事件怎么办?Binding,

如果程序架构设计得好,那么业务逻辑一定会使用Binding与元素关联,。。。。

命令 Command

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

9.1.1 命令系统的基本元素

要素:

  • Command: 实现了ICommand接口的类,使用多的是RoutedCommand类和自定义的
  • Command Source:命令的发送者,实现了ICommandSource接口的类,例如Button、MenuItem、ListBoxItem等
  • Command Target:命令目标必须实现IInputElement接口的类
  • Command Binding:判断和后续工作等

9.1.2 基本元素之间的关系

命令的使用:

  1. 创建命令类
  2. 声明命令实例
  3. 指定命令的源
  4. 指定命令目标
  5. 设置命令关联

命令目标如果被瞄上,会不停地发送可路由的Preview-CanExecute和CanExecute附加事件,命令目标的PreviewCanExecute、CanExecute、PreviewExecuted和Executed事件都是附加事件,Preview-CanExecute和CanExecute执行时机不由程序员控制,很容易出bug,务必小心。

9.1.3 小试命令

1
2
3
4
<StackPanel x:Name="stackPanel">
<Button x:Name="button1" Content="Send Command" Margin="5"/>
<TextBox x:Name="textBoxA" Margin="5,0" Height="100"/>
</StackPanel>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public partial class FirstCommand : Window
{
public FirstCommand()
{
InitializeComponent();
InitializeCommand();
}
//声明定义命令
private RoutedCommand clearCmd = new RoutedCommand("Clear", typeof(FirstCommand));

private void InitializeCommand()
{
//把命令赋值给命令源(发送者)并且指定快捷键
this.button1.Command = this.clearCmd;
this.clearCmd.InputGestures.Add(new KeyGesture(Key.C, ModifierKeys.Alt));

//指定命令目标
this.button1.CommandTarget = this.textBoxA;
//创建命令关联
CommandBinding cb = new CommandBinding();
cb.Command = this.clearCmd; //只关注与clearCmd相关的事件
cb.CanExecute += new CanExecuteRoutedEventHandler(cb_CanExecute);
cb.Executed += new ExecutedRoutedEventHandler(cb_Executed);

//把命令关联安置在外围控件上
this.stackPanel.CommandBindings.Add(cb);

}

//当探测命令是否可以执行时,此方法被调用
void cb_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
if (string.IsNullOrEmpty(this.textBoxA.Text))
{
e.CanExecute = false;
}
else
e.CanExecute = true;
//避免继续向上
e.Handled = true;
}
//当命令送达目标后,此方法被调用
void cb_Executed(object sender, ExecutedRoutedEventArgs e)
{
this.textBoxA.Clear();
//避免继续向上传
e.Handled = true;
}
}

Note:

  1. 使用命令可以少写些代码
  2. RoutedCommand是一个与业务逻辑无关的类,清空操作是CommandBinding做的
  3. CanExecute事件记得Handled
  4. CommandBinding设置在目标的外围控件上

9.1.4 WPF 命令库

  • ApplicationCommands
  • ComponentCommands
  • NavigationCommands
  • MediaCommands
  • EditingCommands

他们都是静态类,以单例模式暴露出来

9.1.5 命令参数

使用CommandParameter区别命令的对象

CommandParameter属性来自ICommandSource接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Title="命令参数" Height="240" Width="400" Background="LightBlue" WindowStyle="ToolWindow">
<Grid Margin="6">
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="4"/>
<RowDefinition Height="24"/>
<RowDefinition Height="4"/>
<RowDefinition Height="24"/>
<RowDefinition Height="4"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--命令和命令参数-->
<TextBlock Text="Name:" VerticalAlignment="Center" HorizontalAlignment="Left" Grid.Row="0"/>
<TextBox x:Name="nameTextBox" Margin="60,0,0,0" Grid.Row="0"/>
<Button Content="New Teacher" Command="New" CommandParameter="Teacher" Grid.Row="2"/>
<Button Content="New Student" Command="New" CommandParameter="Student" Grid.Row="4"/>
<ListBox x:Name="listBoxNewItems" Grid.Row="6"/>
</Grid>
<Window.CommandBindings>
<CommandBinding Command="New" CanExecute="New_CanExecute" Executed="New_Executed"/>
</Window.CommandBindings>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public partial class 命令参数 : Window
{
public 命令参数()
{
InitializeComponent();
}

private void New_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
if (string.IsNullOrEmpty(this.nameTextBox.Text))
{
e.CanExecute = false;
}
else
e.CanExecute = true;
}

private void New_Executed(object sender, ExecutedRoutedEventArgs e)
{
string name = this.nameTextBox.Text;
if (e.Parameter.ToString() == "Teacher")
this.listBoxNewItems.Items.Add(string.Format("New Teacher:{0}, 学而不厌。", name));
if (e.Parameter.ToString() == "Student")
this.listBoxNewItems.Items.Add(string.Format("New Student:{0}, 好好学习。", name));
}
}

9.1.6 命令与Binding的结合

???

9.2 近观命令

9.2.1 ICommand接口和RoutedCommand

ICommand Interface:包含两个方法和一个事件

  • Execute方法
  • CanExecute方法
  • CanExecuteChanged事件

RoutedCommand类与命令相关的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class RoutedCommand : ICommand
{
//由ICommand继承而来,仅供内部使用
private void ICommand.Execute(object parameter)
{
Execute(parameter, FilterInputElement(Keyboard.FocusedElement));
}
//新定义的方法,可由外部调用
public void Execute(object parameter, IInputelement target)
{
//命令目标为空,选定当前具有焦点的控件作为目标
if (target == null)
{
target = FilterInputElement(Keyboard.FocusedElement);
}
//真正执行命令的逻辑
ExcuteImpl(parameter, target, false);
}

private bool ExecuteImpl(object parameter, IInputElement target, bool userInitiated)
{
UIElement targetUIElement = target as UIElement;
ExecutedRoutedEventArgs args = new ExecutedRoutedEventArgs(this,parameter);

}
}

skip。。。。

ButtonBase的OnClick方法如下:

1
2
3
4
5
6
7
8
9
10
11
public class BattonBase:ContentControl,ICommandSource
{
//激发Click路由事件,然后发送命令
protected virtual void OnClick()
{
RoutedEventArgs newEvent = new RoutedEventArgs(ButtonBase.ClickEvent,this);
RaiseEvent(newEvent);
//调用内部类CommandHelpers的ExecuteCommandSource方法
MS.Internal.Commands.CommandHelpers.ExecuteCommandSource(this);
}
}

这节每看懂

9.2.2 自定义Command

1
2
3
4
5
6
7
<StackPanel>
<local:MyCommandSource x:Name="ctrlClear" Margin="10">
<TextBlock Text="Clear" FontSize="16" TextAlignment="Center" Background="LightGreen" Width="80"/>
</local:MyCommandSource>

<local:MiniView x:Name="miniView"/>
</StackPanel>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
namespace CommandHer
{
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

/// <summary>
/// CustomHerCommand.xaml 的交互逻辑
/// </summary>
public partial class CustomHerCommand : Window
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomHerCommand"/> class.
/// </summary>
public CustomHerCommand()
{
this.InitializeComponent();
ClearCommand clearCmd = new ClearCommand();
this.ctrlClear.Command = clearCmd;
this.ctrlClear.CommandTarget = this.miniView;
}
}

public class MyCommandSource : UserControl, ICommandSource
{
public ICommand Command { get; set; }
public object CommandParameter { get; set; }
public IInputElement CommandTarget { get; set; }
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{

base.OnMouseLeftButtonDown(e);
if (this.CommandTarget != null)
{
Console.WriteLine("LeftButtonDown______________");
this.Command.Execute(this.CommandTarget);
}
}
}

/// <summary>
/// The clear command.Custom command
/// </summary>
public class ClearCommand : ICommand
{
// 当命令可执行状态发生改变时,应当被激发

/// <summary>
/// The can execute changed.
/// </summary>
public event EventHandler CanExecuteChanged;

// 用于判断命令是否可以执行
public bool CanExecute(object parameter)
{
throw new NotImplementedException();
}

// 命令执行,带有与业务相关的Clear逻辑

/// <summary>
/// The execute.
/// </summary>
/// <param name="parameter">
/// The parameter.
/// </param>
public void Execute(object parameter)
{
IView view = parameter as IView;
Console.WriteLine("222");
if (view != null)
{
Console.WriteLine("333");
view.Clear();
}
}

public interface IView
{
bool IsChanged { get; set; }

void SetBinding();

void Refresh();

void Clear();

void Save();
}
}
}

自定义的组件

1
2
3
4
5
6
7
8
<Border CornerRadius="5" BorderBrush="LawnGreen" BorderThickness="2">
<StackPanel>
<TextBox x:Name="textBox1" Margin="5"/>
<TextBox x:Name="textBox2" Margin="5,0"/>
<TextBox x:Name="textBox3" Margin="5"/>
<TextBox x:Name="textBox4" Margin="5,0"/>
</StackPanel>
</Border>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MiniView.xaml.cs" company="Neuracle">
// sdf
// </copyright>
// <summary>
// MiniView.xaml 的交互逻辑
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace CommandHer
{
using System.Windows.Controls;
/// <summary>
/// MiniView.xaml 的交互逻辑
/// </summary>
public partial class MiniView : UserControl,ClearCommand.IView
{
public MiniView()
{
InitializeComponent();
}
public bool IsChanged { get; set; }
public void SetBinding() { }
public void Refresh() { }
public void Save() { }
public void Clear()
{
this.textBox1.Clear();
this.textBox2.Clear();
this.textBox3.Clear();
this.textBox4.Clear();
}
}
}

Resources

10.1 对象级资源的定义与查找

最简单的Resource

1
2
3
4
5
6
7
8
9
10
11
12
13
<Window.Resources>
<ResourceDictionary>
<sys:String x:Key="str">
Passion is Lip
</sys:String>
<sys:Double x:Key="dbl">3.1415926</sys:Double>
</ResourceDictionary>
</Window.Resources>
<Grid>
<StackPanel Background="AntiqueWhite">
<TextBlock Text="{StaticResource str}" Margin="5"/>
</StackPanel>
</Grid>

简写

1
2
3
4
5
6
7
8
<Window.Resources>
<sys:String x:Key="str">
Passion is LIp
</sys:String>
</Window.Resources>
<Grid>
<TextBlock Text="{StaticResource str}" Margin="5"/>
</Grid>

在检索资源时,先查找控件自己的Resources属性,如果没有会沿着逻辑树向上一直查找到最顶层容器,如果连最顶层容器都没有这个资源,程序就会去查找Application.Resources 程序的顶级资源。

代码中查找

1
2
3
4
5
private void Window_Loaded(object sender,RoutedEventArgs e)
{
string text=(string)this.FindResource("str");
this.textBlock1.Text=text;
}
1
2
3
4
5
private void Window_L(object sender,RoutedEventArgs e)
{
string text =(string)this.Resources["str"];
...
}

10.2 Static Dynamic Resources

Static:运行时不变用Static

Dynamic:运行时改变用Dynamic

1
2
3
4
5
6
7
8
9
<Window.Resources>
<TextBlock x:Key="res1" Text="Psmyfish"/>
<TextBlock x:Key="res2" Text="Psmyfish"/>
</Window.Resources>
<StackPanel>
<Button Margin="5,5,5,0" Content="{StaticResource res1}"/>
<Button Margin="5,5,5,0" Content="{DynamicResource res2}"/>
<Button Margin="5,5,5,0" Content="Update" Click="Button_Click"/>
</StackPanel>
1
2
3
4
5
private void Button_Click(object sender, RoutedEventArgs e)
{
this.Resources["res1"] = new TextBlock() { Text = "LoveLoda" };
this.Resources["res2"] = new TextBlock() { Text = "LoveLoda" };
}

##10.3 向程序添加二进制资源

资源词典里的资源:WPF资源 对象资源

程序内嵌资源:程序集资源 二进制资源

下面是添加Properties中的Resources.resx资源文件的,这个是方便本地化。记得把它的Access Modifier改成public

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<Window x:Class="ResourceHer.resx"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:prop="clr-namespace:ResourceHer.Properties"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ResourceHer"
mc:Ignorable="d"
Title="resx" Width="240" Height="120">
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="23"/>
<RowDefinition Height="4"/>
<RowDefinition Height="23"/>
</Grid.RowDefinitions>
<TextBlock Text="{x:Static prop:Resources.UserName}"/>
<TextBlock x:Name="textBlockPassword" Grid.Row="2"/>
<TextBox BorderBrush="Black" Grid.Column="2"/>
<TextBox BorderBrush="Red" Grid.Column="2" Grid.Row="2"/>

</Grid>
</Window>
1
2
3
4
5
public resx()
{
InitializeComponent();
this.textBlockPassword.Text = Properties.Resources.Password;
}

加其他资源记得改BuildAction属性为Resource

10.4 使用Pack URI路径访问二进制资源

格式

pack://application,,,[/程序集名称;][可选版本号;][文件夹名称/]文件名称

1
<Image x:Name="ImageBg" Source="Resources/Images/Rafale.jpg" Stretch="Fill"/>

or

1
2
3
<Image x:Name="ImageBg" 
Source="pack://application:,,,/Resources/Images/Rafale.jpg"
Stretch="Fill"/>

or

#
1
2
3
InitializeComponent();
Uri imgUri=new Uri(@"Resources/Images/Rafale.jpg",UriKind.Relative);
this.ImageBg.Source=new BitmapImage(imgUri);

or

1
2
Uri imgUri= new Uri
(@"pack://application:,,,/Resources/Images/Rafale.jpg",UriKind.Absolute);

11 Template

11.1 模板的内涵

引入模板,微软将数据和算法的“内容”与“形式”解耦了。

Template:

  • ControlTemplate
  • DataTemplate

11.2 数据的外衣 DataTemplate

DataTemplate:常用于

  • ContentContro的ContentTemplate属性,给其内容穿衣服
  • ItemsControls的ItemTemplate属性,给ItemsControl的数据条目穿衣服
  • GridViewColumn的CellTemplate属性,给单元格里的数据穿衣服

类定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="Car.cs" company="ss">
// ss
// </copyright>
// <summary>
// Defines the Car type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace TemplateHer
{
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media.Imaging;

/// <summary>
/// The car.
/// </summary>
public class Car
{
public string Automaker { get; set; }
public string Name { get; set; }
public string Year { get; set; }
public string TopSpeed { get; set; }
}

public class AutomakerToLogoPathConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string uriStr = string.Format(@"/Resources/{0}.png", (string)value);
return new BitmapImage(new Uri(uriStr, UriKind.Relative));
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

public class NameToPhotoPathConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string uriStr = string.Format(@"/Resources/{0}.jpg", (string)value);
return new BitmapImage(new Uri(uriStr, UriKind.Relative));
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<Window.Resources>
<local:AutomakerToLogoPathConverter x:Key="a2l"/>
<local:NameToPhotoPathConverter x:Key="n2p"/>
<DataTemplate x:Key="carDetailViewTemplate">
<Border BorderBrush="Black" BorderThickness="1" CornerRadius="6">
<StackPanel Margin="5">
<Image Width="400" Height="250"
Source="{Binding Name,Converter={StaticResource n2p}}"/>
<StackPanel Orientation="Horizontal" Margin="5,0">
<TextBlock Text="Name:" FontWeight="Bold" FontSize="20"/>
<TextBlock Text="{Binding Name}" FontSize="20" Margin="5,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5,0">
<TextBlock Text="Automaker:" FontWeight="Bold"></TextBlock>
<TextBlock Text="Year:" FontWeight="Bold"/>
<TextBlock Text="{Binding Year}" Margin="5,0"/>
<TextBlock Text="TopSpeed:" FontWeight="Bold"/>
<TextBlock Text="{Binding TopSpeed}" Margin="5,0"/>
</StackPanel>
</StackPanel>
</Border>
</DataTemplate>

<DataTemplate x:Key="carListItemViewTemplate">
<Grid Margin="2">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Automaker,Converter={StaticResource a2l}}"
Grid.RowSpan="3" Width="64" Height="64"/>
<StackPanel Margin="5,10">
<TextBlock Text="{Binding Name}" FontSize="16" FontWeight="Bold"/>
<TextBlock Text="{Binding Year}" FontSize="14"/>
</StackPanel>
</StackPanel>
</Grid>
</DataTemplate>
</Window.Resources>
<StackPanel Orientation="Horizontal" Margin="5">
<UserControl ContentTemplate="{StaticResource carDetailViewTemplate}"
Content="{Binding SelectedItem,ElementName=listBoxCars}"/>
<ListBox x:Name="listBoxCars" Width="180" Margin="5,0"
ItemTemplate="{StaticResource carListItemViewTemplate}"/>
</StackPanel>

后台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public CarHerDataSource()
{
InitializeComponent();
InitialCarList();
}

private void InitialCarList()
{
List<Car> carList = new List<Car>()
{
new Car(){Automaker="纸片人老婆",Name="刀女人",Year="1990",TopSpeed="320"},
new Car(){Automaker="纸片人老婆",Name="困女子",Year="2990",TopSpeed="350"},
new Car(){Automaker="纸片人老婆",Name="妹女儿",Year="2000",TopSpeed="325"},
new Car(){Automaker="纸片人老婆",Name="音女郎",Year="2010",TopSpeed="356"},
};
this.listBoxCars.ItemsSource = carList;
}

11.3 控件的外衣 ControlTemplate

披着羊皮的🐺

Note

两大勇武之地:

  • 提升用户体验
  • ControlTemplate,程序员与设计师可以并行工作

11.3.1 庖丁解控件

用了下Blend解开TextBox,很好玩

11.3.2 ItemsControl的PanelTemplate

让程序有机会控制ItemsControl的条目容器

1
2
3
4
5
6
7
8
9
10
11
<ListBox>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"></StackPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<TextBlock Text="Allan"/>
<TextBlock Text="Allan"/>
<TextBlock Text="Allan"/>
<TextBlock Text="Allan"/>
</ListBox>

##11.4 DataTemplate与ControlTemplate的关系与应用

###11.4.1 DataTemplate与ControlTemplate的关系

既然Template生成的控件树都有根,那么如何找到树根呢。每个控件都有个名为TemplatedParent的属性,如果它的值不为null,说明这个控件是由Template自动生成的,而属性值就是应用了模板的控件(模板的目标,模板化控件)。如果由Template生成的控件使用了TemplateBinding获取属性值,则TemplateBinding的数据源就是应用了这个模板的目标控件
回顾一下本章开头的DataTemplate实例代码

1
2
3
4
5
6
7
8
9
10
11
<DataTemplate>
<Grid>
<StackPanel Orientation="Horizontal">
<Grid>
<Rectangle Stroke="Yellow" Fill="Orange" Width="{Bingding Price}"/>
<TextBlock Text="{Binding Year}"/>
</Grid>
<TextBlock Text="{Bingding Price}" Margin="5.0"/>
</StackPanel>
</Grid>
</DataTemplate>

11.4.2 DataTemplate与ControlTemplate的应用

分为逐个应用和整体应用

整体就不标记Key

如果不想应用则需要把控件的Style标记为{x:Null}

1
2
3
4
5
<StackPanel>
<TextBox/>
<TextBox/>
<TextBox Style="{x:Null}" Margin="5" Text="No usage of Style"/>
</StackPanel>

DataTemplate应用到某个数据类型上的方法 是设置DataTemplate的DataType属性,并且不能带Key标记

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<Window.Resources>
<!--DataTemplate-->
<DataTemplate DataType="{x:Type local:Unit}">
<Grid>
<StackPanel Orientation="Horizontal">
<Grid>
<Rectangle Stroke="Yellow" Fill="Orange" Width="{Binding Price}"/>
<TextBlock Text="{Binding Year}"/>
</Grid>
<TextBlock Text="{Binding Price}" Margin="5,0"/>
</StackPanel>
</Grid>
</DataTemplate>
<!--DataSource-->
<c:ArrayList x:Key="ds">
<local:Unit Year="2001 Y" Price="100"/>
<local:Unit Year="2002 Y" Price="120"/>
<local:Unit Year="2003 Y" Price="140"/>
<local:Unit Year="2004 Y" Price="160"/>
<local:Unit Year="2005 Y" Price="180"/>
<local:Unit Year="2006 Y" Price="200"/>
</c:ArrayList>

</Window.Resources>
<StackPanel>
<ListBox ItemsSource="{StaticResource ds}"/>
<ComboBox ItemsSource="{StaticResource ds}" Margin="5"/>
</StackPanel>

所以DataTemplate会自动加载到所有Unit类型对象上

1
2
3
4
5
public class Unit
{
public int Price { get; set; }
public string Year { get; set; }
}

应用到Xml也很方便

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<Window.Resources>
<!--DataTemplate-->
<DataTemplate DataType="Unit">
<Grid>
<StackPanel Orientation="Horizontal">
<Grid>
<Rectangle Stroke="Yellow" Fill="Orange" Width="{Binding XPath=@Price}"/>
<TextBlock Text="{Binding XPath=@Year}"/>
</Grid>
<TextBlock Text="{Binding XPath=@Price}" Margin="5,0"/>
</StackPanel>
</Grid>
</DataTemplate>
<!--DataSource-->
<XmlDataProvider x:Key="ds" XPath="Units/Unit">
<x:XData>
<Units xmlns="">
<Unit Year="2001" Price="100"/>
<Unit Year="2001" Price="110"/>
<Unit Year="2001" Price="120"/>
<Unit Year="2001" Price="130"/>
<Unit Year="2001" Price="140"/>
<Unit Year="2001" Price="150"/>
</Units>
</x:XData>
</XmlDataProvider>
</Window.Resources>

<StackPanel>
<ListBox ItemsSource="{Binding Source={StaticResource ds}}"/>
<ComboBox ItemsSource="{Binding Source={StaticResource ds}}" Margin="5,0"/>
</StackPanel>

主要是多了个XmlDataProvider x:Data & @

班级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<Data xmlns="">
<Grade Name="Grade One">
<Class Name="Class One">
<Group Name="A group"/>
<Group Name="B group"/>
<Group Name="C group"/>
</Class>
<Class Name="Class 2">
<Group Name="A group"/>
<Group Name="B group"/>
<Group Name="C group"/>
</Class>
</Grade>
<Grade Name="Grade Two">
<Class Name="Class One">
<Group Name="A group"/>
<Group Name="B group"/>
<Group Name="C group"/>
</Class>
<Class Name="Class 2">
<Group Name="A group"/>
<Group Name="B group"/>
<Group Name="C group"/>
</Class>
</Grade>
</Data>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<Window.Resources>
<!--DataSource-->
<XmlDataProvider x:Key="ds" Source="Data.xml" XPath="Data/Grade"/>
<!--GradeMod-->
<HierarchicalDataTemplate DataType="Grade" ItemsSource="{Binding XPath=Class}">
<TextBlock Text="{Binding XPath=@Name}"/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="Class" ItemsSource="{Binding XPath=Group}">
<RadioButton Content="{Binding XPath=@Name}" GroupName="gn"/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="Group" ItemsSource="{Binding XPath=Student}">
<CheckBox Content="{Binding XPath=@Name}"/>
</HierarchicalDataTemplate>

</Window.Resources>
<Grid>
<TreeView Margin="5" ItemsSource="{Binding Source={StaticResource ds}}"/>
</Grid>

操作窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<Data xmlns="">
<Operation Name="File" Gesture="F">
<Operation Name="New" Gesture="N">
<Operation Name="Project" Gesture="Control+P"/>
<Operation Name="WebSite" Gesture="Control+W"/>
<Operation Name="Document" Gesture="Control+D"/>
</Operation>
<Operation Name="Save" Gesture="S"/>
<Operation Name="Print" Gesture="P"/>
<Operation Name="Quit" Gesture="X"/>
</Operation>
<Operation Name="Edit" Gesture="E">
<Operation Name="Copy" Gesture="Control+C"/>
<Operation Name="Cut" Gesture="Control+X"/>
<Operation Name="Paste" Gesture="Control+V"/>
</Operation>
</Data>
1
2
3
4
5
6
7
8
9
10
11
12
13
<Window.Resources>
<XmlDataProvider x:Key="ds" Source="/DataXml/Menu.xml" XPath="Data/Operation"/>
<HierarchicalDataTemplate DataType="Operation"
ItemsSource="{Binding XPath=Operation}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding XPath=@Name}" Margin="10,0"/>
<TextBlock Text="{Binding XPath=@Gesture}"/>
</StackPanel>
</HierarchicalDataTemplate>
</Window.Resources>
<StackPanel>
<Menu ItemsSource="{Binding Source={StaticResource ds}}"/>
</StackPanel>
1
2
3
4
5
6
private void StackPanel_Click(object sender, RoutedEventArgs e)
{
MenuItem mi = e.OriginalSource as MenuItem;
XmlElement xe = mi.Header as XmlElement;
MessageBox.Show(xe.Attributes["Name"].Value);
}

在StackPanel_Click上加,就可以根据拿到的数据来决定执行什么RoutedCommand