站在一个WinForm程序员的角度去考虑,他会做这样几件事情:
- 响应slider1的ValueChanged事件,在事件处理函数中让textBox1显示slider1的Value
- 响应textBox1的LostFocus事件,把textBox1的Text转换成数值,并赋值给slider1
注意了!这就是典型的“非数据驱动界面”的思想。为什么呢?
当我们响应slider1的ValueChanged事件时,我们要的是slider1的Value这个值,这时候,slider1处于核心地位、是数据的“源”(Source);当我们响应textBox1的LostFocus事件时,我们需要的是它的Text属性值,这时候,textBox1又成了数据的source。也就是说,在这种处理方法中,数据没有固定的“源”,两个UI元素是对等的、不存在谁从属于谁的问题。换句话说:它们之间是“就事论事”,并没有什么“关联”。
接下来,让我们体验一下“绿色通道”的快捷!
上述例子的XAML源代码如下:
- <Window x:Class="BindingSample.Window1"
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- Title="http://blog.csdn.net/FantasiaX" Height="300" Width="300">
- <Window.Background>
- <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
- <GradientStop Color="Blue" Offset="0.3"/>
- <GradientStop Color="LightBlue" Offset="1"/>
- </LinearGradientBrush>
- </Window.Background>
- <Grid>
- <TextBox Height="23" Margin="10,10,9,0" Name="textBox1" VerticalAlignment="Top" />
- <TextBox Height="23" Margin="10,41,9,0" Name="textBox2" VerticalAlignment="Top" />
- <Slider Height="21" Margin="10,73,9,0" Name="slider1" VerticalAlignment="Top" Maximum="100" />
- </Grid>
- </Window>
剔除那些花里呼哨的装饰品后,最重要的只有下面3行(而实际上第2行那个textBox2只是为了让鼠标的焦点有个去处):
- <Grid>
- <TextBox Height="23" Margin="10,10,9,0" Name="textBox1" VerticalAlignment="Top" />
- <TextBox Height="23" Margin="10,41,9,0" Name="textBox2" VerticalAlignment="Top" />
- <Slider Height="21" Margin="10,73,9,0" Name="slider1" VerticalAlignment="Top" Maximum="100" />
- </Grid>
然后,我只需在第1行代码上做一个小小的修改,就能完成WinForm中需要用两个事件响应、十多行代码才能完成的事情:
- <Grid>
- <TextBox Height="23" Margin="10,10,9,0" Name="textBox1" VerticalAlignment="Top" Text="{Binding ElementName=slider1, Path=Value}"/>
- <TextBox Height="23" Margin="10,41,9,0" Name="textBox2" VerticalAlignment="Top" />
- <Slider Height="21" Margin="10,73,9,0" Name="slider1" VerticalAlignment="Top" Maximum="100" />
- </Grid>
细心的你,一定一眼就看到只多了这样一句话:Text="{Binding ElementName=slider1, Path=Value}"
这句话的意思是说:Hi,textBox1,从此以后,你的Text属性值就与slider1这个UI元素的Value属性值关联上了,Value变的时候你的Text也要跟着变。
这时候的效果是——你拖动Slider的滑块,textBox1就会显示值(双精度,0到100之间);你在textBox1里输入一个0到100之间的数字,当把鼠标移动到textBox2里时,slider1的滑块会跳到相应的值上去,如图:
非常简单是不是?请注意,这里面可蕴含了“数据驱动界面”的模型哦!在这里,我们始终把slider1的Value当成是数据源(Data Source),而textBox1则是用来显示和修改数据的窗口(Data Presenter)——slider1是核心,它的Value属性值将驱动textBox1的Text进行改变;人为改变textBox1的Text属性值,也会被送回到slider1的Value属性值上去。
是时候让我们了解Data Binding的几个关键概念了——
- 数据源(Data Source,简称Source):顾名思义,它是保有数据的实体、是数据的来源、源头。把谁当作数据源完全由程序员来决定——只要你想把它当做数据核心来使用。它可以是一个UI元素、某个类的实例,也可以是一个集合(关于对集合的绑定,非常重要,专门用一篇文章来讨论之)。
- 路径(Path):数据源作为一个实体可能保有着很多数据,你具体关注它的哪个数值呢?这个数值就是Path。就上面的例子而言,slider1是Source,它拥有很多数据——除了Value之外,还有Width、Height等,但都不是我们所关心的——所以,我们把Path设为Value。
- 目标(Target):数据将传送到哪里去?这就是数据的目标了。上面这个例子中,textBox1是数据的Target。有一点需要格外注意:Target一定是数据的接收者、被驱动者,但它不一定是数据的显示者——也许它只是数据联动中的一环——后面我们给出了例子。
- 关联(Binding):数据源与目标之间的通道。正是这个通道,使Source与Target之间关联了起来、使数据能够(直接或间接地)驱动界面!
- 设定关联(Set Binding):为Target指定Binding,并将Binding指向Target的一个属性,完成数据的“端对端”传输。
绿色通道上的“关卡”:
话说眼看就要到奥运会了,北京的各大交通要道上也都加强了安检力度。微软同学也给Binding这条“绿色通道”准备了几道很实用的“关卡”。这些“关卡”的启闭与设置是通过Binding的属性来完成的。其中常用的有:
- 如果你想把“绿色通道”限制为“单行道”,那就设置Binding实例的Mode属性,它是一个BindingMode类型的枚举值,其中包含了TwoWay、OneWay和OneWayToSource几个值。上面这个例子中,默认地是Default,所以才会有双向的数据传递。
- 如果用户提出只要textBox1的文本改变slider1的滑块立刻响应,那就设置Binding的UpdateSourceTrigger属性。它是一个UpdateSourceTrigger类型枚举值,默认值是UpdateSourceTrigger.LostFocus,所以才会在移走鼠标焦点的时候更新数据。如果把它设置为UpdateSourceTrigger.PropertyChanged,那么Target被关联的属性只要一改变,就立刻传回给Source——我们要做的仅仅是改了一个单词,而WinForm程序员这时候正头疼呢,因为他需要去把代码搬到另一个事件响应函数中去。
- 如果Binding两端的数据类型不一致怎么办?没关系,你可以设置Binding的Converter属性,具体内容在下篇文章中讨论。
- 如果数据中有“易燃易爆”的不安全因素怎么办?OK,可以设置Binding的ValidationRules,为它加上一组“安检设施”(同样也在下篇文中讨论)。
在C#代码中设置Binding
XAML代码是如此简单,简单就那么一句话。这可不是吾等C#程序员、刨根问底之徒可以善罢甘休的!
形象地讲,Binding就像一个盒子,盒子里装了一些机关用于过滤和控制数据,盒子两端各接着一根管子,管子是由管壳和管芯构成的,看上去就像下面的图:
当脑子里有了这样一个形象之后,遵循下面的步骤就OK了:
- Source:确定哪个对象作为数据源
- Target:确定哪个对象作为目标
- Binding:声明一个Binding实例
- 把一根管子接到Source上并把管芯插在Source的Path上
- 把另一根管子接到Target上并把管芯插在Target的联动属性上
如果有必要,可以在3与4之间设置Binding的“关卡”们。其实,第3步之后的顺序不是固定的,只是这个步骤比较好记——一概向右连接。所得结果看上去是这样:
我猜你可能会问:“那个D.P.是什么呀?”
D.P.的全称是“Dependency Property”,直译过来就是“依赖式属性”,意思是说它自己本身是没有值的,它的值是“依赖”在其它对象的属性值上、通过Binding的传递和转换而得来的。表现在例子里,它就是Target上的被数据所驱动的联动属性了!
这里是等价的C#代码,我把它写在了Window1的构造函数里:
- public Window1()
- {
- InitializeComponent();
- // 1. 我打算用slider1作为Source
- // 2. 我打算用textBox1作为Target
- Binding binding = new Binding();
- binding.Source = this.slider1;
- binding.Path = new PropertyPath("Value");
- this.textBox1.SetBinding(TextBox.TextProperty, binding);
- }
有意思的是,Source端的操作,接管子和插管芯分两步,而Target端却是在SetBinding方法中一步完成。注意啦,TextBox.TextProperty就是一个Dependency Property的庐山真面目!关于Dependency Property的文档业已完成,我将择一黄道吉日挂将出来:p
上面的代码稍有简化的余地,那就是把Path的设定转移到Binding的构造中去:
- public Window1()
- {
- InitializeComponent();
- // 1. 我打算用slider1作为Source
- // 2. 我打算用textBox1作为Target
- Binding binding = new Binding("Value");
- binding.Source = this.slider1;
- this.textBox1.SetBinding(TextBox.TextProperty, binding);
- }
这样做的好处是——随便你给binding指定一个Source,只要这个Source有“Value”这个属性,binding就会自动提取它的值并传输给Target端。
我们还可以为binding设些“关卡”:
- public Window1()
- {
- InitializeComponent();
- // 1. 我打算用slider1作为Source
- // 2. 我打算用textBox1作为Target
- Binding binding = new Binding("Value");
- binding.Source = this.slider1;
- binding.Mode = BindingMode.TwoWay;
- binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
- this.textBox1.SetBinding(TextBox.TextProperty, binding);
- }
还有一个小小的提示:如果Source碰巧是一个UI元素,那么也可将binding.Source = this.slider1;改写成binding.ElementName = "slider1";或者binding.ElementName = this.slider1.Name;