《Windows Phone 7程序设计》样章-《数据绑定》
在我看来数据绑定是XAML类程序(WPF,Silverlight,WP7,Windows8)最最关键的概念,也是MVVM模式的基础,如果一位开发者在开发XAML类程序时并没有用上数据绑定,那么我觉得他/她还没有掌握开发XAML类程序的能力。数据绑定可以说每个XAML类程序开发者必须掌握的基本技能之一。下面是《数据绑定》样章的一部分,如果觉得翻译的不错而且条件许可,请购买正版书,谢谢您为中国文化事业的贡献。如果觉得有问题请指出,我们会总结堪错列表,谢谢!
---------------------------------------------------------------以下是《数据绑定》样章的一部分-------------------------------------------------------------
假设你想让用户与Slider控件进行交互,而且你也想与ColorScroll程序一样,通过TextBlock显示Slider当前的值。其实很简单。只要为Slider控件的ValueChanged事件创建一个处理程序就可以了,当调用处理程序的时候,从Slider取出Value属性的值并将其转换成字符串,然后把该字符串设置给TextBlock的Text属性。
像这样的任务非常普遍,因此Silverlight提供了一种简便的机制来实现这些任务。这种机制称为数据绑定(data binding),或者就简称为绑定(binding)。数据绑定是一个对象的一个属性与另外一个对象的一个属性之间的一条链接(link),因此在绑定的情况下当一个属性发生改变时,另一个属性也随之更新。绑定可以是双向的(bidirectional),在这种情况下,其中一个属性发生的变化会引起另一个属性也随之发生变化。
从本质上说,数据绑定可能像你所期望的那样:由于注册了一个事件处理程序,使得一个属性从另外一个属性中获取更新,期间可能进行了一些数据转换。通常你可以完全通过XAML来定义数据绑定,这意味着你不必编写任何代码。从语法上看,这好像不需要移动任何部件就能传输数据了。
演示数据绑定最简单的方法是使用两个可视化元素,例如Slider和TextBlock元素,我也从这两个元素开始。但是,如果把可视化元素和基础数据源进行绑定的话,更能体现出数据绑定的强大威力。
本章的目标是避免在代码隐藏文件中显式地使用事件处理程序,但是在本章的结束部分我不得不使用几个事件处理程序。当然,我们还是需要一些其他代码来支持XAML中的数据绑定,但这些代码中的大部分可以恰当地归类为业务对象(business object),而不是用户界面元素。
绑定源与目标
在典型的数据绑定中,一个对象的属性发生变化时,另一个对象的属性也随之自动更新。提供数据的对象,例如Slider,被认为是数据绑定的源(source);接收数据的对象(如TextBlock)是绑定的目标(target)。
通常给数据绑定源指定一个名字:
<Slider Name="slider" .../>
你可以把目标属性作为一个属性元素 并赋值给类型为Binding的对象:
<TextBlock ...> <TextBlock.Text> <Binding ElementName="slider" Path="Value" /> </TextBlock.Text> </TextBlock>
使用ElementName属性指定源元素的名称;使用Path属性指定源属性的名称,在这个例子中Path是Slider的Value属性。有时候把这种类型的绑定称为元素名称绑定,因为绑定源是一个可视化元素,并通过名称来引用。
为了使语法变得更加友好,Silverlight为Binding提供了一个标记扩展(markup extension),在此,所有的东西都定义在一对花括号里面。(这是Silverlight for Windows Phone的几个标记扩展中的一个。第7章介绍过StaticResource,第16章将介绍TemplateBinding。)这里是更精简的语法:
<TextBlock ... Text="{Binding ElementName=slider, Path=Value}" ... />
请注意,ElementName和Path的设置用一个逗号分隔,而slider和Value名称的引号已经去掉了。引号永远都不会出现在标记扩展的大括号中。
SliderBindings程序使用了这样的绑定方式,你可以试验一下,尝试做一些修改。这一切都在XAML文件里面:
Silverlight项目:SliderBindings 文件:MainPage.xaml(节选) <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Slider Name="slider" Value="90" Grid.Row="0" Maximum="180" Margin="24" /> <TextBlock Name="txtblk" Text="{Binding ElementName=slider, Path=Value}" Grid.Row="1" FontSize="48" HorizontalAlignment="Center" VerticalAlignment="Center" /> <Rectangle Grid.Row="2" Width="{Binding ElementName=slider, Path=Value}" RenderTransformOrigin="0.5 0.5" Fill="Blue"> <Rectangle.RenderTransform> <RotateTransform x:Name="rotate" Angle="90" /> </Rectangle.RenderTransform> </Rectangle> </Grid>
这个页面包含了一个范围从0到180的Slider,以及一个TextBlock,TextBlock的Text属性绑定到Slider的Value属性上,另外还有一个Rectangle,它的Width属性也绑定到Slider相同的Value属性上。Rectangle还有一个RotateTransform属性,这个属性使得Rectangle元素旋转了90°。
操作Slider时,TextBlock显示Slider的值,而Rectangle的高度也随之变大或者变小。(Binding的目标是Rectangle的Width属性,而Rectangle的转角是90°。)
在Binding扩展标记中,属性的顺序无关紧要。你可以把Path属性放在前面:
<TextBlock ... Text="{Binding Path=Value, ElementName=slider}"
事实上,如果路径出现第一个位置上,可以删除“Path=”部分,只是使用属性名:
<TextBlock ... Text="{Binding Value, ElementName=slider}"
在本章的后面以及随后的章节中,我将使用这种缩略形式的语法,但对于元素名称绑定,我却不喜欢这样做,因为这样做我就无法知道绑定底层的工作原理。Binding类首先需要在可视化树中找到一个名为slider的元素,然后它需要使用反射来找到该元素的Value属性。我偏向于下面这样的语法,这种语法按照内部操作的过程来排列属性的顺序:
<TextBlock ... Text="{Binding ElementName=slider, Path=Value}"
为什么Binding的这个属性叫做Path而不是Property呢?毕竟,Style类就有一个叫做Property的属性。为什么Binding却没有呢?
简单的答案是Path能把多个属性名组合在一起使用。例如,假设Slider没有名称。如果你知道该Slider是ContentPanel元素的Children集合的第一个子元素,你可以间接地引用Slider:
Text="{Binding ElementName=ContentPanel, Path=Children[0].Value}"
或者,使用可视化树更上一层的元素:
Text="{Binding ElementName=LayoutRoot, Path=Children[1].Children[0].Value}"
Path的组成部件必须是通过点号连接的属性或者索引器(indexer)。
Target和Mode
绑定包含一个源和一个目标。绑定目标被认为是绑定要设置的属性,该属性必须始终都是一个依赖属性,永远都是。当你在代码中创建绑定的时候,这一限制非常明显。
修改一下SliderBindings程序,删除TextBlock的Text属性上的绑定。在MainPage.xaml.cs文件中,你需要添加using指令来引用System.Windows.Data命名空间,这个命名空间包含了Binding类。在构造函数里面,调用完InitializeComponent函数之后,生成一个类型为Binding的对象,并设置它的属性:
Binding binding = new Binding(); binding.ElementName = "slider"; binding.Path = new PropertyPath("Value");
ElementName和Path属性是绑定源。下面看看将TextBlock的Text属性作为绑定目标的代码:
txtblk.SetBinding(TextBlock.TextProperty, binding);
SetBinding方法定义在FrameworkElement里面,第一个参数是依赖属性,也就是目标属性。该目标也是调用SetBinding方法的元素。你也可以使用其他替代方案,使用静态方法BindingOperations.SetBinding来绑定目标:
BindingOperations.SetBinding(txtblk, TextBlock.TextProperty, binding);
但你仍然需要依赖属性。因此,这就是可视化对象的属性应该是依赖属性的另一个原因。你不仅可以为这些属性定制样式,而且可以把它们制作成动画,但所有这些数据绑定的目标必须是依赖属性。
就依赖属性的优先级而论,数据绑定与本地设置的级别相同。
使用BindingOperations.SetBinding方法意味着你可以在任何依赖属性上设置绑定。对于Silverlight for Windows Phone来说,事实并非如此。在Windows Phone中绑定的目标必须是FrameworkElement的属性。
例如,你会发现在MainPage.xaml中的Rectangle元素包含了RotateTransform属性,该属性设置为一个RotateTransform对象。尝试把TextBlock的Text属性和Rectangle的Width属性上的绑定也应用到Angle属性上:
<RotateTransform x:Name="rotate" Angle="{Binding ElementName=slider, Path=Value}" />
这看起来好像没问题,但却不能正常工作。你会在运行时得到一个XamlParseException异常。Angle本身是依赖属性,不是条件充足了吗?但是RotateTransform并不是派生自FrameworkElement,所以它不可以作为绑定的目标。(在Silverlight 4中,应用于RotateTransform的Angle属性的绑定可以正常工作。但是Silverlight for Windows Phone大体上还是Silverlight 3 。)
如果想要这么做,你需要删除RotateTransform的Angle属性上的绑定,以及已添加到MainPage.xaml.cs的所有代码。把Slider的Value属性的值初始化为90:
<Slider Name="slider" Value="90" ... />
绑定的目标是TextBlock的Text属性:
<TextBlock Name="txtblk" Text="{Binding ElementName=slider, Path=Value}" ... />
让我们切换一下,把TextBlock的Text属性初始化为90:
<TextBlock Name="txtblk" Text="90" .../>
然后把Slider的Value属性作为绑定目标:
<Slider Name="slider" Value="{Binding ElementName=txtblk, Path=Text}" .../>
乍一看这似乎能正常地工作。Slider的滚动块最初放在中间,这表示Slider的值为90,该值从TextBlock获取,而Rectangle的大小仍然与Slider绑定。然而,当你滑动Slider时,Rectangle的高度改变了,但TextBlock却没有发生任何变化。Slider上的Binding对象正等待着TextBlock的Text属性发生变化,但Text属性没有任何的改变。
现在为Slider上的绑定添加Mode设置,下面的代码表示这个数据绑定是双向(two-way)的。
<Slider Name="slider" Value="{Binding ElementName=txtblk, Path=Text, Mode=TwoWay}" .../>
现在能正常工作了!绑定的目标仍然是Slider的Value属性。TextBlock的Text属性变化时会影响Slider的Value属性,同样,现在Slider的Value属性变化时也反过来影响到TextBlock。
Mode属性的值为BindingMode枚举类型的成员。Mode属性的默认值是BindingMode. OneWay,除此之外还有BindingMode.TwoWay和BindingMode.OneTime,BindingMode. OneTime表示源只传输一次数据到目标。
使用同样的技巧,可以为RotateTransform的Angle属性建立起绑定关系。首先,把TextBlock上的绑定还原到原始状态:
<TextBlock Name="txtblk" Text="{Binding ElementName=slider, Path=Value}" .../>
现在为Slider设置双向绑定,指向RotateTransform的Angle属性:
<Slider Name="slider" Value="{Binding ElementName=rotate, Path=Angle, Mode=TwoWay}" .../>
运行得很好!当滑动Slider时,Rectangle元素也相应地旋转,如图12-1所示。
绑定转换器
当你运行SliderBindings程序时(或者当你看到该截图的时候会感到惊讶),开始的时候你可能看到TextBlock显示Slider的值有时是整数,有时是包含一或两个小数位的浮点数,但更多的时候看到的是15位的双精度浮点数。
有办法解决这个问题吗?
有,Binding类的其中一个叫做Converter的属性,这个属性的作用是引用一个可以在从源到目标的过程中进行数据转换的类,(如果有需要)也可以从目标转换回源。显然,我们已经使用了一些隐式的数据转换,例如数字转换为字符串,或者字符串转换为数字。但是,我们可以提供一些更明确的手段来协助转换的过程。
Binding类Converter属性是IValueConverter类型,IValueConverter是一个接口,它包含了两个方法:Convert和ConvertBack。Convert方法处理从源到目标的数据转换,而ConvertBack方法处理TwoWay(双向)绑定时另外一个方向的转换。
如果你的转换类从来不使用在双向绑定上,那么在ConvertBack方法中简单地返回null就可以了。
为了给SliderBindings程序添加一个简单的转换器,在项目中增加一个名为Truncation- Converter的类。其实这个类已经在项目中,如下所示:
Silverlight项目:SliderBindings 文件:TruncationConverter.cs using System; using System.Globalization; using System.Windows.Data; namespace SliderBindings { public class TruncationConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is double) return Math.Round((double)value); return value; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return value; } } }
Convert方法的value参数是从源传递到目标的对象。这个方法检查value是否为double类型。如果是double,那么显式地调用Math.Round方法把value转换成double。
如果需要在MainPage.xaml引用这个类,你需要使用XML命名空间声明:
xmlns:local="clr-namespace:SliderBindings"
然后把TruncationConverter类作为资源:
<phone:PhoneApplicationPage.Resources> <local:TruncationConverter x:Key="truncate" /> ... </phone:PhoneApplicationPage.Resources>
你会发现这些已经存在于SliderBindings项目的MainPage.xaml文件中了。
然后Binding扩展标记就可以引用这个资源:
<TextBlock Name="txtblk" Text="{Binding ElementName=slider, Path=Value,
我把扩展标记分成三行使各个部件更加清晰可见。注意StaticResource也是一个扩展标记,它嵌套在第一个标记扩展中,因此整个表达式包含了两个花括号。
现在TextBlock显示的数字被截断了,如图12-2所示。
记住,把转换器定义为StaticResource。很多时候,我可能不由自主地将Binding的Converter属性设置为静态资源的键名:
<! — 这是错误的 !--> <TextBlock Name="txtblk" Text="{Binding ElementName=slider, Path=Value, Converter=truncate}" ... />
我自己经常这样做,但是这种问题却难以追踪。
使用转换器最常用的方法是把转换器定义为资源,但是这不是唯一的方法。如果你使用Binding的元素语法,可以直接把TrunctionConverter类嵌入到标记中:
<TextBlock ... > <TextBlock.Text> <Binding ElementName="slider" Path="Value"> <Binding.Converter> <local:TruncationConverter /> </Binding.Converter> </Binding> </TextBlock.Text> </TextBlock>
不过,如果你在一个XAML文件中多次使用相同的转换器,最好还是把它定义为资源,这样能共享唯一实例。
TrucationConverter实际上是一个糟糕的数据转换器。当然它能完成它应该做的工作,但完成的方式并不灵活。如果你要在转换器类中调用Math.Round方法,那么提供一个小数位舍入功能不是更好吗?试想一下,如果有一个不仅支持数字,而且还支持不同的格式和各种数据类型的功能,那岂不是更好?
这种魔法般的功能由Petzold.Phone.Silverlight库里面的StringFormatConverter类所提供:
Silverlight项目:Petzold.Phone.Silverlight 文件:StringFormatConverter.cs using System; using System.Globalization; using System.Windows.Data; namespace Petzold.Phone.Silverlight { public class StringFormatConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (targetType == typeof(string) && parameter is string) return String.Format(parameter as string, value); return value; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return value; } } }
除了Converter属性以外,Binding类包含一个叫做ConverterParameter的属性。该属性的值是调用Convert时传递给parameter的参数。Convert方法假设该parameter是.NET标准的格式化字符串,该字符串可用来调用String.Format。
为了在SliderBindings程序中使用这个转换器,你需要引用Petzold.Phone.Silverlight库。(这里已经引用了。)并在文件中添加XML命名空间声明:
xmlns:petzold="clr-namespace:Petzold.Phone.Silverlight;assembly=Petzold.Phone.Silv-erlight"
下面的代码用于实例化页面中Resources集合里的StringFormatConverter:
<phone:PhoneApplicationPage.Resources> ... <petzold:StringFormatConverter x:Key="stringFormat" /> </phone:PhoneApplicationPage.Resources>
现在你可以在Binding标记表达式中引用这个转换器了。将ConverterParameter设置为.NET格式化字符串的一个占位符(Placeholder)的:
Text="{Binding ElementName=slider,
Path=Value,
Converter={StaticResource stringFormat},
ConverterParameter=...}"
当你输入.NET格式化字符串时,你会发现一个问题。标准.NET的格式化字符串需要使用大括号,但你也知道当XAML解析器(XAML parser)解码Binding标记表达式时,并不会喜欢非法嵌入的大括号。
简单的解决方法是把ConverterParameter的值用单引号括起来:
Text="{Binding ElementName=slider,
Path=Value,
Converter={StaticResource stringFormat},
ConverterParameter='{0:F2}'}"
Visual Studio中的XAML解析器和可视化设计器(visual designer)不喜欢这种特定的语法,但是运行时却没有问题。如果你想让设计器接受这种语法,在第一个单引号后插入一个空格(或者其他字符)就可以了。
因为ConverterParameter是String.Format调用的第一个参数,你可以把它完善一下:
Text="{Binding ElementName=slider,
Path=Value,
Converter={StaticResource stringFormat},
ConverterParameter='The slider is {0:F2}'}"
结果如图12-3所示。
相对绑定源
根据绑定数据源来分类,Silverlight for Windows Phone支持三种不同的绑定类型。到目前为止,这一章已经介绍了ElementName绑定,这种绑定指向一个命名元素。在本章后面的部分,主要使用Source属性来代替ElementName属性指向数据源。
第3类绑定称为RelativeSource(相对绑定源)。在Windows Presentation Foundation中的RelativeSource比Silverlight中的RelativeSource灵活很多,所以你可能对这个选项没有深刻的印象。使用RelativeSource的一个目的是与模板相关联,你将在第16章看到这样的应用。最后一个选项叫做Self,使用Self可以定义指向自身元素属性的绑定。下面的程序演示了这种语法:
Silverlight项目:BindToSelf 文件:MainPage.xaml(节选) <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock Text="{Binding RelativeSource={RelativeSource Self}, Path=FontFamily}" /> <TextBlock Text=" - " /> <TextBlock Text="{Binding RelativeSource={RelativeSource Self}, Path=FontSize}" /> <TextBlock Text=" pixels" /> </StackPanel> </Grid>
RelativeSource属性是另一个扩展标记,其中包含RelativeSource和Self。Path指向同一个元素的另一个属性。在这个例子中,这两个TextBlock元素分别显示自身的TextBlock的FontFamily和FontSize属性。
this绑定源
也许你的应用程序有这样一个需求:需要显示很多简短的文本字符串,这些字符串都由边界所包围。你决定创建一个派生自UserControl类的控件,并将其命名为BorderedText,如下所示:
<petzold:BorderedText Text="Ta Da!" FontFamily="Times New Roman" FontSize="96" FontStyle="Italic" FontWeight="Bold" TextDecorations="Underline" Foreground="Red" Background="Lime" BorderBrush="Blue" BorderThickness="8" CornerRadius="36" Padding="16 4" HorizontalAlignment="Center" VerticalAlignment="Center" />
从XML命名空间的前缀可以判断出,这个类也在Petzold.Phone.Silverlight库中。
BorderedText派生自UserControl,而UserControl继承自Control,所以我们知道通过类继承BorderedText具备Control类的一些属性。BorderedText需要自己定义Text、TextDecorations、CornerRadius等属性,以及一些使其变得更加灵活的其他属性。
BorderedText.xaml文件中的可视化树很可能包含了一个Border,并且Border里包含了一个TextBlock。TextBlock和Border的属性都是通过BorderedText的属性来设置。
在前面的章节中,你已经见过实现这种功能的方法:ColorColumn类定义了Label和Value属性,然后在代码中使用属性变更处理程序为可视化树中元素的属性设置新值。使用数据绑定能简化这项工作。
BorderedText的代码隐藏文件定义了一些属性,其父类Control类并没有提供这些虚属性供其子类使用:
Silverlight项目:Petzold.Phone.Silverlight 文件:BorderedText.xaml.cs using System; using System.Windows; using System.Windows.Controls; namespace Petzold.Phone.Silverlight { public partial class BorderedText : UserControl { public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(BorderedText), new PropertyMetadata(null)); public static readonly DependencyProperty TextAlignmentProperty = DependencyProperty.Register("TextAlignment", typeof(TextAlignment), typeof(BorderedText), new PropertyMetadata(TextAlignment.Left)); public static readonly DependencyProperty TextDecorationsProperty = DependencyProperty.Register("TextDecorations", typeof(TextDecorationCollection), typeof(BorderedText), new PropertyMetadata(null)); public static readonly DependencyProperty TextWrappingProperty = DependencyProperty.Register("TextWrapping", typeof(TextWrapping), typeof(BorderedText), new PropertyMetadata(TextWrapping.NoWrap)); public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(BorderedText), new PropertyMetadata(new CornerRadius())); public BorderedText() { InitializeComponent(); } public string Text { set { SetValue(TextProperty, value); } get { return (string)GetValue(TextProperty); } } public TextAlignment TextAlignment { set { SetValue(TextAlignmentProperty, value); } get { return (TextAlignment)GetValue(TextAlignmentProperty); } } public TextDecorationCollection TextDecorations { set { SetValue(TextDecorationsProperty, value); } get { return (TextDecorationCollection)GetValue(TextDecorationsProperty); } } public TextWrapping TextWrapping { set { SetValue(TextWrappingProperty, value); } get { return (TextWrapping)GetValue(TextWrappingProperty); } } public CornerRadius CornerRadius { set { SetValue(CornerRadiusProperty, value); } get { return (CornerRadius)GetValue(CornerRadiusProperty); } } } }
这些代码有点长但其实很简单,因为都是一些属性的定义。并没有使用任何属性变更处理程序。下面是包含了Border和TextBlock的XAML文件:
Silverlight项目:Petzold.Phone.Silverlight 文件:BorderedText.xaml <UserControl x:Class="Petzold.Phone.Silverlight.BorderedText" 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" Name="this"> <Border Background="{Binding ElementName=this, Path=Background}" BorderBrush="{Binding ElementName=this, Path=BorderBrush}" BorderThickness="{Binding ElementName=this, Path=BorderThickness}" CornerRadius="{Binding ElementName=this, Path=CornerRadius}" Padding="{Binding ElementName=this, Path=Padding}"> <TextBlock Text="{Binding ElementName=this, Path=Text}" TextAlignment="{Binding ElementName=this, Path=TextAlignment}" TextDecorations="{Binding ElementName=this, Path=TextDecorations}" TextWrapping="{Binding ElementName=this, Path=TextWrapping}" /> </Border> </UserControl>
请注意根元素的命名:
Name="this"
你可以把这个根元素设置成你想要的名称,但使用C#的关键字this是最常见的做法,因为在该XAML文件中,this是指BorderedText类当前的实例,因此可以使用this这一熟悉的概念。你可以通过this这一名字把BorderedText的属性与可视化树中元素的属性建立绑定关系。
这个文件不需要为Foreground属性或者一些其他字体相关的属性进行数据绑定,因为这些属性都可以从可视化树中继承过来。TextBlock的Inlines属性使我感觉不爽。这是因为TextBlock把Inlines属性定义为只读(get-only)属性,所以没办法为它定义数据绑定。
BorderedTextDemo程序测试这个新控件:
Silverlight项目:BorderedTextDemo 文件:MainPage.xaml(节选) <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <petzold:BorderedText Text="Ta Da!" FontFamily="Times New Roman" FontSize="96" FontStyle="Italic" FontWeight="Bold" TextDecorations="Underline" Foreground="Red" Background="Lime" BorderBrush="Blue" BorderThickness="8" CornerRadius="36" Padding="16 4" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Grid>
通知机制
为了使数据绑定能正常运作,绑定源必须实现某种通知机制(notification mechanism)。当属性的值发生改变的时候,这种通知机制会发送通知信号,使得新的值可以从源传递到目标。当你绑定Slider的Value属性到TextBlock的Text属性时,同时使用到两个依赖属性。虽然你在公共的编程接口中看不出来,但是就是这些依赖属性提供了通知机制。
使用数据绑定可以非常方便地关联两个可视化元素,但以可视化元素作为绑定目标,使用业务对象(business object)来代替可视化元素作为绑定源的数据绑定功能最强大。
这里需要提醒一下:
有时候,当程序员学习了操作系统的一个全新且重要的功能时(例如我在前面讨论过的依赖属性),他们就觉得需要在各个地方使用该功能,也许这样做只是为了多练习一下。对于依赖特性来说,这种做法不太可取。当然,如果你的父类的父类已经是DependencyObject的派生类,那么可以使用依赖属性,否则不应该单纯地为了使用依赖属性而继承DependencyObject类。
换句话说:不要为了使用依赖属性而重写业务对象!
数据绑定的目标必须是依赖属性,但是对于绑定的源,并没有严格要求。绑定源可以是普通类的普通属性。但是如果你希望绑定源在变化的时候,绑定目标也随之自动更新,那么绑定源必须实现某种通知机制。
通常用作绑定源的业务对象需要实现的通知机制称为INotifyPropertyChanged接口。
INotifyPropertyChanged定义在System.ComponentModel命名空间。(这清楚地表明该接口不仅仅在Silverlight中,而且在.NET中扮演着非常重要的角色。)这就是业务对象提供数据变更通知的方法。
INotifyPropertyChanged的定义非常简单,如下所示:
public interface INotifyPropertyChanged { event PropertyChangedEventHandler PropertyChanged; }
实现INotifyPropertyChanged接口的类只需要简单地定义一个公共事件Property- Changed。理论上,这个派生类并不需要为这个事件做任何特殊的处理,但是当它的某个属性发生变化时,可以通过这个事件来触发变更事件。
PropertyChangedEventHandler委托与PropertyChangedEventArgs类相关联,Pro- pertyChangedEventArgs类只有一个string类型的只读属性PropertyName,你要将发生变化的属性名传递给PropertyChangedEventArgs的构造函数。
有时候实现INotifyPropertyChanged接口的类需要定义一个受保护的虚方法OnPropertyChanged,该方法具有一个PropertyChangedEventArgs类型的参数。这个方法不是必须的,但能给派生类带来便利。我在这个例子中使用了该方法,因为在该方法中可以方便触发事件。
由于实现了INotifyPropertyChanged接口的业务对象并没有继承FrameworkElement,它们并不是XAML文件中可视化树的组成部分,因此它们通常会被实例化为XAML的资源或者位于代码隐藏文件中。
简单的绑定服务
有时我觉得使用业务对象的本意是在XAML文件中进行绑定,这种绑定称为绑定服务(binding server)。绑定服务公开了一些公共属性,当这些属性发生变化时,绑定服务会触发相应的PropertyChanged事件。
例如,假设你想在Windows Phone 7应用程序中显示当前时间,并且提供灵活的呈现方式。有时你只想显示秒,并完全通过XAML来完成这个功能。例如,你想通过XAML显示:“目前是X秒”,中间放一个每秒钟更新一次的数字。当然这里讲述的技巧可以扩展到许多其他的应用中,而不仅仅是一个钟表应用。
虽然可以完全通过XAML实现整体的可视元素,但是你还是需要一些辅助的代码(可能是一个命名为Clock的类,该类包含了Year、Month、Day、DayOfWeek、Hour、Minute和Second等属性。)我们将在XAML文件中实例化这个Clock类并通过数据绑定来访问它的属性。
如你所知,在.NET中已经存在一个包含Year、Month和Day等相关属性的结构体:DateTime。尽管DateTime对于编写Clock类是必不可少的,但是它并不大符合我们的需求,因为DateTime的属性不可以动态更改。相反,我要演示的Clock类所包含的属性会实时地反映当前的时间,而且会通过PropertyChanged事件通知外部世界相应的变化。
Clock类包含在Petzold.Phone.Silverlight库中,如下所示:
Silverlight项目:Petzold.Phone.Silverlight 文件:Clock.cs using System; using System.ComponentModel; using System.Windows.Threading; namespace Petzold.Phone.Silverlight { public class Clock : INotifyPropertyChanged { int hour, min, sec; DateTime date; public event PropertyChangedEventHandler PropertyChanged; public Clock() { OnTimerTick(null, null); DispatcherTimer tmr = new DispatcherTimer(); tmr.Interval = TimeSpan.FromSeconds(0.1); tmr.Tick += OnTimerTick; tmr.Start(); } public int Hour { protected set { if (value != hour) { hour = value; OnPropertyChanged(new PropertyChangedEventArgs("Hour")); } } get { return hour; } } public int Minute { protected set { if (value != min) { min = value; OnPropertyChanged(new PropertyChangedEventArgs("Minute")); } } get { return min; } } public int Second { protected set { if (value != sec) { sec = value; OnPropertyChanged(new PropertyChangedEventArgs("Second")); } } get { return sec; } } public DateTime Date { protected set { if (value != date) { date = value; OnPropertyChanged(new PropertyChangedEventArgs("Date")); } } get { return date; } } protected virtual void OnPropertyChanged(PropertyChangedEventArgs args) { if (PropertyChanged != null) PropertyChanged(this, args); } void OnTimerTick(object sender, EventArgs args) { DateTime dt = DateTime.Now; Hour = dt.Hour; Minute = dt.Minute; Second = dt.Second; Date = DateTime.Today; } } }
Clock类实现了INotifyPropertyChanged接口,因此包含PropertyChanged公共事件。靠近结尾的地方,有一个受保护的OnPropertyChanged方法,负责触发实际的事件。在Clock类的构造函数中创建DispatcherTimer的实例并为它的Tick事件注册了一个事件处理程序,这个处理程序的调用间隔(Interval)为1/10秒。在类最底部的OnTimerTick处理程序负责给这个类的Hour、Minute、Second和Data属性设置新值,这些属性的结构都非常相似。
例如,看一下Hour属性:
public int Hour { protected set { if (value != hour) { hour = value; OnPropertyChanged(new PropertyChangedEventArgs("Hour")); } } get { return hour; } }
set存储器是受保护的。因此该值只能在内部设置,我们并不希望外部的类设置该属性。set存取器检查正在设置的值与存储在字段中的值是否相同,如果不相同,就把新值设置到hour字段中,同时调用OnPropertyChanged来触发更新事件。
有些程序员不使用if语句来检查属性是否发生变化,其结果是一旦属性被设置就马上触发PropertyChanged事件,即使该属性的值并没有发生变化也是如此。这并不是一个好主意,特别是像这样的类。我们不希望每隔1/10秒PropertyChanged事件就报告Hour属性发生了变化,其实该属性每一小时才改变一次。
要在XAML文件中使用Clock类,你需要声明一个XML命名空间来引用Petzold.Phone.Silverlight库:
xmlns:petzold="clr-namespace:Petzold.Phone.Silverlight;assembly=Petzold.Phone.Silverlight"
当绑定源不是DependencyObject的子类时,你会在Binding中使用Source属性来代替ElementName属性。我们要创建的绑定的Source属性设置为Petzold.Phone.Silverlight库中的Clock对象。
你可以直接在Binding中插入Clock类的引用:
<TextBlock> <TextBlock.Text> <Binding Path="Second"> <Binding.Source> <petzold:Clock /> </Binding.Source> </Binding> </TextBlock.Text> </TextBlock>
Binding的Source属性是一个属性元素,并设置为Clock类的实例。Path属性指向Clock类的Second属性。
或者按照惯例把Clock类定义为XAML的资源:
<phone:PhoneApplicationPage.Resources> <petzold:Clock x:Key="clock" /> ... </phone:PhoneApplicationPage.Resources>
然后就可以在Binding扩展标记中引用该资源了:
TextBlock Text="{Binding Source={StaticResource clock}, Path=Second}" />
注意StaticResource的嵌入标记表达式(embedded markup expression)。
TimeDisplay项目演示了这种方法,该项目使用了水平StackPanel来串联文本:
Silverlight项目:TimeDisplay 文件:MainPage.xaml <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock Text="The current seconds are " /> <TextBlock Text="{Binding Source={StaticResource clock}, Path=Second}" /> </StackPanel> </Grid>
运行效果如图12-4所示。
再次强调:绑定目标(例如TextBlock的Text属性)必须是依赖属性。为了使绑定目标随着绑定源(例如Clock的Second属性)的变化而更新,绑定源必须实现某种通知机制。
当然,我并不想使用承载多个TextBlock元素的StackPanel。可以使用StringFormat- Converter(我已在TimeDisplay项目中把它定义为资源,并指定了它的键为StringFormat,因此你现在可以使用它了),我可以简单地在一个TextBlock中包括整段文字,如下所示:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding Source={StaticResource clock}, Path=Second, Converter={StaticResource stringFormat}, ConverterParameter='The current seconds are {0}'}" /> </Grid>
现在Binding标记表达式包含了两个嵌入标记表达式。
如果想显示Clock类的多个属性,你可以回去使用多个TextBlock元素。例如,可以通过冒号来分割时、分和秒,同时在分和秒前补0:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock Text="{Binding Source={StaticResource clock}, Path=Hour}" /> <TextBlock Text="{Binding Source={StaticResource clock}, Path=Minute, Converter={StaticResource stringFormat}, ConverterParameter=':{0:D2}'}" /> <TextBlock Text="{Binding Source={StaticResource clock}, Path=Second, Converter={StaticResource stringFormat}, ConverterParameter=':{0:D2}'}" /> </StackPanel> </Grid>
如你所见,3个绑定都使用了相同的Source设置。是否有方法让我们避免这种重复呢?有,并且这种技巧是一个非常重要的概念。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
原文首发《图灵社区》
出处:http://procoder.cnblogs.com
本作品由Jake Lin创作,采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。 任何转载必须保留完整文章,在显要地方显示署名以及原文链接。如您有任何疑问或者授权方面的协商,请给我留言。