控件模型:WPF 提供三个用于创建控件的常规模型,每个模型都提供不同的功能集和灵活度。 三个模型的基类是UserControl、Control 和 FrameworkElement 。其中UserControl称为用户控件继承自ContentControl,提供类似于Window窗口的简单布局控件创建方式(实现组合控件)。而Control 和 FrameworkElement 称为自定义控件,自定义控件比用户控件更低级别,得到的控制越多,但继承的功能就越少。用户控件和自定义控件之间的主要区别之一:自定义控件可以设置样式/模板(DataTemplate/ContentTemplate),而用户控件则不能。
从 UserControl 派生的优点:如果符合以下所有情况,请考虑从 UserControl 派生(我们以UserControl为例):
希望采用与生成应用程序相似的方法生成控件。
控件仅包含现有组件(组合控件)。
无需支持复杂的自定义项。
用户组合控件实践:UserControl 的使用类似于WPF Window窗口,包含用户控件.xaml UI文件,以及后置代码.xaml.cs文件。UserControl控件包含一个Content的Object属性,行为很像WPF窗口用于放置其他控件布局。用户控件是将标记和代码分组到可重用容器中的概念。在实践中,我们将创建一个可重用的简单用户控件,能够将TextBox中的文本数量限制为特定数量的字符,同时向用户显示已使用的字符数以及可以使用的字符数。
<UserControl x:Class="WPF_Demo.LimitedInputUserControl"
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:WPF_Demo"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
Grid.ColumnDefinitions>
<Label Content="{Binding Title}" />
<Label Grid.Column="1">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding ElementName=txtLimitedInput,Path=Text.Length}"/>
<TextBlock Text="/" />
<TextBlock Text="{Binding MaxLength}"/>
StackPanel>
Label>
<TextBox Name="txtLimitedInput" Grid.Row="1" Grid.ColumnSpan="2" MaxLength="{Binding MaxLength}" TextWrapping="Wrap" ScrollViewer.VerticalScrollBarVisibility="Auto"/>
Grid>
UserControl>
namespace WPF_Demo
{
///
/// LimitedInputUserControl.xaml 的交互逻辑
///
public partial class LimitedInputUserControl : UserControl
{
public LimitedInputUserControl()
{
InitializeComponent();
this.DataContext = this;
}
public string Title { set; get; }
public int MaxLength { set; get; }
}
}
我们看到在上面的例子中,TextOptions和ToolTipService等属性以及在数据绑定中声明的Binding对象等都不是控件的基本属性,但为什么这些属性能直接作用于当前的控件对象呢?这其实就是WPF中的依赖属性在起作用。 依赖属性的目的是提供一种方法来基于其他输入的值来计算属性的值,具体如下。
WPF引入了一种新的属性机制被称为依赖属性,依赖属性可以通过普通属性进行包装,让我们使用起来与普通属性操作没有什么区别。其目的是提供一种方法来基于其他输入的值来计算属性的值。简而言之,依赖属性就是一种可以自己没有值,并能通过使用Binding机制从数据源获得值(依赖在别的对象身上)的属性,拥有依赖属性的对象被称为“依赖对象”。依赖属性带来的优点包括:
- 节省内存对实例的开销:假设当前有一个对象, 它拥有100个标准属性,并且背后都定义了一个4字节的字段, 如果我们初始化10000个这样的对象, 那么这些字段将占用100×4×10000= 3.81M 内存。但是实际上, 我们并非使用到所有的属性, 这就意味着大多数内存会被浪费。但是通过依赖属性,我们可以将不同的属性依赖统一的外部提供给我们,只有在明确设定/用到属性值时才去开辟空间,可以减少内存消耗。
- 属性值可以通过Binding依赖在别的对象身上,自动建立对象属性之间的计算关系,来完成“数据绑定”的效果
在 WPF系统中,依赖对象的概念被 DependencyObject类所实现,依赖属性的概念则由DependencyProperty类所实现。DependencyObject具有GetValue和SetValue两个方法,这两个方法都以DependencyProperty对象为参数,GetValue方法通过DependencyProperty对象获取所依赖数据源的数据;SetValue通过 DependencyProperty对象存储值----正是这两个方法把 DependencyObject和 DependencyProperty 紧密结合在一起。WPF开发中,必须使用依赖对象作为依赖属性的宿主,使二者结合起来,才能形成完整的 Binding目标被数据所驱动。换句话说,使用依赖属性必须要继承DependencyObject类,只有将DependencyObject所提供的GetValue与SetValue方法与DependencyProperty结合在一起才能发挥出依赖属性的作用,具体原因我们将在源码分析中进行说明。
在WPF中,WPF的所有UI控件都是依赖对象。WPF的类库在设计时充分利用了依赖属性的优势,UI控件的绝大多数属性都已经依赖化了,比如Width、Height、Text等等。我们平时所使用的这些属性只不过是内部依赖属性的包装器,但我们根本察觉不出依赖属性的存在。因为字段也好,依赖属性也好,我们在外部看到的操作都是它的属性而已。
public class MyClass : DependencyObject //1.继承DependencyObject类
{
public string MyName //3.依赖属性包装器
{
get => (string)GetValue(MyNameProperty); //使用DependencyObject类提供的GetValue方法取值
set => SetValue(MyNameProperty, value);
}
public static readonly DependencyProperty MyNameProperty =
DependencyProperty.Register("MyName", typeof(string), typeof(MyClass)); //2.初始化DependencyProperty依赖属性对象
}
- 要点一 : 使用依赖属性必须要继承DependencyObject类,目的的是结合其提供的Set/GetValue方法以及内部提供的一些数据结构来操作DependencyProperty
- 要点二 : 依赖属性声明需要使用public static readonly三个修饰符修饰,实例依赖属性也不是通过new操作符,而是通过DependencyProperty的Register方法来获取。
- 修饰符目的 : 通过static将依赖属性对象声明为静态的属于类的而不是每个实例,减少内存开销;通过readonly是为了保证依赖属性对象内部一些属性值计算的稳定性(GlobaIndex)
- 要点三 : 依赖属性对象的命名约定 : 以Property为后缀,以表明它是一个依赖属性
- 要点四 : Register方法有三个重载,此处用的是其三个参数的重载,它还有四个参数和五个参数的重载。我们这里介绍一下它的主要参数
- 第一个参数(string) : 指定依赖属性的包装器名称是什么(包装器是通过一个属性来包装依赖属性供外部使用的)
- 第二个参数(type) : 指定依赖属性要存储的值的类型是什么
- 第三个参数(type) : 指定依赖属性属于哪个类的,或者说是为哪个类定义依赖属性(依赖属性的宿主类型)
- 要点五 : 我们可以通过声明依赖属性包装器将依赖属性暴露出去,内部将依赖属性的SetValue/GetValue封装起来。对依赖属性的这层包装,使得我们在外部操作依赖属性变得简单,这也是为什么我们在正常使用中感觉不到依赖属性的存在
public MainWindowBase()
{
InitializeComponent();
this.DataContext = this;
Data = "我是皮卡丘";
MyClass myclass = new MyClass();
//使用Binding操作类将MyClass对象的名字依赖属性关联到Data上
BindingOperations.SetBinding(myclass,MyClass.MyNameProperty, new Binding("Data") { Source = this });
//将按钮的Content依赖属性绑定到皮卡丘的皮卡丘名字包装器上
btn_show.SetBinding(Button.ContentProperty, new Binding(nameof(myclass.MyNameProperty)) { Source = myclass });
}
这个例子的逻辑是有一个名为Data的属性作为数据源,先将皮卡丘对象的依赖属性绑定到Data数据源上,再将Button的Content依赖属性绑定到皮卡丘对象的依赖属性包装器上,这就形成了一个Binding链。整个过程中,只有Data属性是有字段在背后支撑的,它存储了“我是皮卡丘”这个数据,皮卡丘对象和Button对象都是依赖属性,不占内存空间,它们之间使用Binding关联,形成数据通道,这样就实现了一块内存,供给多处使用。按照之前的编程模式,需要皮卡丘和Button各自开辟一段空间存储Data来的数据,现在由三块内存节省为一块内存,这就是依赖属性对于节省内存的效果。
(1)DependencyProperty类
public sealed class DependencyProperty
{
...
//关键1 全局Hashtable : PropertyFromName是DependencyProperty类中的一个全局静态的Hashtable,它类似于Dictionary,是一个键值对集合。这个全局Hashtable就是用来注册保存DependencyProperty实例对象的
private static Hashtable PropertyFromName = new Hashtable();
...
//关键2 GlobalIndex唯一标识 : 该属性是一个只读的实例属性,通过算法生成每个DependencyProperty实例对象的全局且唯一的标识ID
public int GlobalIndex
{
get { return (int) (_packedData & Flags.GlobalIndexMask); }
}
...
//关键3 : RegisterCommon 注册 : DependencyProperty的Register方法最终会调用RegisterCommon来进行全局依赖属性对象的注册(生成key存入Hashtable)
private static DependencyProperty RegisterCommon(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, ValidateValueCallback validateValueCallback)
{
//根据DependencyProperty注册时的 包装器name和所属类型ownerType参数,通过FromNameKey类提供的异或运算方式生成对应的唯一key
FromNameKey key = new FromNameKey(name, ownerType);
lock (Synchronized)
{
//如果有重复值key:则抛出异常(也就是说每对 “宿主类型-包装器属性名称” 只能对应生成一个全局DependencyProperty依赖属性对象)
if (PropertyFromName.Contains(key))
{
throw new ArgumentException(SR.Get(SRID.PropertyAlreadyRegistered, name, ownerType.Name));
}
}
//做一些校验逻辑、默认值计算等操作
...
// new DependencyProperty对象,经过层层把关,依赖属性终于new出来
DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultMetadata, validateValueCallback);
...
//PropertyFromName是存储依赖属性对象的一个全局key-value集合,所有new出来的依赖对象都存储在这里,它的key就是之前通过FromNameKey类通过异或出的。
lock (Synchronized)
{
PropertyFromName[key] = dp;
}
//返回生成的DependencyProperty全局对象
return dp;
}
}
(2) DependencyObject类
public class DependencyObject : DispatcherObject
{
...
//关键一 : EffectiveValueEntry数组,用于存储EffectiveValueEntry类型,EffectiveValues采用索引——值的形式,保存的是本对象实例中所有用到的依赖属性所对应设置的值。要注意的是该数组非静态全局,是属于每个对象实例的。因此我们可以得出,依赖属性是全局static保存的,但依赖属性对应的值在每个实例对象中都是不一样的,因此保存在每个实例对象的EffectiveValueEntry数组中(用到才保存,用不到不存储节约空间),值和属性分开存储,建立映射。
private EffectiveValueEntry[] _effectiveValues;
...
//关键二 : 获取DependencyProperty依赖属性在本对象实例中对应的值,根据dp.GlobalIndex唯一标识来在本地EffectiveValueEntry数组中获取索引值
public object GetValue(DependencyProperty dp)
{
// Do not allow foreign threads access.
// (This is a noop if this object is not assigned to a Dispatcher.)
//
this.VerifyAccess();
if (dp == null)
{
throw new ArgumentNullException("dp");
}
// Call Forwarded
return GetValueEntry(
LookupEntry(dp.GlobalIndex),
dp,
null,
RequestFlags.FullyResolved).Value;
}
...
//关键三 : 设置DependencyProperty依赖属性的值。检查EffectiveValucEntry数组中是否已经存在相应依赖属性的位置,如果有则把旧值改写为新值,如果没有则新建EffectiveValueEntry对象并存储新值。这样,只有被用到的值才会被放进这个列表,借此,WPF系统用算法(时间)换取了对内存(空间)的节省。
public void SetValue(DependencyProperty dp, object value)
{
// Do not allow foreign threads access.
// (This is a noop if this object is not assigned to a Dispatcher.)
//
this.VerifyAccess();
// Cache the metadata object this method needed to get anyway.
PropertyMetadata metadata = SetupPropertyChange(dp);
// Do standard property set
SetValueCommon(dp, value, metadata, false /* coerceWithDeferredReference */, false /* coerceWithCurrentValue */, OperationType.Unknown, false /* isInternal */);
}
}
所以,依赖属性的工作方式如下:
附加属性就是对于一个对象而言, 本来它不具备这个属性, 但是由于附加给这个对象, 然后才有了这个属性,这种我们称之为附加属性。
注:附加属性也是依赖属性, 只是它的注册方式与表达方式略有不同。
- prop+tab : 自动创建 属性
- propdp+tab : 自动创建 依赖属性及其包装器
- propfull+tab : 自动创建 属性及其字段
- propa+tab : 自动创建 附加属性
数据绑定的核心是Binding对象和依赖属性,其中依赖属性我们之前已经说过了,所以我们这里介绍一下Binding对象的概念。WPF的数据绑定功能提供了一种简单一致的方法来呈现应用程序和与数据交互。 通过数据绑定,可以同步两个不同的对象的属性的值,Binding对象就是实现数据绑定的数据通道。
简而言之,我们可以想象Binding这座桥梁上铺设了高速公路,我们不但可以控制公路是在源与目标之间双向通行还是某个方向的单行道(Mode),还可以控制对数据放行的时机(Trigger),甚至可以在桥上架设一些“关卡”用来转换数据类型(Convert)或者检验数据的正确性。
- 每个绑定通常包含以下四个组件:绑定目标对象、目标属性、绑定源以及Path(要使用的绑定源中的值)
- 目标属性必须为依赖属性。 这也意味着不能绑定字段。 也就是目标属性自己没有值,只能通过依赖绑定的数据对象来计算值
- 绑定源对象不限于自定义 CLR 对象。绑定源可以是 UIElement、任何列表对象等
- Binding对象只提供数据通道/通路的作用,并不提供数据变更通知机制。可以使用 Mode 属性指定数据流的开放方向。 若要检测单向或双向绑定中的源更改,源必须实现适当的属性更改通知机制,例如 INotifyPropertyChanged。 具体的若要支持 OneWay 或 TwoWay 绑定,从而使绑定目标属性能够自动反映绑定源的动态更改(例如,用户编辑窗体后,预览窗格会自动更新),类需要提供相应的属性更改通知。
简单而言, 数据绑定是一种关系, 这种关系告诉WPF 从一个源目标对象中提取一些信息, 并且使用该信息设置为目标对象的属性。目标属性总是依赖项属性, 并且通常位于WPF元素中。然而, 源对象可以是任何内容, 可是是随机生成的一个对象、也可以是数据库的数据对象,或者手动创建的对象。
public class Binding : System.Windows.Data.BindingBase
- Binding(String) : 使用初始路径Path 初始化 Binding 类的新实例。
- Converter(IValueConverter) : 获取或设置要使用的转换器。
- ElementName(string) : 获取或设置要用作绑定源对象的元素的名称(相关元素的 Name 属性或 x:Name的值)
- Mode(BindingMode) : 获取或设置一个值,该值指示绑定的数据流方向(开启数据通道的方向,但并不监听数据改变)。
- Path(PropertyPath) : 获取或设置绑定源-属性的路径。默认值为 null。
- 在最简单的情况下,Path 属性值是要用于绑定的源对象的属性名称,支持多级路径等(一路.下去)。
- 注意典型的string、int等基本类型的实例本身就是数据,我们无法指出通过它的哪个属性来访问这个数据,这时我们只需将Path的值设置为“.”就可以了。在XAML代码里这个“.”可以省略不写,但在C#代码里却不能省略。
- 若要绑定到整个对象/本身数据,也可无需指定 Path 属性。{Binding} = {Binding Path="."}
- Source(Object) : 获取或设置要用作绑定源的对象。没有Source时,从当前节点沿着UI元素树一路向树的根部找过去
- StringFormat(string) : 获取或设置一个字符串,该字符串指定如果绑定值显示为字符串,应如何设置该绑定的格式。
- UpdateSourceTrigger : 获取或设置一个值,它可确定绑定源更新的时机。绑定的UpdateSourceTrigger属性控制将更改的值由UI发送回源的方式和时间。侦听目标属性中的更改并将其传播回源,这称为更新源
ElementName常用于在不同的UI元素之间建立关系,其输入数据为绑定元素的 Name或x:Name 字符串
(1)XAML绑定格式
<Window x:Class="WPF_Demo.MainWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPF_Demo"
mc:Ignorable="d"
Title="MainWindow" Height="110" Width="300">
<StackPanel>
<TextBox x:Name="textBox" Text="{Binding Path=Value,ElementName=slider}" BorderBrush="Black" Margin="5"/>
<Slider x:Name="slider" Maximum="100" Minimum="0" Margin="5"/>
StackPanel>
Window>
(2)代码绑定格式
//代码绑定声明格式
namespace WPF_Demo
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//1.新建Binding对象
Binding binding = new Binding("Value");
//2.关联绑定的属性
//binding.Path = new PropertyPath("Value");
//3.指定源对象
//binding.Source = this.slider;
binding.ElementName = "slider";
//4.绑定对象 目标对象.SetBinding(目标全局依赖属性,绑定对象)
this.textBox.SetBinding(TextBox.TextProperty, binding);
//5.C#3.0 初始化语法 四合一操作
//this.textBox.SetBinding(TextBox.TextProperty, new Binding("Value") { Source = this.slider });
}
}
}
Source的类型为Object,可以绑定任意对象数据。但自定义对象无法自动通知绑定UI更新数据(无法实时同步),Binding对象只是提供了一个数据通道,只会绑定显示数据初始值。要实现自定义对象的属性更新通知,要源实现INotifyPropertyChanged接口。当为Binding 设置了数据源后,Binding就会自动侦听来自这个接口的PropertyChanged事件。
public class Person : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return name; }
set
{
if(value != null)
{
//属性值改变时,通知绑定事件
this.name = value;
this.NotifyPropertyChanged("Name");
}
name = value;
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
(1)代码绑定格式
namespace WPF_Demo
{
public partial class MainWindow : Window
{
Person per;
public MainWindow()
{
InitializeComponent();
//初始化自定义对象
per = new Person() { Name = "Init" };
//准备Binding
//1.新建Binding对象
Binding binding = new Binding();
//2.关联绑定的属性
binding.Path = new PropertyPath("Name");
//3.指定源数据
binding.Source = per;
//4.绑定对象 BindingOperations.SetBinding(目标对象,目标全局依赖属性,绑定对象)
BindingOperations.SetBinding(this.textBox, TextBox.TextProperty, binding);
//注意:上面等价于下面的操作,只不过为了方便快捷,WPF UI控件将BindingOperations.SetBinding封装进了自身SetBinding方法里
//this.textBox.SetBinding(TextBox.TextProperty, binding);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
per.Name += " add...";
}
}
}
(2)XAML绑定格式
值得注意的是,在C#代码中可以访问XAML 代码中声明的变量;但XAML 代码中却无法直接访问C#代码中声明的变量。因此,要想在XAML中建立UI元素与逻辑层对象的 Binding 需要借助资源(Resource)的使用。
<Window x:Class="WPF_Demo.MainWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPF_Demo"
mc:Ignorable="d"
Title="MainWindow" Height="110" Width="300">
<Window.Resources>
<local:Person x:Key="per" Name="Init"/>
Window.Resources>
<StackPanel>
<TextBox x:Name="textBox" BorderBrush="Black" Margin="5" Text="{Binding Path=Name,Source={StaticResource per}}"/>
<Button Content="Add" Margin="5" Click="Button_Click"/>
StackPanel>
Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
((Person)this.FindResource("per")).Name += "add...";
}
}
DataContext属性是数据绑定行为的默认源。DataContext属性被定义在FrameworkElement类里,这个类是WPF控件的基类,这意味着所有WPF 控件(包括容器控件)都具备这个属性。如前所述,WPF的UI布局是树形结构,这棵树的每个结点都是控件,也就是说在UI元素树的每个结点都有其DataContext属性。形象地说,DataContext相当于一个数据的“制高点”。
若 一个Binding对象 只知道自己的Path而不知道自己的Soruce时,它会从当前节点沿着UI元素树一路向树的根部找过去,每路过一个结点就要看看这个结点的 DataContext是否具有Path 所指定的属性名。如果有,那就把这个对象作为自己的 Source;如果没有,那就继续找下去;如果到了树的根部还没有找到,那这个Binding就没有Source,因而也不会得到数据(但不会报错,只是没有值)。DataContext的使用场景如下:
- 当UI上的多个控件都使用Binding关注同一个对象时
- 当作为Source的对象不能被直接访问时。另外,DataContext本身也是一个依赖属性,可以通过Binding关联到其他数据源上形成 Binding链。
<Window x:Class="WPF_Demo.MainWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPF_Demo"
mc:Ignorable="d"
Title="MainWindow" Height="110" Width="300">
<StackPanel>
<TextBox x:Name="textBox" BorderBrush="Black" Margin="5" Text="{Binding Name}"/>
<Button Content="Add" Margin="5" Click="Button_Click"/>
StackPanel>
Window>
namespace WPF_Demo
{
public partial class MainWindow : Window
{
Person per;
public MainWindow()
{
InitializeComponent();
//初始化自定义对象
per = new Person() { Name = "Init" };
//声明本Window的上下文DataContext
this.DataContext = per;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.per.Name += " add...";
}
}
public class Person : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return name; }
set
{
if(value != null)
{
this.name = value;
this.NotifyPropertyChanged("Name");
}
name = value;
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
列表控件都派生自 ItemsControl 类,继承了列表控件关于数据绑定的一些属性。其原理为:ItemsControl的ItemsSource绑定集合后,会自动为每一个数据元素创建等量的条目容器 xxxItem(继承自ContentControl ),这是列表控件中每个数据条目展示的 “数据外衣”,并自动使用Binding在每个数据条目容器与数据元素之间建立关联和绑定(将每个列表集合数据作为每条Item的DataContext)。因此列表控件的数据绑定包括外部整个列表集合的绑定和内部每个数据元素的绑定。
- Items : [获取]用于生成 ItemsControl 的内容的集合,可以是不同类型数据。 默认值为空集合。
- ItemsSource : [获取或设置]用于生成 ItemsControl 的内容的集合。接收一个 IEnumerable 接口派生类实例作为数据源(所有可迭代的集合都实现了该接口),默认值为 null。设置ItemsSource后集合Items是只读且固定大小的(无法通过Items添加修改数据)。
- DisplayMemberPath : 获取或设置[源对象上的值的路径]。默认值为空字符串("")。若未指定DisplayMemberPath以及DataTemplate,则 ListBox 显示基础集合中每个对象的字符串表示形式(toString)
ListBox:
- SelectedItem(Object) : 获取或设置当前选择中的第一项,如果选择为空,则返回 null。
- SelectedItems(IList) : 获取当前选定的项。
(1)代码示例
<Window x:Class="WPF_Demo.MainWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPF_Demo"
mc:Ignorable="d"
Title="MainWindow" Height="270" Width="300">
<StackPanel x:Name="stackPanel" Background="LightBlue">
<TextBlock Text="Student ID:" FontWeight="Bold" Margin="5"/>
<TextBox x:Name="textBoxId" BorderBrush="Black" Margin="5"/>
<TextBlock Text="Student List:" FontWeight="Bold" Margin="5"/>
<ListBox x:Name="listBoxStudents" Height="110" Margin="5"/>
<Button Content="Add" Margin="5" Click="Button_Click"/>
StackPanel>
Window>
namespace WPF_Demo
{
public partial class MainWindow : Window
{
List<Student> stuList;
public MainWindow()
{
InitializeComponent();
//列表数据源
stuList = new List<Student>()
{
new Student(){Id=202201,Name="Tim",Age=19},
new Student(){Id=202202,Name="Kitty",Age=20},
new Student(){Id=202203,Name="Tom",Age=18},
new Student(){Id=202204,Name="Mike",Age=22},
};
//为ListBox设置Binding(ItemSource依赖属性)
this.listBoxStudents.ItemsSource = stuList;
//设置源数据的值路径(如果不设置,则默认显示对象的toString)
this.listBoxStudents.DisplayMemberPath = "Name";
//上述等价于
//Binding bind1 = new Binding() { Source = this.stuList };
//this.listBoxStudents.SetBinding(ListBox.ItemsSourceProperty, bind1);
//this.listBoxStudents.DisplayMemberPath = "Name";
//为TextBox设置Binding
Binding binding = new Binding("SelectedItem.Id") { Source = this.listBoxStudents };
this.textBoxId.SetBinding(TextBox.TextProperty, binding);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
//测试数据变化能否自动推送到UI : 显然否
MessageBox.Show(this.listBoxStudents.Items.Count.ToString());
this.stuList.Add(new Student() { Id = 202205, Name = "Wx", Age = 23 });
}
}
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
}
注意: 普通的List作为源,逻辑层修改数据后无法立即响应推送给UI更新,只能绑定初始数据。需要 INotifyCollectionChanged 通知接口的协助。
(2)XAML示例
<Window x:Class="WPF_Demo.MainWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPF_Demo"
mc:Ignorable="d"
Title="MainWindow" Height="270" Width="300">
<StackPanel x:Name="stackPanel" Background="LightBlue">
<TextBlock Text="Student ID:" FontWeight="Bold" Margin="5"/>
<TextBox x:Name="textBoxId" BorderBrush="Black" Margin="5" Text="{Binding ElementName=listBoxStudents,Path=SelectedItem.Id}"/>
<TextBlock Text="Student List:" FontWeight="Bold" Margin="5"/>
<ListBox x:Name="listBoxStudents" Height="110" Margin="5" DisplayMemberPath="Name" ItemsSource="{Binding}" />
<Button Content="Add" Margin="5" Click="Button_Click"/>
StackPanel>
Window>
namespace WPF_Demo
{
public partial class MainWindow : Window
{
List<Student> stuList;
public MainWindow()
{
InitializeComponent();
//列表数据源
stuList = new List<Student>()
{
new Student(){Id=202201,Name="Tim",Age=19},
new Student(){Id=202202,Name="Kitty",Age=20},
new Student(){Id=202203,Name="Tom",Age=18},
new Student(){Id=202204,Name="Mike",Age=22},
};
//设置上下文,为ListBox设置Binding
this.DataContext = stuList;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show(this.listBoxStudents.Items.Count.ToString());
this.stuList.Add(new Student() { Id = 202205, Name = "Wx", Age = 23 });
}
}
}
注意: 我们看到一个 {Binding} 的空绑定语法,首先该语法省略了 Source 属性,则数据源会从当前节点向上查找 DataContext;然后该语法又省略了 Path 属性,则意味着绑定数据对象本身(toString),则找到的第一个DataContext 上下文即为绑定对象。
在数据Binding中,绑定数据源主要有四种方式,分别是:
- Binding.ElementName :绑定名称Name、x:Name的 UI 元素(string类型)。适用于明确知道Name时的前端UI元素绑定
- Binding.Source:显式的指定要绑定的源对象(object类型)。适用于明确知道对象实例时的前端资源绑定
- DataContext : 数据绑定的默认行为,寻找上下文作为数据绑定源(object类型)。适用于后台全局逻辑对象绑定
- Binding.RelativeSource:通过指定绑定源相对于绑定目标位置的位置(相对绑定),获取或设置此绑定源。(RelativeSource对象)。适用于需要绑定的数据源可能没有明确的Name而只有模糊的位置关系时的绑定
RelativeSource 属性
- AncestorLevel: 以 FindAncestor 模式获取或设置要查找的上级级别。 使用 1 指示最靠近绑定目标元素的项。
- AncestorType: 获取或设置要查找的上级节点的类型。
- Mode(enum RelativeSourceMode): 获取或设置 RelativeSourceMode 值,该值描述相对于绑定目标的位置的绑定源的位置关系。
- FindAncestor: 引用数据绑定元素的父链中的上级。 这可用于绑定到特定类型的上级或其子类。 如果您要指定 AncestorType 和/或 AncestorLevel,可以使用此模式。
- PreviousData: 允许在当前显示的数据项列表中绑定上一个数据项(不是包含数据项的控件)。
- Self: 引用正在其上设置绑定的元素,并允许你将该元素的一个属性绑定到同一元素的其他属性上。
- TemplatedParent: 引用应用了模板的元素,其中此模板中存在数据绑定元素。 这类似于设置 TemplateBindingExtension,并仅当 Binding 在模板中时适用。
- PreviousData: 获取一个[静态值],该值用于返回为 RelativeSource 模式构造的 PreviousData。
- Self: 获取一个[静态值],该值用于返回为 RelativeSource 模式构造的 Self。
- TemplatedParent: 获取一个[静态值],该值用于返回为 RelativeSource 模式构造的 TemplatedParent。
(1)Mode Self 关联控件自身
<TextBlock FontSize="18" Text="Simple"
Name="Box1"
FontWeight="Bold"
Width="80"
Height="{Binding ElementName=Box1,Path=Width}"/>
<TextBlock FontSize="18" Text="Simple"
FontWeight="Bold"
Width="80"
Height="{Binding RelativeSource={RelativeSource Mode=self},Path=Width}"/>
//代码形式
Binding myBinding = new Binding("Height");
myBinding.RelativeSource = new RelativeSource(RelativeSourceMode.Self);
this.Box1.SetBinding(Height.HeightProperty, myBinding);
(2)Mode FindAncestor 关联父级容器
FindAncestor模式绑定当前元素的父容器中的上级元素,需搭配AncestorType和AncestorLevel使用,其规则如下:
- AncestorLevel:指的是以Bingding目标控件为起点的向上层级偏移量,比如这里S1的偏移量是1,G2的偏移量是2,G1是偏移量3;
- AncestorType:指的是要找的目标对象的类型。
- 注意:AncestorLevel参考AncestorType为基础生效。比如这里设置了AncestorType={x:Type Grid},Bingding在寻找时会忽略非Grid的控件,则此时G2的偏移量是1,G1的偏移量是2,StackPanel被忽略。
<Window x:Class="RelativeSource.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid Name="G1">
<Grid Name="G2">
<StackPanel Name="S1">
<TextBox Height="30" Width="60"
Name="Box1"
Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Grid}, AncestorLevel=2},Path=Name }"/>
StackPanel>
Grid>
Grid>
Window>
Binding myBinding = new Binding();
// Returns the second ItemsControl encountered on the upward path
// starting at the target element of the binding
myBinding.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(ItemsControl), 2);
(3)Mode TemplatedParent 关联模板
此 Mode 用于将ControlTemplate模板中的控件属性关联绑定到应用模板的控件属性上,应用给定控件的属性到它的控件模板,类似于TemplateBinding。{TemplateBinding X}只是编写{Binding X, RelativeSource={RelativeSource TemplatedParent}}的快捷方式,二者基本等价。
<Window.Resources>
<Style TargetType="{x:Type Button}">
"Background" Value="Green"/>
"Template" >
"{x:Type Button}">
"{Binding Path=Background.Color,RelativeSource={RelativeSource TemplatedParent}}"/>
Style>
Window.Resources>
TemplateBinding是Binding的一个轻量级版本,它简化了成熟版本Binding的很多功能。TemplateBinding最主要的用途是内置在模板中绑定模板目标控件或者模板化控件的属性,在这种情况下,比起成熟Binding效率要高得多。比如 ControlTemplate 里面的控件可以使用TemplateBinding将自己的某个属性值关联到模板控件的某个属性值上,必要的时候还可以添加Converter。Template的特点如下:
- TemplateBinding的数据绑定是单向的,只从数据源到目标(即从应用Template的控件上到Template内的控件元素上)
- TemplateBinding不能对数据对象进行自动转换,数据源和目标的数据类型若不同,需要自己写转换器
<TextBlock Text="{TemplateBinding MyText}"/>
<TextBlock Text="{Binding Path=MyText, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"/>
(1)TemplateBinding是在编译时评估的,而RelativeSource TemplatedParent是在运行时评估的。所以模板Template中的触发器是在运行中进行判断的,属性绑定应该使用TemplatedParent的形式,使用TemplateBinding则会报错。
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ButtonEx}">
<Border x:Name="border" Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
SnapsToDevicePixels="True">
<TextBlock x:Name="txt" Text="{TemplateBinding Content}" Foreground="{TemplateBinding Foreground}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="{Binding MouseOverBackground,RelativeSource= {RelativeSource Mode=TemplatedParent}}"/>
<Setter TargetName="txt" Property="Foreground" Value="{Binding MouseOverForeground,RelativeSource= {RelativeSource Mode=TemplatedParent}}"/>
<Setter TargetName="border" Property="BorderBrush" Value="{Binding MouseOverBorderbrush,RelativeSource= {RelativeSource Mode=TemplatedParent}}"/>
Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Background" Value="{Binding MousePressedBackground,RelativeSource={RelativeSource TemplatedParent}}"/>
<Setter TargetName="txt" Property="Foreground" Value="{Binding MousePressedForeground,RelativeSource={RelativeSource TemplatedParent}}"/>
Trigger>
ControlTemplate.Triggers>
ControlTemplate>
Setter.Value>
Setter>
(2)TemplateBinding的数据绑定是单向的,从数据源到目标(即从应用Template的控件到Template)。Binding的数据绑定方式是能够经过Mode设置的,可单向、双向等。
(3)TemplateBinding不能对数据对象进行自动转换,数据源和目标的数据类型不同时候,需要自己写转换器。Binding会对数据源和目标的数据类型进行自动转换
(4)TemplateBinding速度更快(编译时求值)
如果想让作为Binding源的对象具有自动通知 Binding 自己的属性值已经变化的能力,达到立即更新UI的效果。那么就需要让数据源实现 变更通知的接口 ,并在变更中激发Changed事件。当为Binding 设置了数据源后,Binding就会自动侦听来自这个接口的Changed事件。
(1)普通对象:自定义类需要实现INotifyPropertyChanged接口,并在属性的 set 语句中激发PropertyChanged事件,来通知绑定通道Binding以及UI更新属性数据显示。
public class User : INotifyPropertyChanged
{
private string name;
public string Name {
get { return this.name; }
set
{
if(this.name != value)
{
this.name = value;
this.NotifyPropertyChanged("Name");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if(this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
(2)列表集合:WPF提供了一种动态集合列表 ObservableCollection ,该集合类实现了INotifyCollectionChanged,INotifyPropertyChanged两个接口,可以在列表发生变更时,自动来通知绑定通道Binding以及UI更新数据显示。在使用上,ObservableCollection与List没有什么区别。也可以自己实现一个全面的模板通知类
namespace ClientNew.ViewModel
{
public class ViewModelBase : INotifyPropertyChanged, INotifyCollectionChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
public void RaiseCollectionChanged(ICollection collection)
{
if (CollectionChanged != null)
{
CollectionChanged(collection, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
public void RaiseCollectionAdd(ICollection collection, object item)
{
if (CollectionChanged != null)
{
if (PropertyChanged != null)
{
PropertyChanged(collection, new PropertyChangedEventArgs("Count"));
PropertyChanged(collection, new PropertyChangedEventArgs("Item[]"));
}
CollectionChanged(collection, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
//CollectionChanged(collection, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
}
}
}
}
注意: 若集合内部的数据的属性发生改变,则集合数据项对象也必须要实现INotifyPropertyChanged接口才可以自动进行UI通知,更新数据元素绑定的条目容器xxxItem,否则只有集合列表的变更才会更新。
此处以Textbox的Binding通知为例。在实现数据Binding后,UI 元素与后端数据Binding,前端的数据改动可以实时更新到后端数据模型上;而后端的数据模型需要实现 INotifyPropertyChanged 接口才能向前端推送更新,接下来我们就来分析一下他们的工作原理。
(1)View -> ViewModel 通知
这里主要讲述,如果直接在前端文本框TextBox内直接修改数据,Binding是如何更新通知到后端数据模型上的(View->ViewModel)。
创建Binding对象。SetBinding时将控件的依赖属性和要Binding的数据做了一个关系,建立绑定表达式 CreateBindingExpression() ,返回一个BindingExpressionBase对象。BindingExpressionBase从字面意思可以理解为Binding的表达式,实际上就是存储Binding的值的关系(谁绑定了谁),将依赖属性和控件、绑定对象关联起来。
手动在Textbox中输入内容则会被控件中的OnPreviewTextInput事件捕捉到,最后由BindingExpressionBase.OnPreviewTextInput触发Drity方法。Drity方法会检测是否有数据改动,若没有改动则退出更新机制。如果在绑定表达式中用了Delay属性,则会触发BindingExpressionBase中的DispatcherTimer来达到数据延迟更新的效果。
若监测到数据改动,则接下来会调用 BindingExpressionBase.UpdateValue() 方法,访问依赖属性Text的内容并去修改绑定在ViewModel的属性,主要包括几个部分:
// transfer a value from target to source
internal bool UpdateValue()
{
ValidationError oldValidationError = BaseValidationError;
if (StatusInternal == BindingStatusInternal.UpdateSourceError)
SetStatus(BindingStatusInternal.Active);
//获取依赖属性数值
object value = GetRawProposedValue();
if (!Validate(value, ValidationStep.RawProposedValue))
return false;
//转换器转换
value = ConvertProposedValue(value);
if (!Validate(value, ValidationStep.ConvertedProposedValue))
return false;
//最终触发更新
value = UpdateSource(value);
if (!Validate(value, ValidationStep.UpdatedValue))
return false;
value = CommitSource(value);
if (!Validate(value, ValidationStep.CommittedValue))
return false;
if (BaseValidationError == oldValidationError)
{
// the binding is now valid - remove the old error
UpdateValidationError(null);
}
EndSourceUpdate();
NotifyCommitManager();
return !HasValue(Feature.ValidationError);
}
最终由依赖属性中PropertyMetadata注册的PropertyChangedCallback来落实值的修改。看到这里大家应该会明白设计者为什么不把ViewModel的每个字段默认集成数据通知机制,我个人的理解是数据通知会带来一定的性能损耗所以开放给开发者“按需”添加通知的成员。
(2)ViewModel -> View 通知
后端实现INotifyPropertyChanged接口通知前端推送更新的本质还是传统的Event 发布-订阅的模型,但是我们好像没有看到事件发布订阅流程的声明代码,那么Binding是如何实现事件的订阅?
// System.ComponentModel.PropertyChangedEventManager
protected override void StartListening(object source)
{
INotifyPropertyChanged notifyPropertyChanged = (INotifyPropertyChanged)source;
notifyPropertyChanged.PropertyChanged += OnPropertyChanged;
}
其实在初始化Binding数据源时,创建BindingExpression在调用UpdateTarget时最终会调用PropertyChangedEventManager的StartListening方法订阅INotifyPropertyChanged接口实现类的PropertyChanged事件,至此两个绑定的属性产生了联系。
在Binding数据源时会开始监听StartListening(),根据传入的 DataContext数据源类对象,从上层引用中拿到ViewModel的引用(引用会逐层从Binding类的层面逐层传递进来)然后会判断这个ViewModel是否继承了INotifyPropertyChanged;如果继承了,则找到其 event PropertyChangedEventHandler PropertyChanged 的引用并Add进行管理,添加订阅事件。后续在Set属性时触发事件,就会调用OnPropertyChanged方法实时更改界面值。
有时UI显示的信息需要由多个数据源来共同决定,这时我们就需要使用多路绑定。多路绑定 MultiBinding 类和 Binding 一样均继承自BindingBase类,这也就是说所有使用 Binding 的场景均可以使用 MultiBinding 来代替。MultiBinding 对象常与转换器一起使用,它会根据这些绑定的值为绑定目标生成最终值,但该转换器类需继承 IMultiValueConverter 接口。
MultiBinding 属性:
- Bindings: 获取此 MultiBinding 实例中的 Binding 对象的集合。Collection 通过该属性 MultiBinding 将一组 Binding 对象聚合起来
- Converter: 获取或设置用于在源值和目标值之间来回转换的转换器。
- Mode: 获取或设置一个值,该值指示此绑定的数据流的方向。
- StringFormat: 获取或设置一个字符串,该字符串指定如果绑定值显示为字符串,应如何设置该绑定的格式。
(1)MultiBinding XAML实现
需求:
- 第1、2个 TextBox 输入用户名,要求内容一致;
- 第3、4个 TextBox 输入邮箱,要求内容一致;
- 当满足以上两个条件且内容均不为空的时候,Button 显示可用。点击显示成功提示
namespace WPF_Demo.Convert
{
class MultiInfoConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
//values存储了绑定值的集合(按照绑定顺序传入)
if(values.Length >= 4)
{
string username_1 = values[0].ToString();
string username_2 = values[1].ToString();
string email_1 = values[2].ToString();
string email_2 = values[3].ToString();
if (!string.IsNullOrEmpty(username_1) && !string.IsNullOrEmpty(username_2) && !string.IsNullOrEmpty(email_1) && !string.IsNullOrEmpty(email_2))
{
if (username_1 == username_2 && email_1 == email_2)
{
return true;
}
}
}
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
<Window x:Class="WPF_Demo.MainWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPF_Demo"
xmlns:vm="clr-namespace:WPF_Demo.Status"
xmlns:cv="clr-namespace:WPF_Demo.Convert"
xmlns:control="clr-namespace:WPF_Demo.Style"
mc:Ignorable="d"
DataContext="{x:Static vm:MainWindowStatus.Instance}"
Title="MainWindow" Height="185" Width="300">
<Window.Resources>
<cv:MultiInfoConverter x:Key="mfCV"/>
Window.Resources>
<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.IsEnabled>
<MultiBinding Mode="OneWay" Converter="{StaticResource mfCV}">
<Binding ElementName="textBox1" Path="Text"/>
<Binding ElementName="textBox2" Path="Text"/>
<Binding ElementName="textBox3" Path="Text"/>
<Binding ElementName="textBox4" Path="Text"/>
MultiBinding>
Button.IsEnabled>
Button>
StackPanel>
Window>
(2)MultiBinding C#实现
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//准备基础Binding
Binding Binding1 = new Binding("Text") { Source = this.textBox1 };
Binding Binding2 = new Binding("Text") { Source = this.textBox2 };
Binding Binding3 = new Binding("Text") { Source = this.textBox3 };
Binding Binding4 = new Binding("Text") { Source = this.textBox4 };
//准备MultiBinding(子Binding添加顺序敏感)
MultiBinding multiBinding = new MultiBinding() { Mode = BindingMode.OneWay };
multiBinding.Bindings.Add(Binding1);
multiBinding.Bindings.Add(Binding2);
multiBinding.Bindings.Add(Binding3);
multiBinding.Bindings.Add(Binding4);
multiBinding.Converter = new MultiInfoConverter();//添加转换器
//关联控件与MultiBinding对象
this.button1.SetBinding( Button.IsEnabledProperty, multiBinding);
}
}