WPF9 Binding-1

程序的本质是数据加算法。数据会在存储、逻辑和展示三个层流通,在数据角度上看,三层都很重要,但从算法角度上来看,算法在程序中的分布就不是很均匀,对于一个三层结构的程序来说,算法一般分布在:

A、数据库内部

B、读取和写会数据

C、业务逻辑

D、数据展示

E、界面和逻辑的交互

A、B两个部分的算法一般都非常稳定,不会轻易改动,复用性也很高;C处与客户需求关系最紧密,最复杂,变动也最大,大多数算法都集中在此;D、E两层负责UI与逻辑的交互,也占有一定量的算法。

C是程序的核心,是占用开发精力最多的地方,但D、E却因为与逻辑层紧密相关,而容易把本应该写在逻辑层里的算法写在D、E部分(所以才有了MVC等模式)。WPF可以让展示出永远处于逻辑层的从属地位,这种能力的实现关键是它引入了Data Binding概念以及配套的Dependency Property系统和DataTemplate。

Data Binding在WPF系统中起到的是数据告诉公路的作用,加工好的数据会自动送达用户界面加以显示,被用户修改过的数据也会自动传回逻辑层,一旦数据被加工好又会送达用户界面,这就是数据驱动UI,也是现在前端流行的各类MVVM框架的核心概念。

Binding的两端分别是源Source和目标Target,数据源是一个对象,这个对象可能有和很多数据,这些数据通过属性暴露给外界,UI上的元素关心的是哪个属性值的变化,这个属性就成为Binding的路径,但Binding还需要有一个自动机制,即当值变化后属性要有能力通知Binding,让Binding把变化传递给UI元素,这个功能的实现是在属性的set语句中激发一个PropertyChanged事件。这个事件不需要手动声明,要做的是让作为数据源的类实现System.ComponentModel名称空间中的INotifyPropertyChanged接口。当为Binding设置了数据源后,Binding就会自动监听来自这个接口的PropertyChanged事件。

 

    public class Student:INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private string name;
        public string Name
        {
            get
            {
                return name;
            }
            set
            {
                name = value;
                //触发事件
                if(this.PropertyChanged!=null)
                {
                    this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Name"));
                }
            }
        }
    }

为了在Student的值发生改变时有能力通知Binding,让Binding把变化传递给UI元素。方法是在属性的set语句中激发一个PropertyChanged事件。这个事件不需要自行声明,而是让作为数据源的类实现System.ComponentModel名称空间中的INotifyPropertyChanged接口。

在窗体上准备一个TextBox和一个Button,TextBox将作为Binding目标,Button的Click事件发生时会改变Student对象的Name属性值。

    <StackPanel>
        <TextBox x:Name="textBoxName" BorderBrush="Black" Margin="5"/>
        <Button Content="AddAge" Margin="5" Click="Button_Click"/>
    </StackPanel>

然后使用Binding把数据源和UI元素连接起来。

    public partial class MainWindow : Window
    {
        Student stu;
        public MainWindow()
        {
            InitializeComponent();
            //准备数据源
            stu = new Student();
            //准备Binding
            Binding binding = new Binding();
            binding.Source = stu;
            binding.Path = new PropertyPath("Name");
            //使用Binding连接数据源与Binding目标
            BindingOperations.SetBinding(this.textBoxName, TextBox.TextProperty, binding);
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            stu.Name += "Name";
        }
    }

Student stu;声明在构造函数和按钮事件外是为了让Window的构造器和按钮事件都能访问由它引用的Student实例。

Binding部分,先创建Binding的实例,然后使用binding.Source=stu;为Binding实例指定数据源,最后使用binding.Path=new PropertyPath("Name");来为Binding指定访问路径。

把数据源和目标连接在一起的任务是使用"BindingOperations.SetBinding(...)"方法完成的,这个方法有三个重要参数:

1、第一个参数用于指定Binding的目标,本例中是this.textBox.Name

2、第二个参数用于为Binding指明把数据送达目标的哪个属性。但这个参数并没有使用对象的属性,而只是一个静态只读的DependencyProperty类型成员变量,这就是依赖属性。这类属性的值可以通过Binding依赖在其他对象的属性值上,被其他对象的属性值所驱动。

3、第三个参数很明了,就是指定使用哪个Binding实例将数据源与目标关联起来。

按钮事件用于对Name属性进行更新。

一、Binding的源和路径

Bingding的源也就是数据的源头。Binding对源的要求并不苛刻,只要它是一个对象,并且通过属性公开自己的数据,就能作为Binding的源。

数据源不仅仅可以是类(比如实现INotifyPropertyChanged接口,并在属性set语句中激发PropertyChanged事件),还可以是控件自己、自己的容器、子元素,另一个控件、集合(作为ItemsControl)的数据源、XML(TreeView、Menu的数据源)、把多个控件关联到一个“数据制高点”上,甚至不给Binding指定数据源,让他自己去寻找。

1、把控件作为Binding源和Binding标记扩展

大多数情况下Binding的源是逻辑层的对象,但有时候为了让UI元素产生一些联动效果,也会使用Binding在控件间关联。

        <TextBox x:Name="textBox1" Text="{Binding Path=Value,ElementName=slider}" BorderBrush="Black" Margin="5"/>
        <Slider x:Name="slider" Maximum="100" Minimum="0" Margin="5"/>

WPF中,代码可以访问XAML代码中声明的变量,但XAML代码却无法访问C#代码中声明的变量、

上一句话等价于this.TextBox1.SetBinding(TextBox.TextProperty,new Binding("Value")){ElementName="slider"}

2、控制Binding的方向和数据更新

Binding在源和目标之间架起了沟通的桥梁,默认情况下数据既能够通过Binding送达目标,也能够从目标返回源(手机用户对数据的修改)。有时候数据只需要展示给用户、不允许用户修改,这时候可以把Binding模式更改为源向目标的单项沟通。

控制Binding数据流向的属性是Mode,它的类型是BindingMode枚举。BindingMode可取值为TwoWay、OneWay、OnTime、OneWayToSource和Default。这里的Default值是指Binding的模式会根据目标的实际情况来确定,比如若是可编辑的(TextBox.Text属性),Default就采用双向模式;若是只读的(TextBlock.Text)就采用单向模式。

Binding的另一个属性——UpdateSourceTrigger,它的类型是UpdateSourceTrigger枚举,可取值为PropertyChanged、LostFocus、Explicit、Default、

3、Binding的路径

作为Binding源的对象可以由很多属性,通过这些属性Binding源可以把数据暴露给外界。Binding Path属性来指定Binding到底关注那个属性的值。例如把Slider控件对象作为源、把它的Value属性作为路径

<TextBox x:Name="textBox1" Text="{Binding Path=Value,ElementName=slider}"/>

等价于C#:

Binding binding=new Binding(){Path=new PropertyPath("Value"),Source=this.slider};

this.textBox1.SetBinding(TextBox.TextProperty,binding);

也可以让一个TextBox显示另一个TextBox的文本长度

        <TextBox x:Name="textBox2" BorderBrush="Black" Margin="5"/>
        <TextBox x:Name="textbox3" Text="{Binding Path=Text.Length,ElementName=textBox2,Mode=OneWay}" BorderBrush="Black" Margin="5"/>
    

 

索引器也能作为Path来使用,比如让一个TextBox显示另一个TextBox文本的第四个字符

        <TextBox x:Name="textbox4" BorderBrush="Black" Margin="5"/>
        <TextBox x:Name="textbox5" Text="{Binding Path=Text.[3],ElementName=textbox4,Mode=OneWay}" BorderBrush="Black" Margin="5"/>
    

Text.[3]中的.可以去掉。

当使用一个集合或者DataView作为Binding源时,如果我们想把它的默认元素当做Path使用,则需要这样的语法:

            List<string> stringList = new List<string>() { "Tim", "Tom", "Blog" };
            this.textbox4.SetBinding(TextBox.TextProperty, new Binding("/Length") { Source = stringList, Mode = BindingMode.OneWay });      

如果要是用默认元素,则直接/即可,如果要取第3个元素/[2],如果要取子集合元素,则可以一路/下去。

4、没有path的Binding

当Binding源本身就是数据且不需要Path类指定时,Path就是一个.或者干脆没有Path的Binding。典型的,string、int等基本类型就是这样,它们的实例本身就是数据,则无法通过它的某个属性来访问这个数据,此时只需要把Path的值设置为.即可。XAML中可以省略,而C#中不可以。

        <StackPanel.Resources>
            <sys:String x:Key="myString">
                static string
            </sys:String>
        </StackPanel.Resources>
<TextBlock x:Name="textBlock1" TextWrapping="Wrap" Text="{Binding Path=.,Source={StaticResource ResourceKey=myString}}" FontSize="16" Margin="5"/>

等价于C#代码:

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

4、为Binding指定源的集中方法

Binding的源时数据的来源,所以只要一个对象包含数据并能通过属性把数据暴露出来,它就能高档做Binding的源来使用,包含数据的对象比比皆是,但必须为Binding的Source指定合适的对象Binding才能正确工作。

常见的绑定方法有:

A、把普通CLR类型单个对象指定为Source:包括.Net Framework自带类型对象和用户自定义类型的对象。如果类型实现了INotifyPropertyChanged接口,则可通过在属性的set语句里激发PropertyChanged事件来通知Binding数据已被更新。

B、把普通CLR集合类型对象指定为Source:包括数组、List<T>、ObservableCollection<T>等集合类型,实际工作中,我们经常需要把一个集合作为ItemsControl派生类的数据源来使用,一般是把控件的ItemsSource属性使用Binding关联到一个集合对象上。

C、把ADO.NET数据对象指定为Source,包括DataTable和DataView等对象。

D、使用XmlDataProvider把XML数据指定为Source:XML作为标准的数据存储和传输格式几乎无处不在,可以用它标识单个数据对象或集合;

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

F、把容器的DataContext指定为Source:(WPF Data Binding默认行为),直到从哪个属性获取数据,但把哪个对象作为Binding源还不能确定。此时,就可以先建立一个Binding、只给它设置Path而不设置Source,让这个Binding自己去寻找Source。此时,Binding会自动把控件的DataContext当做自己的Source(会沿着控件树一层一层向外找,直到找到带有Path指定属性的对象为止)。

G、通过ElementName指定Source:在C#代码里可以直接把对象作为Source赋值给Binding,但XAML无法访问对象,所以只能使用对象的Name属性来找到对象。

H、通过Binding的RelativeSource属性相对地指定Source,当控件需要关注自己的、自己容器的或自己内部元素的某个值就需要使用这种方法。

I、通过Binding的RelativeSource属性相对地指定Source,当控件需要关注自己的、自己容器的或者自己内部元素的某个值就需要使用这种方法。

J、把ObjectDataProvider对象指定为Source:当数据源的数据不是通过属性而是通过方法暴露给外界的时候,可以使用这两种对象来包装数据源并再把他们指定为Source。

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

Binding可以把单个CLR类型对象指定为Binding的Source,但如果一个Binding只知道自己的Path而不知道自己的Source时,就会沿着UI元素树一路向树根找过去,没过一个节点就会看这个节点的DataContext是否具有Path所制定的属性。如果有,这把这个对象作为自己的Source,否则继续寻找。

        <StackPanel.DataContext>
            <local:Student Id="6" Age="29" Name="Tim"/>
        </StackPanel.DataContext>
        <Grid>
            <StackPanel>
                <TextBox Text="{Binding Path=Id}" Margin="5"/>
                <TextBox Text="{Binding Path=Name}" Margin="5"/>
                <TextBox Text="{Binding Path=Age}" Margin="5"/>
            </StackPanel>
        </Grid>

 

WPF9 Binding-1

 

此时属性结构就有一个DataContext:Student。

使用xmlns:local="clr-namespace:WPF_XMAL_6_Binding"就可以使用在C#中定义的Student类。

                <TextBox Text="{Binding Path=Id}" Margin="5"/>
                <TextBox Text="{Binding Path=Name}" Margin="5"/>
                <TextBox Text="{Binding Path=Age}" Margin="5"/>

3个TextBox虽然没有指定Source,但Binding就会自动向UI树的长层去寻找可用的DataContext对象。Binding的Path可以设置为.,也可以直接省略不写。

Binding这么智能是因为,当没有为空间的某个依赖属性显式赋值时,空间会把自己容器的属性值借过来当自己的属性值,例如如果为一个Button所在的Grid设置DataContext=“6”,然后Button的事件中设置MessageBox.Show(btn.DataContext.ToString());,虽然Button本身没有值,但它会显式Grid的DataContext。

<StackPanel DataContext="LiaoRan">
<Button x:Name="btn" Content="OK" Click="btn_Click"/>
.... private void btn_Click(object sender, RoutedEventArgs e) { MessageBox.Show(btn.DataContext.ToString()); }

属性值沿着UI树向下传递了。

实际工作中DataContext用法非常灵活,比如:

1、当UI上的多个控件都使用Binding关注同一个对象时,不妨使用DataContext

2、当作为Source的对象不能被直接访问的时候,例如A窗体内的控件是private访问级别,B要去访问A窗体内的控件作为自己的Binding源,就可以把A窗体内的这个控件或控件的值作为窗体A的DataContext(这个属性是public级别)从而暴露数据。

6、使用集合对象作为列表控件的ItemSource

WPF中的列表式控件都派生自ItemsControl类,也就自然继承了ItemsControl这个属性。ItemsSource属性可以接受一个IEnumerable接口派生类的实例作为自己的值,每个ItemsControl派生类都具有自己对应的条目容器,比如ListBox的条目容器是ListBoxItem,ItemSource里存放的是一条一条的数据,要想把数据显示出来需要为其穿上外衣,就是Binding。只要为每一个ItemsControl对象设置ItemsSource属性值,ItemsControl对象就会自动迭代其中的数据元素,为每个元素准备一个条目容器,并建立起关联。

        <StackPanel x:Name="stackPanel" Background="LightBlue">
            <TextBlock Text="StudentID:" FontWeight="Bold" Margin="5"/>
            <TextBox x:Name="textBoxId" Margin="5"/>
            <TextBlock Text="StudentList:" FontWeight="Bold" Margin="5"/>
            <ListBox x:Name="listBoxStudents" Height="110" Margin="5"/>
        </StackPanel>  
        public MainWindow()
        {
            InitializeComponent();
            //准备数据
            List<Student> stulist = new List<Student>()
            {
                new Student(){Id=0,Name="Tim",Age=21},
                new Student(){Id=1,Name="Tom",Age=22},
                new Student(){Id=2,Name="Cloud",Age=23},
                new Student(){Id=3,Name="Strife",Age=24},
                new Student(){Id=4,Name="Tifa",Age=25},
                new Student(){Id=5,Name="Victor",Age=26},
            };
            //为ListBox设置Binding
            this.listBoxStudents.ItemsSource = stulist;
            this.listBoxStudents.DisplayMemberPath = "Name";
            //为TextBox设置Binding
            Binding binding = new Binding("SelectedItem.Id") { Source = this.listBoxStudents };
            this.textBoxId.SetBinding(TextBox.TextProperty, binding);
        }

7、使用Linq作为Binding的源

        <StackPanel Background="LightBlue">
            <ListView x:Name="listViewStudent" Height="143" Margin="5">
                <ListView.View>
                    <GridView>
                        <GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding Id}"/>
                        <GridViewColumn Header="Id" Width="100" DisplayMemberBinding="{Binding Name}"/>
                        <GridViewColumn Header="Id" Width="80" DisplayMemberBinding="{Binding Age}"/>
                    </GridView>
                </ListView.View>
            </ListView>
            <Button Content="Local" Height="25" Margin="5,0" Click="Button_Click_1"/>
        </StackPanel>
        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            //获取DataTable实例
            List<Student> list = new Student().GetStudentList();
            //绑定
            this.listViewStudent.ItemsSource = list.Where(x => x.Name.StartsWith("T"));
        }

绑定到listViewStudents.ItemsSource上即可。

 

 WPF9 Binding-1

 

WPF9 Binding-1

上一篇:Win10系统易升如何彻底关闭?【系统天地】


下一篇:leetcode hot 100-560. 和为K的子数组