代码改变世界

Binding

2013-04-23 21:14  小汪quant  阅读(355)  评论(0)    收藏  举报

Data Binding 在WPF中的地位

应用程序一般可以分为三个层次:数据存储层、数据处理层、数据展示层。程序=数据+算法,其中数据会在这三个层次之间流动,所以对数据来说,这三个层次都很重要;但是算法在这三个层次中的分布并W不均匀,对于一个三层的程序来说,算法主要集中在以下几处:

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

其中,A、B两部分算法相对稳定,很少需要改动;C与需求密切相关,最复杂变动也最大,大多算法集中于此;D、E两层负责UI与逻辑的交互,也占有一定的算法。

显然C部分是程序开发的重点,但是D、E两部分经常成为麻烦的来源:
其一,由于D、E与C紧密相关,所以有可能吧本该放在逻辑层的算法写到这部分(所以才有了MVC、MVP等模式来避免发生这种情况);
其次,这两个部分以消息或者事件的方式与逻辑层沟通,一旦出现同一个数据需要在多处展示/修改是,用于同步的代码就会比较复杂;
最后,D、E本来是互逆的一对,但是却需要分开写——现实数据一个算法,修改数据一个算法。

问题的原因在于逻辑层和展示层的地位不固定——当实现客户需求的时候,逻辑层处于中心内地位,但到了实现UI交互的时候展示层又处于中心地位。WPF的最重要作用是帮助我们将思维的重心固定在逻辑层,让交互层永远处于逻辑层的从属地位。实现这一点的就是Data Binding 及与之配套的Dependency Property 系统和Data Template

Binding 基础

Binding 的源

Binding对源的要求并不苛刻——只要它是一个对象,并且通过属性暴露自己的数据,就可以作为Binding的源。

但是,如果希望作为Binding源的对象有自动通知Binding自己的属性已经变化的能力,那么就需要让类实现InotifyPropertyChanged 接口并在其属性的set语句中激发PropertyChanged事件,并通过事件参数告诉Binding是哪个属性发生了变化。

在日常工作中,除了使用这种对象作为数据源之外,我们还有更多的选择,比如控件吧自己或者自己的容器或自己元素当源、用一个控件作为另一个控件的数据源、把集合作为ItemsControl 的数据源、使用XML作为TreeView或者Menu的数据源、把多个控件关联到一个"数据制高点"上,甚至干脆不给Binding指定数据源让他自己去找。

设定Binding的源一般来说有以下方法:

  1. 把普通的CLR类型单个对象指定为Source,包括dotNet框架自带类型的对象和用户自定义类型对象
  2. 上述类型的对象的集合,如 数组、List<T>、ObservableCollection<T>等。实际工作中,我们经常需要把一个集合作为ItemsControl派生类的数据源来使用,一般是把控件的ItemsSource 属性使用Binding关联到一个集合对象上
  3. 把依赖对象(Dependency Object)指定为Source:依赖对象不仅可以作为Binding的目标,同时也可以作为Binding的源,这样就有可能形成Binding链。依赖对象中的依赖属性可以作为Binding的Path
  4. 把容器对象的DataContext指定为Source ,这是WPF Data Binding 的默认行为:所以我们可以建立一个Binding,只给它设置Path而不设置Source,让这个Binding自己去寻找Source,这个时候Binding会自动把控件的DataContext当作自己的源(它会沿着控件树一层层向外找,直到找到含有Path指定属性的对象为止)
  5. 通过ElementName通过指定对象的Name属性来找到对象,多用于XAML中,因为XAML中无法直接把对象作为Source赋值给Binding
  6. 通过Binding的RelativeSource 属性相对地指定Source:当控件需要关注自己的或者自己内部元素的某个值时就需要使用这种办法。
  7. 把ADO.NET数据对象指定为Source:包括DataTable和DataView等对象
  8. 使用XmlDataProvider把XML数据指定为Source
  9. 把ObjectDataProvider对象指定为Source:当数据源的数据不是通过属性而是通过方法暴露给外界的时候,可以使用这两种对象来包装数据源再把它作为Source
  10. 把使用LINQ检索得到的数据对象作为Binding的源

Binding 的路径

Binding源的对象可能有很多属性,那么Binding到底需要关注那个属性的值呢?这个就是通过设定Binding的Path属性来指定的。在下面的例子中,我们就是以Slider控件对象当作源,而以其Value属性作为路径:

        <TextBox Text="{Binding Path=Value, ElementName=slider1, Mode=TwoWay}" Margin="5"/>
        <Slider x:Name="slider1" Margin="5" Minimum="0" Maximum="100"/>

在XAML代码中或者Binding类的构造器参数列表中我们以一个字符串来表示Path,但是Path的实际类型是PropertyPath,所以上述语句对应的C#代码是:

            Binding binding = new Binding("Value") { Source = this.slider1 };
            this.textBox.SetBinding(TextBox.TextProperty, binding);

Binding 还支持多级路径(即一路"点"下去)。比如,我们想让一个TextBox显示另一个TextBox中的数据的长度,那么我们可以这样写:

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

而索引器(Indexer)又称为带参属性,所以索引器也可以作为Path来使用,比如可以让一个TextBox显示另一个TextBox文本的第四个字符,可以这样写:

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

当使用一个集合或者DataView作为Binding的源时,如果我们想要把它的默认元素作为路径可以使用"/",例如

      List<string> names = new List<string>{"Tom","Tim","Jack"};
      textBox1.SetBinding(TextBox.TextProperty, new Binding("/") { Source = names });
      textBox2.SetBinding(TextBox.TextProperty, new Binding("/Length") { Source = names, Mode = BindingMode.OneWay });
      textBox3.SetBinding(TextBox.TextProperty, new Binding("/[2]") { Source = names, Mode = BindingMode.OneWay });

"没有Path"的Binding

有时候我们会在代码中看到Path是一个"."或者干脆没有Path。这是一种比较特殊的情况:Binding的source本身就是数据且不需要Path来指明。例如string、int等都是如此

Binding对数据的转化和校验

Binding用于数据有效性校验的关卡是它的ValidationRules属性,用于数据转化的是它的Converter属性。

ValidationRules属性的类型是Collection<ValidationRule>,从名称和类型就可以知道可以为每个Binding设置多个数据校验条件,每个条件是一个ValidationRule类型的对象。

ValidationRule是一个抽象类,在使用的时候需要创建它的派生类并实现它的Validate方法,Validate方法的返回值是ValidationResult类型对象,如果校验通过,就把ValidationResult的IsValid属性设置为true,反之设置为false,并为其ErrorContent属性设置一个合适的消息内容。

Binding进行校验时的默认行为是认为来自Source的数据总是正确的,只有来自Target的数据才可能有问题,为了不让有问题的数据污染Source,所以需要进行校验。如果想要对Source也进行校验的话,需要设置校验条件的ValidatesOnTargetUpdated属性设置为true

例如下图中文本框与slider的Value属性双向绑定,拖动滑块可以改变文本框的数据,在文本框输入新的数据也会更新Slider的位置。Slider的范围我们设置为【0,100】,所以我们需要对文本框的数据数据做校验

我们定义如下InRangeValidation类:

    public class InRangeValidation:ValidationRule
    {
        private double min;
        private double max;
        public InRangeValidation(double minValue, double maxValue)
        {
            min = minValue;
            max = maxValue;
        }
        public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
        {
            double d;
            if (double.TryParse(value.ToString(),out d))
            {
               if (d >= min && d <= max)
                    return new ValidationResult(true"");
            }
            return new ValidationResult(false"设定的值超出范围");
        }
    }

而绑定代码我们这样设定:

    Binding binding = new Binding("Value") { Source = slider1,Mode=BindingMode.TwoWay };
    binding.ValidationRules.Add(new InRangeValidation(slider1.Minimum, slider1.Maximum));
    textBox1.SetBinding(TextBox.TextProperty, binding);

 

前面的例子中,我们将Slider的Value属性与TextBox的Text属性绑定。而实际上,他们一个是double型,一个是String类型,为什么可以在C#这样的强类型语言中通行无阻呢?原因在于Binding还有一个数据转化的机制,而double到String的转化比较简单,所以WPF在后台偷偷帮我们完成了这个转化。

但是如果更复杂的类型或者有我们自定义的转化,则需要我们自己定义转化规则,方法是创建一个类并继承IvalueConverter接口,该接口包含两个方法,Convert和ConvertBack,在binding的Mode为OneWay的时候,只有Convert会被调用,为TwoWay的时候两者都会被调用。

例如,我们定义如下从string到Visibility枚举类型的转化

    public class TextBoxToBtnShowConverter : IvalueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value.ToString().Length > 0)
                return Visibility.Visible;
            else
                return Visibility.Hidden;
        }

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

我们在XAML中引入命名空间,并用资源的形式生成一个TextBoxToBtnShowConverter实例,然后在Binding中使用它

<Window x:Class="TempTest.MainWindow"
        xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation
        xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
        xmlns:local="clr-namespace:TempTest"
        Title="MainWindow" Height="110" Width="300">
    <Window.Resources>
        <local:TextBoxToBtnShowConverter x:Key="cov1"/>
    </Window.Resources>
    <StackPanel Margin="5">
        <TextBox x:Name="textBox1" Text="abc" Margin="5"/>
        <Button x:Name="btn1" Margin="5" HorizontalAlignment="Center" MinHeight="20" MinWidth="100"
                Content="Btn" 
                Visibility="{Binding Path=Text, ElementName=textBox1, Converter={StaticResource ResourceKey=cov1},Mode=OneWay}"
                />
    </StackPanel>
</Window>

效果是当textBox1中没有字符的时候,btn1会自动隐藏

多路Binding

有时候UI显示跟多个对象相关,比如需要同时输入了用户名和密码之后才能点击登录按钮,这个时候就需要将登录按钮绑定两个文本框。方法是使用MultiBinding,MultiBinding与Binding一样都继承自BindingBase,所以所有可以使用Binding的地方都可以使用MultiBinding。

MultiBinding有一个Bindings属性,为Collection<BindingBase>类型