WPF学习(8)数据绑定
说到数据绑定,其实这并不是一个新的玩意儿。了解asp.net的朋友都知道,在asp.net中已经用到了这个概念,例如Repeater等的数据绑定。那么,在WPF中的数据绑定相比较传统的asp.net中的数据绑定又有哪些优点呢?
1)具有双向性,即从源到目标是双向的
2)及时更新,源发生改变时,能够及时更新UI
3)Validation和Converter,前者保证数据的合法性,后者保证数据的有效性
接下来,我们将从这么几个方面来说明:Binding对象(对应xaml中的Binding扩展标记)、Binding的Path以及Source、Validation及Converter和MultiBinding。
1.Binding对象
在Binding对象中,主要成员可以分为这么几类:
1)路径:Path属性和XPath属性
2)源:Source、RelativeSource和ElementName
3)更新通知:NotifyOnSourceUpdated、NotifyOnTargetUpdated和NotifyOnValidationError
4)转换器:Converter、ConverterCulture和ConverterParameter
5)验证:ValidatesOnDataErrors、ValidatesOnExceptions、ValidatesOnNotifyDataErrors和ValidationRules
6)绑定方式:Mode,BindingMode枚举类型:TwoWay,OneWay,OneTime,OneWayToSource和Default
需要注意的是:Binding的目标必须是依赖对象的某个依赖属性。
2.Binding的Path以及Source
对于Binding的Path及Source,并非要是依赖属性及依赖对象。几乎任何一个对象都可以作为Binding的Source,主要有普通CLR对象、ado.net对象、XML、Linq、依赖对象、容器的DataContext、RelativeSource和ObjectDataProvider等。
而Path就是普通CLR对象、容器的DataContext、RelativeSource和Linq的某个属性、
ado.net对象的某个字段、依赖对象的某个依赖属性和ObjectDataProvider的某个方法名。
这里需要注意一下几点:
1)有Path没Source,将去找其父元素有该Path的DataContext;
2)有Source没Path,则将Source也作为其Path;
3)无Source无Path,则将其父元素的DataContext既作为Source也作为Path。
关于Binding的Source这里只说下Xml、Linq to Xml和ObjectDataProvider,其它几种略过。
2.1使用Xml作为Binding Source
首先,我们来准备xml数据,命名为Students.xml,如下:
<?xml version="1.0" encoding="utf-8" ?> <Students> <Student ID="1"> <Name>Jello</Name> <Score>80</Score> </Student> <Student ID="2"> <Name>Taffy</Name> <Score>100</Score> </Student> </Students>
Xaml代码如下:
<Window x:Class="DataBindingDemo.XmlWindow1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="XmlWindow1" Height="300" Width="300"> <Grid> <ListBox x:Name="lb"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock x:Name="tbID" Text="{Binding XPath=@ID}" Width="20" Foreground="Red"/> <TextBlock x:Name="tbName" Text="{Binding XPath=Name}" Width="40" Foreground="Green"/> <TextBlock x:Name="tbScore" Text="{Binding XPath=Score}" /> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window>
cs代码如下:
/// <summary> /// XmlWindow1.xaml 的交互逻辑 /// </summary> public partial class XmlWindow1 : Window { public XmlWindow1() { InitializeComponent(); /*第一种写法: XmlDocument doc = new XmlDocument(); doc.Load(@"./Students.xml"); XmlDataProvider xdp = new XmlDataProvider(); xdp.Document = doc; xdp.XPath = "/Students/Student"; this.lb.SetBinding(ListBox.ItemsSourceProperty, new Binding(".") { Source = xdp }); */ //第二种写法: XmlDataProvider xdp = new XmlDataProvider(); xdp.Source = new Uri(@"F:\dotnet\WPF\学习\WPF\Demo\WpfDemo\DataBindingDemo\Students.xml"); xdp.XPath = "/Students/Student"; this.lb.DataContext = xdp; this.lb.SetBinding(ListBox.ItemsSourceProperty, new Binding()); } }
第一种写法借助于XmlDataProvider的Document属性,第二种写法借助于XmlDataProvider的Source属性。
如果想直接在Xaml代码里面来直接使用XmlDataProvider来进行绑定的话,需要将Xml数据放在<x:XData>...</x:XData>标签内,代码如下:
<Window x:Class="DataBindingDemo.XmlWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="XmlWindow" Height="300" Width="300"> <Window.Resources> <XmlDataProvider x:Key="xdp" XPath="Students/Student"> <x:XData> <Students xmlns=""> <Student ID="1"> <Name>Jello</Name> <Score>80</Score> </Student> <Student ID="2"> <Name>Taffy</Name> <Score>100</Score> </Student> </Students> </x:XData> </XmlDataProvider> </Window.Resources> <Grid> <ListBox x:Name="lb" ItemsSource="{Binding Source={StaticResource xdp}}"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding XPath=@ID}" Width="20" Foreground="Red"/> <TextBlock Text="{Binding XPath=Name}" Width="40" Foreground="Green"/> <TextBlock Text="{Binding XPath=Score}" /> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window>
效果如下:
2.2使用Linq to Xml作为Binding Source
在使用Linq to Xml作为Binding Source之前,我们当然需要准备model,这里创建一个Student实体类:
public class Student { public int ID { get; set; } public string Name { get; set; } public int Score { get; set; } }
Xaml代码如下:
<Window x:Class="DataBindingDemo.XmlWindow2" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="XmlWindow2" Height="300" Width="300"> <Grid> <ListBox x:Name="lb"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock x:Name="tbID" Text="{Binding Path=ID}" Width="20" Foreground="Red"/> <TextBlock x:Name="tbName" Text="{Binding Path=Name}" Width="40" Foreground="Green"/> <TextBlock x:Name="tbScore" Text="{Binding Path=Score}" /> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window>
cs代码如下:
/// <summary> /// XmlWindow2.xaml 的交互逻辑 /// </summary> public partial class XmlWindow2 : Window { public XmlWindow2() { InitializeComponent(); XDocument doc = XDocument.Load(@"F:\dotnet\WPF\学习\WPF\Demo\WpfDemo\DataBindingDemo\Students.xml"); this.lb.ItemsSource = from e in doc.Descendants("Student") select new Student { ID = Int32.Parse(e.Attribute("ID").Value), Name = e.Element("Name").Value, Score = Int32.Parse(e.Element("Score").Value) }; } }
2.3使用ObjectDataProvider作为Binding Source
其他对象都是针对属性作为Path的情况,而ObjectDataProvider对象主要是针对方法。所以,我们先准备一个具有Add方法的Calculate类:
public class Calculate { public double Add(string d1, string d2) { double i, j; if (double.TryParse(d1, out i) && double.TryParse(d2, out j)) return i + j; else return 0; } }
Xaml代码如下:
<Window x:Class="DataBindingDemo.ODPWindow1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="ODPWindow1" Height="300" Width="300"> <Grid> <StackPanel> <TextBox x:Name="tb1"/> <TextBox x:Name="tb2"/> <TextBox x:Name="tbResult"/> </StackPanel> </Grid> </Window>
cs代码如下:
/// <summary> /// ODPWindow1.xaml 的交互逻辑 /// </summary> public partial class ODPWindow1 : Window { public ODPWindow1() { InitializeComponent(); ObjectDataProvider odp = new ObjectDataProvider(); odp.ObjectInstance = new Calculate(); odp.MethodName = "Add"; odp.MethodParameters.Add("0"); odp.MethodParameters.Add("0"); this.tb1.SetBinding(TextBox.TextProperty, new Binding("MethodParameters[0]") { Source = odp, BindsDirectlyToSource = true, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged }); this.tb2.SetBinding(TextBox.TextProperty, new Binding("MethodParameters[1]") { Source = odp, BindsDirectlyToSource = true, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged }); this.tbResult.SetBinding(TextBox.TextProperty, new Binding(".") { Source = odp }); } }
效果如下:
3.Binding的Converter
数据在Binding的Target端和Source端交换时,经常会出现类型或者格式不一致的情况,这时候,我们就可以使用Converter来处理。
WPF内置了许多的Converter,例如:
<Grid.Background> Red </Grid.Background>
Backgroud属性是Brush抽象类型,而我们只是用一个Red字符串赋值就能达到效果,这是内置的从String类型到SolidColorBrush类型的Converter。
我们也可以实现自己的Converter,只要实现IValueConverter接口即可。
4.Binding的Validation
数据在Binding的Target端和Source端交换时,除了经常出现类型或者格式不一致,还出现数据不合法的情况。为了避免脏数据的出现,需要在交换前进行Validate。例如:
<StackPanel> <TextBox x:Name="tb" Text="{Binding Value,ElementName=slider,UpdateSourceTrigger=PropertyChanged}"/> <Slider x:Name="slider" Minimum="0" Maximum="99" /> </StackPanel>
这里,我们要实现的效果是在TextBox中输入一个0到99之间的数字,Slider会划到相应位置,若输入的数字不在该范围,则TextBox提示数据不合法。
Xaml代码如下:
<StackPanel> <TextBox x:Name="tb" /> <Slider x:Name="slider" Minimum="0" Maximum="200" /> </StackPanel>
cs代码如下:
/// <summary> /// ConverterWnd.xaml 的交互逻辑 /// </summary> public partial class ConverterWnd : Window { public ConverterWnd() { InitializeComponent(); Binding binding = new Binding("Value") { Source = this.slider, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged, }; RangeValidation rv = new RangeValidation(); rv.ValidatesOnTargetUpdated = true;//验证Source binding.NotifyOnValidationError = true;//触发Validation.ErrorEvent binding.ValidationRules.Add(rv); this.tb.SetBinding(TextBox.TextProperty, binding); this.tb.AddHandler(Validation.ErrorEvent, new RoutedEventHandler(NotifyError)); } protected void NotifyError(object sender, RoutedEventArgs e) { TextBox tb = sender as TextBox; if (tb != null) { if (Validation.GetErrors(tb).Count > 0) tb.ToolTip = Validation.GetErrors(tb)[0].ErrorContent.ToString(); } } } public class RangeValidation : ValidationRule { public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo) { double d; if (double.TryParse(value.ToString(), out d)) { if (d >= 0 && d <= 99) return new ValidationResult(true, null); } return new ValidationResult(false, "数据不合法"); } }
这里将Slider的最大值设为了200,当其值大于99时,由于ValidatesOnTargetUpdate=true,所以也会路由Validation.ErrorEvent事件。
效果如下:
其实,当未添加验证时,Slider的CoerceValue会强制处理。
5.MultiBinding多路绑定
经常会遇到这样的需求,要求显示“开始日期 -- 结束日期”这样的格式,这时候比较好的做法就是使用MultiBinding,当然,你也可以重新定义一个属性。
Xaml代码如下:
<Grid> <StackPanel> <TextBlock Text="{Binding Name}"/> <TextBlock> <TextBlock.Text> <MultiBinding StringFormat="{}{0:yyyy-MM-dd}至{1:yyyy-MM-dd}"> <Binding Path="StartDate" /> <Binding Path="EndDate" /> </MultiBinding> </TextBlock.Text> </TextBlock> </StackPanel> </Grid>
cs代码如下:
/// <summary> /// MultiBindingWnd.xaml 的交互逻辑 /// </summary> public partial class MultiBindingWnd : Window { public MultiBindingWnd() { InitializeComponent(); Fruit fruit = new Fruit() { Name = "Apple", StartDate = DateTime.Today, EndDate = DateTime.Today.AddYears(1) }; this.DataContext = fruit; } }
这里需要注意两点:
1)MultiBinding添加Binding的顺序会影响Converter
2)MultiBinding的Converter实现的是IMultiValueConverter接口