绑定表述的是一种关系,通过某种关系将多个事物联系在一起。
WPF中绑定的完成需要两个角色:
Target
:界面对象中需要进行数据交互的依赖属性(非依赖属性无法绑定)。Source
:也就是需要与界面做数据交互的数据对象。绑定表达式的作用就是将目标对象与源对象建立关联,通过绑定可以将界面与数据逻辑进行隔离。
常规绑定表达式:{Binding Source=sourceName, Path=dataPath}
数据源类型
WPF中对数据源的类型有做规定,具体有以下类型可以做为数据源(瞄一眼就行了,不要记):
INotifyPropertyChanged
(MVVM开发中的基础)DataTable
、DataView[0] →转换成List
。XmlDataProvider
做为数据源。ObjectDataProvider
做为数据源。Linq
结果做为数据源 :List
。绑定表达式中可以指定绑定的源对象是何种类型:
Source
:指定普通数据类型或集合类型做为数据源。
1.1、静态资源
值类型
Path
的用法,.
表示直接将当前的源对象作为Text
属性的值,这种情况下Path
是可以省略的。<Window ......
xmlns:sys="clr-namespace:System;assembly=mscorlib"
......>
<Window.Resources>
<sys:String x:Key="str">test</sys:String>
</Window.Resources>
<Grid>
<StackPanel>
<TextBlock Text="{Binding Source={StaticResource str},Path=.}"/>
</StackPanel>
</Grid>
</Window>
类型对象中的某个属性
<Window.Resources>
<local:Person x:Key="person" PName="Schuyler"/>
</Window.Resources>
<Grid>
<StackPanel>
<TextBlock Text="{Binding Source={StaticResource person},Path=PName}"/>
</StackPanel>
</Grid>
集合类型对象
<Window.Resources>
<x:Array x:Key="arr" Type="sys:String">
<sys:String>schuyler1</sys:String>
<sys:String>schuyler2</sys:String>
<sys:String>schuyler3</sys:String>
<sys:String>schuyler4</sys:String>
</x:Array>
</Window.Resources>
<Grid>
<StackPanel>
<TextBlock Text="{Binding Source={StaticResource arr},Path=[1]}"/>
</StackPanel>
</Grid>
1.2、XmlDataProvider
有些情况下我们需要将xml文件中的数据作为数据源,此时就需要将xml文档作为项目的文件资源,然后使用WPF的XmlDataProvider
元素来加载xml资源文件后作为数据源了。
<?xml version="1.0" encoding="iso-8859-1"?>
<breakfast_menu>
<food attr="a1" prop="p1">
<name>Belgian Waffles</name>
<price>$5.95</price>
</food>
<food attr="a2">
<name>Strawberry Belgian Waffles</name>
</food>
</breakfast_menu>
<Window.Resources>
<XmlDataProvider Source="/WpfApp2;component/simple.xml" x:Key="xmlResource"/>
</Window.Resources>
<Grid>
<StackPanel>
<TextBlock Text="{Binding Source={StaticResource xmlResource},XPath=breakfast_menu/food[1]/name}"/>
</StackPanel>
</Grid>
注意查看上面的代码,使用了XPath
,XPath
是针对XmlDataProvider
数据源而准备的。
XPath=.
(不能省略)。XPath=node/childNode
[num]
,这里要注意下标是从1开始的,XPath=node/childNode[1]
。@
符号,XPath=node/childNode[1]/@propertyName
。XML文件中存在多个相同类型的节点,XmlDataProvider
可以将同类节点提取成集合来作为数据源
<Window.Resources>
<XmlDataProvider Source="/WpfApp2;component/simple.xml" x:Key="xmlResource"/>
</Window.Resources>
<Grid>
<StackPanel>
<ListView ItemsSource="{Binding Source={StaticResource xmlResource},XPath=breakfast_menu/food}"/>
</StackPanel>
</Grid>
1.3、ObjectDataProvider
ObjectDataProvider
是WPF提供的用于在XAML中调用C#函数的元素,具体用法如下:
定义要使用的函数
string
。public class MethodClass
{
public string Calculate(string a)
{
return (int.Parse(a)/2).ToString();
}
}
在XAML中通过ObjectDataProvider
元素加载方法,并作为数据源进行绑定。
<Window ......
xmlns:sys="clr-namespace:System;assembly=mscorlib"
......>
<Window.Resources>
<ObjectDataProvider x:Key="calculate" MethodName="Calculate" ObjectType="{x:Type local:MethodClass}">
<ObjectDataProvider.MethodParameters>
<sys:String>200</sys:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
<Grid>
<StackPanel>
<TextBlock Text="{Binding Source={StaticResource calculate}}"/>
<TextBox Text="{Binding Source={StaticResource calculate},
Path=MethodParameters[0],
UpdateSourceTrigger=PropertyChanged,
BindsDirectlyToSource=True}"/>
</StackPanel>
</Grid>
</Window>
ElementName
:根据xaml中的元素名称,将该元素中的属性作为数据源进行绑定,也就是将依赖属性作为数据源。
<Grid>
<StackPanel VerticalAlignment="Center">
<Button Content="第一个按钮" x:Name="FirstButton" Width="200" Height="80"/>
<Button Content="{Binding ElementName=FirstButton,Path=Content}"
Width="{Binding ElementName=FirstButton,Path=Width}" Height="{Binding ElementName=FirstButton,Path=Height}"/>
</StackPanel>
</Grid>
当使用静态属性来作为数据源时,做法与实例资源有所不同。
上文中为了方便,在XAML中使用了静态资源的方式来举例,然而XAML中的每一个元素都是一个新建的实例对象,如果想要使用某个类型中的静态属性作为数据源,显然从新建的实例对象中获取是不合理的。WPF为此提供了一个较为简便的做法,具体如下:
{Binding Path=(local:StaticClass.StaticValue)}
:静态属性的绑定,直接使用Path
即可,Path
中使用()
是为了告诉WPF这个是静态属性,不需要在XAML中逐层往上寻找该属性。
<Grid>
<!--假设项目已经创建了一个StaticClass类型,并在类型中定义了一个静态属性,StaticValue-->
<TextBlock Text="{Binding Path=(local:StaticClass.StaticValue)}"/>
</Grid>
<Grid>
<TextBlock Text="{Binding}"/>
</Grid>
上面代码,可以看到在绑定表达式中并没有指定数据源,但是程序并没有报错,可以正常启动运行。这个过程中,TextBlock
元素的Text
属性到底以什么作为数据源呢?实际上,当没有指定数据源时,绑定表达式会从当前元素(也就是当前控件对象中)开始逐层向上寻找DataContext
数据上下文属性对象,一旦找到,就将DataContext
对象作为数据源(默认情况下,DataContext
对象为null
)。
如果想直接拿DataContext
对象作为数据源(例如值类型)绑定表达式应该写成{Binding Path=.}
又因为Path=.
时可以省略因此就变成了{Binding}
。
如果想使用DataContext
对象中的某个属性成员作为数据源,可以将“Path=”
省略,直接写成{Binding PropertyName}
。
DataContext
后,会在对象中寻找指定的PropertyName
的实行,找不到则会继续向上层寻找DataContext
中的属性。如果想使用DataContext
对象中的某个属性成员中的属性作为数据源,直接通过.
来链接,即{Binding PropertyName.Property}
。
ItemsControl
对于ItemsControl
以及继承了ItemsControl
类型的控件,其ItemsSource
属性在进行数据绑定时,如果没有指定数据源,会正常的向上寻找DataContext
。需要注意的是,这类集合控件的数据子控件在做绑定时,如果没有指定数据源,默认绑定的是ItemsSource
所绑定的子项对象的属性。
如下面的例子,Persons
集合中包含了许多个Person
对象,ItemsControl
控件的子项使用绑定时指定的属性,默认是子项对应的Person
对象中的属性。
<ItemsControl ItemsSource="{Binding Persons}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Age}" Grid.Column="1"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
DataContext的定义方式
相同层级中,DataContext
对象只能存在一个,常见的有以下几种:
在后台代码中进行定义:
public MainWindow()
{
InitializeComponent();
DataContext = new DataClass();
}
在xaml的窗体中进行定义
<Window ......>
<Window.DataContext>
<local:DataClass/>
</Window.DataContext>
......
</Window>
在元素中进行定义(内联)
<Grid>
<TextBlock DataContext="123" Text="{Binding}"/>
</Grid>
WPF的绑定表达式中,除了可以使用Source
、ElementName
指定数据源对象之外,还以使用相对源RelativeSource
来指定。
语法
{Binding Path=property, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}
RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}
的意思是,寻找当前控件的类型(Type
)为Window
的祖先对象,最终会找到当前窗体对象。RelativeSource
中,AncestorType
属性默认配合FindAncestor
模式使用的,所以如果指定了AncestorType
,Mode
属性可以省略。需要关注的是Mode
属性,共有四种相对源模式:
FindAncestor
:寻找指定类型的祖先作为数据源,配合AncestorType
使用。PreviousData
:当前数据的上一个数据作为数据源,一般用于集合控件中,寻找自己的哥。TemplatedParent
:父类模板对象作为数据源,可以参考TemplateBinding
的用法。例如:{Binding Tag, RelativeSource={RelativeSource Mode=TemplatedParent}}
Self
:将自己作为数据源。例:{Binding Path=Width, RelativeSource={RelativeSource Mode=Self}}
public class World
{
public Person PersonOne { get; set; } = new Person() { Name="123123", Age=22 };
public Person PersonTwo { get; set; } = new Person() { Name="333333", Age=21 };
public List<Person> Persons { get; set; } = new List<Person>()
{
new Person(){Name="aaa", Age=1},
new Person(){Name="bbb", Age=2},
new Person(){Name="ccc", Age=3}
};
}
<Window.DataContext>
<local:World/>
</Window.DataContext>
<Grid>
<ItemsControl ItemsSource="{Binding Persons}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Path=DataContext.PersonOne.Name , RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"/>
<TextBlock Text="{Binding Age}" Grid.Column="1"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
上面的例子中,ItemsControl
的子项中,第一个TextBlock
所绑定的数据为当前窗体对象中的DataContext.PersonOne.Name
(注意,此时DataContext
显式调用)。第二个TextBlock
的绑定的则是ItemsControl
的数据源ItemsSource
所绑定的数据集合的子项的Age
属性。
在实际开发过程中,这种关系绑定会有个缺点,当向上查找指定类型的控件时,有可能因为上层由多个相同类型的控件,导致找错控件对象,因此如果可以尽量在指定控件上面定义Name
属性,然后通过ElementName
来进行数据源的指定。
绑定表达式中除了可以指定数据源对象(Source
、ElementName
、RelativeSource
以及隐性的DataContext
)、路径(Path
、XPath
)这两个比较核心的属性外,还可以设定一些辅助特性。
语法:{Binding Source={...}, Path=..., Mode=ModeType}
TwoWay
:双向绑定。OneWay
:单向绑定,数据只能从数据源到目标,目标数据的改变不会影响数据源。OneTime
:目标只从数据源从接收一次数据,也就是只接收一次数据源的初始值。OneWayToSource
:单向绑定,数据只能从目标到数据源,数据源的修改不会影响目标,但目标的修改会改变数据源。Default
:根据依赖属性定义时的默认方式,表达式中设定的绑定模式,优先级要高于依赖属性定义时设置的绑定模式。有一点需要注意的是,OneWay
模式下,如果在C#代码中直接对控件对象的对应依赖属性进行赋值,会直接切断绑定关系,相当于将控件中的绑定表达式去除。
进行数据绑定后,有些控件(例如TextBox
)的数据发生变化时,默认情况下是在控件失去焦点时候触发变化通知,从而修改数据源的数据的。如果希望改变触发时机,可以在绑定表达式中,通过UpdateSourceTrigger
属性进行设置。
语法:{Binding ......, UpdateSourceTrigger=TriggerType}
Default
:使用依赖属性定义时设置的触发方式。
PropertyChanged
:数据一旦发生变化,就触发。
LostFocus
:失去焦点时,触发。
Explicit
:控件数据变化时不进行触发,通过代码来进行触发。
<Window.DataContext>
<local:World/>
</Window.DataContext>
<StackPanel>
<TextBox Text="{Binding PersonOne.Name, UpdateSourceTrigger=Explicit}" Name="tb"/>
<TextBox Text="{Binding PersonOne.Name}"/>
<Button Click="Button_Click" Content="trigger"/>
</StackPanel>
private void Button_Click(object sender, RoutedEventArgs e)
{
BindingExpression bindingExpression = tb.GetBindingExpression(TextBox.TextProperty);
bindingExpression.UpdateSource();//触发更新
}
有些情况下,在使用绑定表达式进行数据绑定的时,会出现数据源与目标的数据类型不一致的情况,或者说希望对绑定的数据源进行数据的判断、校验,最后返回所需要的数据。这个时候就要用到绑定表达式中的Converter
属性了。
在使用Converter属性时,可以在绑定表达式中使用ConverterParameter
属性给转换器传递参数。
语法:{Binding ……, ConverterParameter=parameter, Converter=convertor}
实现IValueConverter
使用Converter
属性需要先创建一个实现IValueConverter
接口的转换器类型,该接口有两个需要实现的函数。
object Convert(object value, Type targetType, object parameter, CultureInfo culture)
:实现从数据源到目标的数据转换,最后返回将要给目标的值。
value
:数据源的值。targetType
:目标属性的类型。parameter
:绑定函数中通过ConverterParameter
属性传给转换器的参数。culture
:本地化信息(日期格式、文字等本地化信息)。object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
:实现从目标到数据源的数据转换,最后返回将要给数据源的值。
示例
public class Int2BoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value.ToString() == parameter.ToString())
return true;
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return parameter;
}
}
xaml中使用
注意,绑定表达式的转换器可以作为xaml的静态资源进行定义。
<Window ......>
<Window.DataContext>
<local:Data/>
</Window.DataContext>
<Window.Resources>
<local:DataConverter x:Key="dataConverter"/>
</Window.Resources>
<StackPanel>
<RadioButton Content="男" IsChecked="{Binding Gender, ConverterParameter=1, Converter={StaticResource dataConverter}}"/>
<RadioButton Content="女" IsChecked="{Binding Gender, ConverterParameter=2, Converter={StaticResource dataConverter}}"/>
</StackPanel>
</......>
除了自己可以自定义转换器外, WPF为了便于开发者进行开发,提供一些较为常用的转换器,例如BooleanToVisibilityConverter
转换器,根据绑定的数据自动转换为Visibility
的属性值,在xmal中直接使用即可。
<Window.Resources>
<!--内置转化器-->
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
......
</Window.Resources>
此外,转换器的功能是很强大的,它不仅限于对数据的转换,还可以根据绑定的数据返回对应的控件对象,从而实现动态的控件修改。
ContentControl
控件,让Content
属性绑定ViewModel上的某个字符串属性,然后利用转换器进行反射,将对应名称的控件对象创建并返回。绑定表达式中,如果需要对绑定数据进行字符串的格式化,需要使用StringFormat
属性。
Content
属性是无法直接进行字符串格式化的,因为Context
属性的类型是object
而不是一个字符串。语法:{Binding ……, StringFormat=format}
常用表达式
货币:
StringFormat={}{0:C}
:美元(默认)。Text="{Binding Value,StringFormat={}{0:C3},ConverterCulture=zh-CN}"
:中文¥格式。C3
表示后面保留三位小数。数字:
StringFormat={}{0:D10}
:固定10位,不够补零。StringFormat={}{0:F2}
:保留2位小数 。StringFormat={}{0:N3}
:千分位加3位小数,N0
表示无小数的整数。StringFormat={}{0:P1}
:百分比加1位小数StringFormat={}{0:0000.00}
:整数为4位、小数位2位。StringFormat={}{0:#.00}
:整数部分不限制长度,小数保留两位。时间:
StringFormat={}{0:yyyy-MM-dd HH:mm:ss}
或StringFormat=时间:{0:yyyy-MM-dd HH:mm:ss}
ConverterCulture
设置文化格式,例如使用中国的日期展示形式,可以设置为:{Binding Now, StringFormat=dddd,ConverterCulture=Zh}
,其中dddd
表示展示星期几。保留原句,在前后加内容:
StringFormat=前面{0}后面
表达式前缀说明
WPF中StringFormat
的格式化表达式中的0类似与C#中字符串格式化时的占位符,WPF要求0之前必须要有值,因此需要在表达式之前添加一些字符,如果表达式之前不希望添加字符,则可以在表达式之前使用{}
。
public class Data
{
public DateTime DateTime { get; set; } = DateTime.Now;
}
<Window.DataContext>
<local:Data/>
</Window.DataContext>
<StackPanel>
<TextBlock Text="{Binding DateTime,StringFormat=时间:{0:yyyy-MM-dd hh:mm:ss}}"/>
</StackPanel>
当我们在绑定表达式中使用了UpdateSourceTrigger=PropertyChanged
属性,则目标数据发生变动时会实时触发数据源的变化。这样在一些检索框的使用上会很不友好,每次输入字符都会去进行检索。此时可以在绑定表达式中使用Delay
属性,可以指定当目标数据发生变化后多少毫秒内没有再次发生变法时才触发数据源的变化。
语法:{Binding ……, Delay=200}
FallbackValue
:指定当获取不到绑定的数据时(例如Path
写错时),用什么值来代替。
<TextBlock Text="{Binding DateTimea,FallbackValue=ok}"/>
TargetNullValue
:指定当获取到的绑定数据为null时,用什么值代替。
<TextBlock Text="{Binding DateTime,TargetNullValue=ok}"/>
在实际项目中,很多情况下需要对用户的录入信息进行校验并给出提示,注册用户的表单就是个典型的案例。
在WPF中,对于数据的校验有很多种方式,这里将几种比较常用的方式进行记录。
在进行依赖属性的定义时,是可以设置校验回调函数的,当校验回调函数返回false
时,会在后台(Debug控制台)中打出异常信息,那么如何将异常信息显式在界面上呢?可以通过在绑定表达式中使用ValidationRules
属性来进行校验异常捕获。
定义依赖属性
public class Data : DependencyObject
{
public int Value
{
get { return (int)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(int), typeof(Data), new PropertyMetadata(default), new ValidateValueCallback(ValidateCallBack));
private static bool ValidateCallBack(object value)
{
if (int.Parse(value.ToString()) == 123)
return false;
return true;
}
}
异常捕获
要对获取依赖属性抛出来的异常,首先需要在使用依赖属性的绑定表达式中,使用ValidationRules
来开启校验异常捕获。
开启之后,通过静态属性Validation.Errors
可以获得控件对象中所捕获到的所有校验异常集合,再通过集合中元素对象的ErrorContent
属性就能获得校验异常中的异常信息。
<Window.DataContext>
<local:Data/>
Window.DataContext>
<StackPanel>
<TextBox Name="tb">
<TextBox.Text>
<Binding Path="Value"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<ExceptionValidationRule/>
Binding.ValidationRules>
Binding>
TextBox.Text>
TextBox>
<TextBlock Text="{Binding Path=(Validation.Errors)[0].ErrorContent,ElementName=tb}"/>
StackPanel>
C#代码取得异常信息的做法如下
if (Validation.GetHasError(this.tb))
{
var errors = Validation.GetErrors(this.tb);
var errorContent = Validation.GetErrors(this.tb)[0].ErrorContent;
}
如果绑定数据时,数据源是一个普通的属性而不是依赖属性,有没有办法对其进行校验呢?答案是肯定的,只需要在属性的设置逻辑中进行校验并抛出Exception
异常就可以了,开启捕获的方式则是与依赖属性一样。
这种做法虽然方便,但是Exception
的频繁抛出会带来一些性能上的损耗。
定义属性
public class Data
{
private string _value = "0";
public string Value
{
get { return _value; }
set
{
_value = value;
if (value == "222")
throw new Exception("不能为222~~~[Exception]");
}
}
}
异常捕获
<Window.DataContext>
<local:Data/>
Window.DataContext>
<StackPanel>
<TextBox Name="tb">
<TextBox.Text>
<Binding Path="Value"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<ExceptionValidationRule/>
Binding.ValidationRules>
Binding>
TextBox.Text>
TextBox>
<TextBlock Text="{Binding Path=(Validation.Errors)[0].ErrorContent,ElementName=tb}"/>
StackPanel>
如果不希望再普通属性中做过校验逻辑,或者不想使用依赖属性定义时所设置的校验异常,可以通过捕获自定义的校验规则来实现同样的效果。
需要注意的是,如果普通属性的设置逻辑中做了抛出异常的校验然后使用了捕获自定义校验规则,则两则都会触发。
如果是依赖属性中设置了校验回调函数,然后使用了捕获自定义校验规则,只会触发自定义校验规则。
创建校验规则类
创建校验规则类需要实现ValidationRule
类型并重写Validate
函数。
public class MyValidtionRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (value.ToString() == "333")
return new ValidationResult(false, "输入值不能是333");
return new ValidationResult(true, "");
}
}
public class Data :DependencyObject
{
public int Value { get; set; } = 0;
}
捕获异常
<Window.DataContext>
<local:Data/>
Window.DataContext>
<StackPanel>
<TextBox Name="tb">
<TextBox.Text>
<Binding Path="Value"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:MyValidtionRule/>
Binding.ValidationRules>
Binding>
TextBox.Text>
TextBox>
<TextBlock Text="{Binding Path=(Validation.Errors)[0].ErrorContent,ElementName=tb}"/>
StackPanel>
除了上文中所说的捕获校验异常的方式外,还可以通过在表达式中使用ValidatesOnDataErrors
属性,来开启数据异常校验,其实底层与捕获效果是一样的,但是实现的方式不同。
这种方式面对大量数据校验的时候比较友好,推荐使用。
创建自定义特性
public class NotValueAttribute : Attribute
{
public string Value { get; set; }
public NotValueAttribute(string value)
{
this.Value = value;
}
}
创建数据异常信息类型
要实现这种方式首先要创建数据异常信息类型,类中定义了所有需要进行校验的属性。
首先要实现IDataErrorInfo
接口并实现。
public class ValidateProperty : IDataErrorInfo
{
//接口实现,一般用不到
public string Error = null;
//接口实现,索引器,用来返回异常信息,索引器中使用了反射,通过索引名称来获取对应的属性对象
public string this[string columnName]
{
get
{
var pi = GetType().GetProperty(columnName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (pi.IsDefined(typeof(NotValueAttribute), false))
{
NotValueAttribute attr = (NotValueAttribute)pi.GetCustomAttributes(typeof(NotValueAttribute), false)[0];
if (pi.GetValue(this) == null)
{
return "字段不能为空";
}
else if (pi.GetValue(this).ToString() == attr.Value)
{
return "字段不能为" + attr.Value;
}
}
return "";
}
}
}
进行校验设置
在需要进行校验的类上继承上面创建的数据异常信息类型,并对需要校验的属性通过上面所创建的自定义特性来进行校验设置。
public class User: ValidateProperty
{
private string _value;
[NotValue("22222")]//这个是自定义的特性
public string Value
{
get { return _value; }
set { _value = value;}
}
private int myVar;
[NotValue("33333")]//这个是自定义的特性
public int MyProperty
{
get { return myVar; }
set { myVar = value; }
}
}
开启数据异常校验
<Window.DataContext>
<local:User/>
Window.DataContext>
<StackPanel>
<TextBox Text="{Binding Value,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}" Name="tb"/>
<TextBlock Text="{Binding Path=(Validation.Errors)[0].ErrorContent,ElementName=tb}"/>
StackPanel>
代码
public class Data : IDataErrorInfo
{
public string this[string columnName]
{
get
{
var pi = this.GetType().GetProperty(columnName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (pi.IsDefined(typeof(RequiredAttribute), false)
&&
(pi.GetValue(this) == null || string.IsNullOrEmpty(pi.GetValue(this).ToString()))
)
return "字段为空了~~~[IDataErrorInfo]";
return "";
}
}
public string Error => null;
private string _projectName = "新建项目";
[Required]
public string ProjectName
{
get { return _projectName; }
set
{
_projectName = value;
}
}
private string _solutionName = "新建项目";
[Required]
public string SolutionName
{
get { return _solutionName; }
set { _solutionName = value; }
}
}
<Window x:Class="WpfApp4.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:WpfApp4"
mc:Ignorable="d" Foreground="#000" WindowStartupLocation="CenterOwner"
FontFamily="Microsoft YaHei" FontWeight="ExtraLight"
SizeToContent="Height"
Title="MainWindow" MinHeight="550" Width="800">
<Window.DataContext>
<local:Data/>
</Window.DataContext>
<Window.Resources>
<Style TargetType="TextBlock" >
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="FontSize" Value="12"/>
</Style>
<ControlTemplate x:Key="TextBoxErrorTemplate">
<AdornedElementPlaceholder/>
</ControlTemplate>
<Style TargetType="TextBox">
<Setter Property="Margin" Value="0,5"/>
<Setter Property="Width" Value="500"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition MinHeight="30"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
<ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"
VerticalContentAlignment="Center" BorderThickness="0"/>
</Border>
<!--这里调用静态属性Validation.Errors来获取校验异常时,需要指定校验数据所在的对象,即TextBox-->
<TextBlock Text="{Binding Path=(Validation.Errors)[0].ErrorContent,
RelativeSource={RelativeSource Mode=TemplatedParent},StringFormat=* {0}}"
Grid.Row="1" Foreground="Red" Margin="0,3" FontSize="12" Visibility="Collapsed"
Name="errorTxt"/>
</Grid>
<ControlTemplate.Triggers>
<!--触发器这里使用Validation.HasError属性来确认有无校验异常时,并没有指定数据源,这是因为调用这个触发器的对象就是TextBox-->
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="BorderBrush" Value="Red" TargetName="border"/>
<Setter Property="Visibility" Value="Visible" TargetName="errorTxt"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<!--每个TextBox都会有Validation.ErrorTemplate模板,下面的做法是将默认模板进行替换-->
<Setter Property="Validation.ErrorTemplate" Value="{StaticResource TextBoxErrorTemplate}"/>
</Style>
<Style TargetType="ComboBox">
<Setter Property="Height" Value="30"/>
<Setter Property="Margin" Value="0,5"/>
<Setter Property="Width" Value="500"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
<Style TargetType="Button">
<Setter Property="Height" Value="28"/>
<Setter Property="Width" Value="80"/>
<Setter Property="Margin" Value="5,0"/>
</Style>
</Window.Resources>
<Grid Margin="30">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
<RowDefinition Height="28"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="28"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="28"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="28"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="28"/>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<TextBlock Text="配置新项目" FontSize="22"/>
<TextBlock Text="WPF应用程序" Grid.Row="1" FontSize="16" Margin="0,15"/>
<TextBlock Text="项目名称" Grid.Row="2"/>
<TextBox Grid.Row="3" Text="{Binding ProjectName,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}"/>
<TextBlock Text="位置" Grid.Row="4"/>
<ComboBox Grid.Row="5"/>
<TextBlock Text="解决方案" Grid.Row="6"/>
<ComboBox Grid.Row="7"/>
<TextBlock Text="解决方案名称" Grid.Row="8"/>
<TextBox Grid.Row="9" Text="{Binding SolutionName,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}"/>
<CheckBox Content="将解决方案和项目放在同一个目录中" Grid.Row="10"
VerticalAlignment="Center"/>
<StackPanel Orientation="Horizontal" Grid.Row="12" HorizontalAlignment="Right">
<Button Content="上一步"/>
<Button Content="下一步"/>
</StackPanel>
</Grid>
</Window>
在进行数据绑定时,如果希望绑定多个数据,可以使用多重绑定表达式MultiBinding
来实现。
多重绑定由于绑定的数据有多个,所以要求必须对绑定的数据进行转换,而对数据进行转换的方式有两种。
可以通过字符串格式化的方式对多重绑定的数据进行转换。
public class Data
{
public string Name { get; set; } = "Schuyler";
public int Age { get; set; } = 22;
//多重绑定其实有多种方式可以替代,一般很少用到,例如新建一个属性,属性中做数据处理,xaml中则直接绑定这个属性
public string TheMulti {
get
{
return Name + ":" + Age;
}
}
}
<Window.DataContext>
<local:Data/>
Window.DataContext>
<Grid>
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}-{1}-{2}">
<Binding Path="Name"/>
<Binding Path="Age"/>
<Binding Path="WrongName" FallbackValue="ok"/>
MultiBinding>
TextBlock.Text>
TextBlock>
Grid>
使用绑定表达式的转换器也可以完成对数据的转换,首先要创建转化器类型,实现IMultiValueConverter
(单值转换器实现的是IValueConverter
)。
多值转换器的用法跟单值转换器的用法是一样的,只不过是接收的参数跟返回的类型根据多值情景做了改变。
public class ValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values[0].ToString() + values[1].ToString() + " - IMultiValueConverter";
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return new object[] { 1, 2, 3 };
}
}
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource ValueConverter}">
<Binding Path="Name"/>
<Binding Path="Age"/>
MultiBinding>
TextBlock.Text>
TextBlock>
其实除了上面两种方式外,直接用其他方式来替代多重绑定的做法更为常见。
<TextBlock>
<Run Text="{Binding Value1}"/><Run Text="{Binding Value2}"/>
TextBlock>
WPF中的数据绑定存在优先级的概念,需要使用PriorityBinding
表达式,其优先级是按照控件的顺序来决定的。
C#代码
public class Values
{
private int value1 = 1;
public int Value1
{
get {
Thread.Sleep(3000);
return value1;
}
set { value1 = value; }
}
private int value2 = 2;
public int Value2
{
get
{
Thread.Sleep(2000);
return value2;
}
set { value2 = value; }
}
private int value3 = 3;
public int Value3
{
get
{
Thread.Sleep(1000);
return value3;
}
set { value3 = value; }
}
}
<Window.DataContext>
<local:Values/>
Window.DataContext>
<Grid>
<TextBlock>
<TextBlock.Text>
<PriorityBinding FallbackValue="正在获取数据...">
<Binding Path="Value1" IsAsync="True"/>
<Binding Path="Value2" IsAsync="True"/>
<Binding Path="Value3" IsAsync="True"/>
PriorityBinding>
TextBlock.Text>
TextBlock>
Grid>
如上图所示,如果Value1、Value2没有值,或者值还没返回来之前,Value3的值先拿到,就先显示Value3,如果Value1的值拿到之前,先拿到Value2,则先显示Value2,等Value1的值拿到了,就会显示Value1。
Binding
表达式中的IsAsync
为异步绑定。
数据绑定
除了在Xaml中使用绑定表达式进行绑定外,还可以通过在代码中实现动态数据绑定。有些时候会需要在C#中进行控件的数据绑定,例如动态加载用户控件时,xaml中是无法直接操作用户控件元素属性的,此时可以通过在C#中去处理绑定关系,具体做法如下:
ViewModel
public class MainWindowViewModel
{
public string TextContent { get; set; } = "绑定数据";
}
View
<Window ......>
<Window.DataContext>
<local:MainWindowViewModel/>
Window.DataContext>
<Grid>
<Button Background="Yellow" Width="100" Height="80" Click="Button_Click"/>
Grid>
Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var button = sender as Button;
Binding binding = new Binding();
//binding.Source = XXX; 这里可以设置源数据对象,也可以不设置,相当于标签中的{Bindg TextContext}
binding.Path = new PropertyPath("TextContent");
BindingOperations.SetBinding(button, ContentControl.ContentProperty, binding);
//button.SetBinding(ContentControl.ContentProperty, binding); 也可以控件对象的用实例方法来绑定
}
}
关系绑定
如果需要设置相对关系,那么可以进行如下设置:
binding.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(Canvas), 1);
绑定移除
如果在OneWay
模式下,C#代码中直接对控件对象的对应依赖属性进行赋值,会直接切断绑定关系,相当于将控件中的绑定表达式去除。
如果是其他绑定模式下,C#代码中可以借助BindingOperations.ClearBinding(DependencyObject target, DependencyProperty dp)
方法来是实现。
在绑定时,当使用依赖属性作为数据源的时,由于依赖属性本身就具备变化通知的特性,因此在绑定后对数据源进行改变,目标值也会随之改变。
相比之下,如果使用普通属性作为数据源,由于普通属性不具备变化通知的特性,因此在绑定后对数据源进行改变,目标的值并不会随之该。
因此,如果希望实现双向绑定那么就需要对普通属性加入变化通知的处理,这也是MVVM模式得以实现的基础。
变化通知的实现比较简单,所在类实现INotifyPropertyChanged
接口的PropertyChangedEventHandler
事件,并在属性中调用事件即可。
public class Person: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _name;
public string PName {
get { return _name; }
set {
_name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("PName"));
}
}
}
静态属性的变化通知与实例属性有所不同,原因在于INotifyPropertyChanged
接口中的PropertyChanged
事件不是静态的,无法在静态属性逻辑中直接调用。因此,静态属性的变化通知需要自己定义变化通知事件,由于WPF对其事件对象有命名规范,所以可以分为两种方式。
Name+Changed方式(不推荐)
WPF对于属性变化通知的事件对象名称是有要求的,其中一种命名规范就是属性名称+Changed
。
public class StaticClass
{
private static int _value= 0;
public static int Value
{
get { return _value; }
set { _value = value;
//由于是静态,这里无法传入this,直接传null就可以了
ValueChanged?.Invoke(null, new PropertyChangedEventArgs("Value"));
}
}
//注意这里事件的名称要跟属性名称匹配
public static event EventHandler<PropertyChangedEventArgs> ValueChanged;
}
StaticPropertyChanged方式(推荐)
使用Name+Changed
命名方式来创建事件有一个弊端,就是如果有很多个静态属性需要进行变化通知的话,就必须为每一个静态属性创建各自的事件对象,很不便捷。针对此,WPF提供了一个通用的静态属性变化通知事件名StaticPropertyChanged
。
public class StaticClass
{
private static int _value= 0;
public static int Value
{
get { return _value; }
set { _value = value;
StaticPropertyChanged?.Invoke(null, new PropertyChangedEventArgs("Value"));
}
}
public static event EventHandler<PropertyChangedEventArgs> StaticPropertyChanged;
}
需要注意的是,在MVVM中,属性变化通知一般是在Model
层完成的。
如果每个需要进行属性变化通知的类型都要取继承并实现INotifyPropertyChanged
显然很冗余,可以实现一个基础类,定义一个通用方法从而减少重复代码。
实现通用方法
public class NotifyBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public void SetProperty<T>(ref T field, T value, [CallerMemberName] string propName = "")
{
if (field == null || !field.Equals(value))
{
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
}
继承与使用
public class MainViewModel : NotifyBase
{
private object _pageObject;
public object PageObject
{
get { return _pageObject; }
set { SetProperty(ref _pageObject, value); }
}
}
当使用C#中的常见集合,例如数组、List
等对像做为控件的数据源时,会发现在程序中动态的去增删这些集合对象的元素时,窗口的集合控件并会做相对应的更改,也就是C#集合对象中元素的增删并不会主动通知控件对象。对于此,只需要将常用的集合类型替换ObservableCollection
类型即可。
//public List Persons { get; set; } = new List(); 替换成如下类型
public ObservableCollection<Person> Persons { get; set; } = new ObservableCollection<Person>();
ObservableCollection
类型对比List
类型而言,还多了个CollectionChanged
事件,方便对集合元素变化时进行业务处理。
**ObservableCollection
元素的改变触发所在类对象变更通知**
现在,假设ObservableCollection
对象是某个类中的属性成员,如下:
public class RecipeModel:BindableBase
{
public ObservableCollection<StepModel> StepModels { get; set; } = new ObservableCollection<StepModel>();
}
在实际开发过程中会发现,此时即使StepModels属性发生了增加或则修改,是不会出发RecipeModel的属性变更通知的,这是因为StepModels元素的增减并不会改变RecipeModels的引用,因此RecipeModel对象并不能知道他的变化。
如果希望在这种情况下,出发RecipeModel的属性变更通知,那么可以做如下操作:
OnPropertyChanged
是非公开的,所以只能再包一层)public class RecipeModel:BindableBase
{
public ObservableCollection<StepModel> StepModels { get; set; } = new ObservableCollection<StepModel>();
public void NotifyChanged(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
}
recipeModel.StepModels.Add(...);
recipeModel.OnPropertyChanged("StepModels");