WPF入门到跪下 第五章 数据绑定


绑定表述的是一种关系,通过某种关系将多个事物联系在一起。

WPF中绑定的完成需要两个角色:

  • 目标对象Target:界面对象中需要进行数据交互的依赖属性(非依赖属性无法绑定)。
  • 源对象Source:也就是需要与界面做数据交互的数据对象。

绑定表达式

绑定表达式的作用就是将目标对象与源对象建立关联,通过绑定可以将界面与数据逻辑进行隔离。

常规绑定表达式:{Binding Source=sourceName, Path=dataPath}

数据源类型

WPF中对数据源的类型有做规定,具体有以下类型可以做为数据源(瞄一眼就行了,不要记):

  • 依赖属性对象做为数据源。
  • 普通数据类型或集合类型做为数据源。
  • 单个对象做为数据源,INotifyPropertyChanged(MVVM开发中的基础)
  • ADO.NET数据对象做为数据源,DataTable、DataView[0] →转换成List
  • XmlDataProvider做为数据源。
  • ObjectDataProvider做为数据源。
  • Linq结果做为数据源 :List
  • 静态属性。

一、指定数据源

绑定表达式中可以指定绑定的源对象是何种类型:

1、Source

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文件数据,将文件放在程序集中作为文件资源
<?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>
  • XAML中加载资源,并进行数据绑定
<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>

注意查看上面的代码,使用了XPathXPath是针对XmlDataProvider数据源而准备的。

  • 绑定xml文件中所有节点的内容,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#函数的元素,具体用法如下:

定义要使用的函数

  • 注意,为了在XAML中能方便使用,传入的参数跟返回的类型可以使用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>

2、ElementName

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>

3、静态属性

当使用静态属性来作为数据源时,做法与实例资源有所不同。

上文中为了方便,在XAML中使用了静态资源的方式来举例,然而XAML中的每一个元素都是一个新建的实例对象,如果想要使用某个类型中的静态属性作为数据源,显然从新建的实例对象中获取是不合理的。WPF为此提供了一个较为简便的做法,具体如下:

{Binding Path=(local:StaticClass.StaticValue)}:静态属性的绑定,直接使用Path即可,Path中使用()是为了告诉WPF这个是静态属性,不需要在XAML中逐层往上寻找该属性。

<Grid>
    <!--假设项目已经创建了一个StaticClass类型,并在类型中定义了一个静态属性,StaticValue-->
    <TextBlock Text="{Binding Path=(local:StaticClass.StaticValue)}"/>
</Grid>

4、DataContext

<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>
    

5、RelativeSource

WPF的绑定表达式中,除了可以使用SourceElementName指定数据源对象之外,还以使用相对源RelativeSource来指定。

语法

{Binding Path=property, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}

  • 其中RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}的意思是,寻找当前控件的类型(Type)为Window的祖先对象,最终会找到当前窗体对象。
  • RelativeSource中,AncestorType属性默认配合FindAncestor模式使用的,所以如果指定了AncestorTypeMode属性可以省略。

需要关注的是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来进行数据源的指定。

二、辅助属性

绑定表达式中除了可以指定数据源对象(SourceElementNameRelativeSource以及隐性的DataContext)、路径(PathXPath)这两个比较核心的属性外,还可以设定一些辅助特性。

1、绑定模式

语法:{Binding Source={...}, Path=..., Mode=ModeType}

  • TwoWay:双向绑定。
  • OneWay:单向绑定,数据只能从数据源到目标,目标数据的改变不会影响数据源。
  • OneTime:目标只从数据源从接收一次数据,也就是只接收一次数据源的初始值。
  • OneWayToSource:单向绑定,数据只能从目标到数据源,数据源的修改不会影响目标,但目标的修改会改变数据源。
  • Default:根据依赖属性定义时的默认方式,表达式中设定的绑定模式,优先级要高于依赖属性定义时设置的绑定模式。

有一点需要注意的是,OneWay模式下,如果在C#代码中直接对控件对象的对应依赖属性进行赋值,会直接切断绑定关系,相当于将控件中的绑定表达式去除。

2、触发机制

进行数据绑定后,有些控件(例如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();//触发更新
    }
    

3、绑定表达式的转换器

有些情况下,在使用绑定表达式进行数据绑定的时,会出现数据源与目标的数据类型不一致的情况,或者说希望对绑定的数据源进行数据的判断、校验,最后返回所需要的数据。这个时候就要用到绑定表达式中的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上的某个字符串属性,然后利用转换器进行反射,将对应名称的控件对象创建并返回。

4、字符串格式化

绑定表达式中,如果需要对绑定数据进行字符串的格式化,需要使用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>

在这里插入图片描述

5、延时触发

当我们在绑定表达式中使用了UpdateSourceTrigger=PropertyChanged属性,则目标数据发生变动时会实时触发数据源的变化。这样在一些检索框的使用上会很不友好,每次输入字符都会去进行检索。此时可以在绑定表达式中使用Delay属性,可以指定当目标数据发生变化后多少毫秒内没有再次发生变法时才触发数据源的变化。

语法:{Binding ……, Delay=200}

6、替补值

FallbackValue:指定当获取不到绑定的数据时(例如Path写错时),用什么值来代替。

<TextBlock Text="{Binding DateTimea,FallbackValue=ok}"/>

TargetNullValue:指定当获取到的绑定数据为null时,用什么值代替。

<TextBlock Text="{Binding DateTime,TargetNullValue=ok}"/>

三、数据校验

在实际项目中,很多情况下需要对用户的录入信息进行校验并给出提示,注册用户的表单就是个典型的案例。

在WPF中,对于数据的校验有很多种方式,这里将几种比较常用的方式进行记录。

1、依赖属性的校验

在进行依赖属性的定义时,是可以设置校验回调函数的,当校验回调函数返回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;
    }
    

2、普通属性的校验

如果绑定数据时,数据源是一个普通的属性而不是依赖属性,有没有办法对其进行校验呢?答案是肯定的,只需要在属性的设置逻辑中进行校验并抛出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>
    

3、自定义校验规则类

如果不希望再普通属性中做过校验逻辑,或者不想使用依赖属性定义时所设置的校验异常,可以通过捕获自定义的校验规则来实现同样的效果。

  • 需要注意的是,如果普通属性的设置逻辑中做了抛出异常的校验然后使用了捕获自定义校验规则,则两则都会触发。

  • 如果是依赖属性中设置了校验回调函数,然后使用了捕获自定义校验规则,只会触发自定义校验规则。

  • 创建校验规则类

    创建校验规则类需要实现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>
    

4、开启数据异常校验(利用特性)(推荐)

除了上文中所说的捕获校验异常的方式外,还可以通过在表达式中使用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>

5、数据校验实例

  • 代码

    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来实现。

多重绑定由于绑定的数据有多个,所以要求必须对绑定的数据进行转换,而对数据进行转换的方式有两种。

1、字符串格式化

可以通过字符串格式化的方式对多重绑定的数据进行转换。

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>

2、绑定表达式的多值转换器

使用绑定表达式的转换器也可以完成对数据的转换,首先要创建转化器类型,实现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模式得以实现的基础。


一、属性变化通知

1、实例属性变化通知

变化通知的实现比较简单,所在类实现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"));
        } 
    }
}

2、静态属性的变化通知

静态属性的变化通知与实例属性有所不同,原因在于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层完成的。

3、通用属性变化通知

如果每个需要进行属性变化通知的类型都要取继承并实现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));
    }
}
  • 当StepModels发生变化时,调用变更通知的方法。
recipeModel.StepModels.Add(...);
recipeModel.OnPropertyChanged("StepModels");

你可能感兴趣的:(WPF,wpf)