Binding就是搭建在Source与Target之间的桥梁,数据就是桥梁上来往的车辆,但是某些情况下,桥梁上可设置关卡即对数据的有效性进行校验。同时对于桥梁两端不同类型的数据,可以设置转换器。Binding的有效性校验的关卡是ValidationRules属性,数据类型转换的关卡是Converter属性。
DataBinding数据校验
Binding的ValidationRules属性类型为Collection<ValidationRule>,从类型和名称可以得出,它可以为每个Bingding设置多个数据校验条件,每个条件是ValidationRule类型对象。ValidationRule是抽象类,所以需要创建它的派生类并实现它的Validate方法。Validate方法返回值是ValidationResult类型对象,如果校验通过,把ValidationResult的IsValid属性设为true,反正为false并为ErrorContent属性设置消息字符串。
下面已Slider为源,TextBox为目标。Slider取值范围是0到100,因此需要校验TextBox里输入的值是不是在0到100这个范围。
XAML代码如下:
<Window x:Class="Demo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="120" Width="300">
<StackPanel Background="AliceBlue" Margin="10">
<TextBox Name="textBox1" Margin="5"/>
<Slider Name="slider1" Margin="5" Maximum="100" Minimum="0"/>
</StackPanel>
</Window>
创建ValidationRule派生类:
public class RangeValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
double d = 0;
if (double.TryParse(value.ToString(), out d))
{
if (d >= 0 && d <= 100)
{
return new ValidationResult(true, null);
}
}
return new ValidationResult(false, "ErrorContent");
}
}
建立Binding:
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
Binding bind = new Binding("Value") { Source = slider1};
bind.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
RangeValidationRule rvr = new RangeValidationRule();
bind.ValidationRules.Add(rvr);
this.textBox1.SetBinding(TextBox.TextProperty, bind);
}
}
运行程序,当输入0到100之间的值的时候正常显示,当不在这个范围内时,TextBox会显示红色边框,表示值是错误的,不能把它传递给源,如下所示:
Binding校验时默认总是认为Source的数据总是正确的,即不进行校验。因此只有来自Target的数据才有可能有问题,为了不让有问题的数据影响Source,才需要校验Target。如果想校验Source,则需要将校验条件ValidatesOnTargetUpdated属性设为true。
设置错误消息提示,如何显示消息呢?这里需要用到--路由事,(Routed Event)。创建Binding时设置NotifyOnValidationError属性设为true。当数据校验失败,Binding会发出报警信号,从Binding对象的Target为起点在UI元素树路由,即信号在树上传递的过程就称为路由(Route)。信号会被拦截到每个被设置信号侦听器(事件处理器)的节点,即触发侦听器。信号处理完毕,你可以选择信号继续向下传播还是终止(路由事件)。
Binding代码如下:
public Window3()
{
InitializeComponent();
Binding bind = new Binding("Value") { Source = slider1 };
bind.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
RangeValidationRule rvr = new RangeValidationRule();
rvr.ValidatesOnTargetUpdated = true;
bind.ValidationRules.Add(rvr);
bind.NotifyOnValidationError = true;
this.textBox1.SetBinding(TextBox.TextProperty, bind);
this.textBox1.AddHandler(Validation.ErrorEvent, new RoutedEventHandler(ValidationError));
}
用于侦听错误的事件处理器如下:
private void ValidationError(object sender, RoutedEventArgs e)
{
if (Validation.GetErrors(textBox1).Count > 0)
{
this.textBox1.ToolTip = Validation.GetErrors(textBox1)[0].ErrorContent.ToString();
}
else
{
this.textBox1.ToolTip = "";
}
}
Validation.GetErrors(textBox1)[0],这里稍作解释,GetErrors返回ReadOnlyObservableCollection<ValidationError>类型,即静态的ObservableCollection<T> 类,它是集合。因为只有一个ValidationError对象,所有只能有索引0。
如果校验失败,ToolTip就会显示,如下图所示:
Binding的数据转换
Binding存在一种称为数据转换(Data Convert)的机制,当Source端Path所关联的数据与Target端目标属性数据类型不一致时,可以添加数据转换器(Data Converter)。比较简单的类型,如上面的例子double转string,即系统的基本类型,WPF类库就自动实现了。但遇到某些复杂的情况,如:
- Source数据对象有a、b、c三个值(可能是int、string、自定义类型),UI上比如对应CheckBox控件,需要把这三个值映射为它的IsChecked属性值(bool类型);
- 当TextBox里已经输入了文字时才有Button出现,这是string类型与Visibility枚举类型或者bool类型之间的转换(Binding的Mode是OneWay);
- Source里数据比如是Male或者Female(string或者枚举),UI上对应的是用于显示头像的Image控件,这时候需要把Source里的值转换成对应的头像图片URI(OneWay的Mode)。
遇到以上诸多复杂的情况,就需要自己实现Converter,创建一个类并让该类实现IValueConverter接口。IValueConverter接口定义如下:
public interface IValueConverter
{
object Convert(object value, Type targetType, object parameterm, CultureInfo culture);
object ConvertBack(object value, Type targetType, object parameterm, CultureInfo culture);
}
当数据从Binding的Source流向Target时,Convert方法将被调用;反之,ConvertBack方法将被调用。这两个方法基本一致,参数一模一样。重点第一个参数是object,它是转化的源对象;第二个参数targetType,即方法的返回类型,同时也是目标类型;第三个参数用于把额外信息传入方法;第四个参数是提供有关转换器中区域性的信息(对于非托管代码开发,则称为“区域设置”)。这些信息包括区域性的名称、书写系统、使用的日历以及对日期和排序字符串的格式化设置。
Binding对象的Mode属性会影响到这两个方法的调用。如果Mode为TwoWay,两个方法都会被调用;如果OneWay则是一个方法被调用。
下面是一个Converter实例,在列表里显示一些军用飞机的状态。
首先创建几个自定义类:
/// <summary>
/// 种类
/// </summary>
public enum Category
{
Bomber,
Fighter
}
/// <summary>
/// 状态
/// </summary>
public enum State
{
Available,
Locked,
Unknown
}
/// <summary>
/// 飞机
/// </summary>
public class Plane
{
public Category category { get; set; }
public State state { get; set; }
public string name { get; set; }
}
在解决方案里加入轰炸机和战斗机的图标,如下所示:
这里要做的是飞机的State属性映射为CheckBox,即State自定义类型与bool之间的双向转换;以及飞机的Category类型单向转换为图标,即Category自定义类型转换为string类型,代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;
namespace Demo.BLL
{
public class CategoryToSourceConverter:IValueConverter
{
// 将Category转化为Uri
public object Convert(object value, Type targetType, object parameters, System.Globalization.CultureInfo culture)
{
Category category = (Category)value;
switch (category)
{
case Category.Bomber:
return @"ICONS/Bomber.png";
case Category.Fighter:
return @"ICONS/Fighter.png";
default:
return null;
}
}
// 不会被调用
public object ConvertBack(object value, Type targetType, object parameters, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;
namespace WpfApplication1.BLL
{
public class StateToNullableBoolConverter:IValueConverter
{
// 将State转化为bool
public object Convert(object value, Type targetType, object parameters, System.Globalization.CultureInfo culture)
{
State state = (State)value;
switch (state)
{
case State.Available:
return true;
case State.Locked:
return false;
case State.Unknown:
default:
return null;
}
}
// 将bool转化为State
public object ConvertBack(object value, Type targetType, object parameters, System.Globalization.CultureInfo culture)
{
bool? nb = (bool?)value;
switch (nb)
{
case true:
return State.Available;
case false:
return State.Locked;
case null:
default:
return State.Unknown;
}
}
}
}
XAML代码如下:
<Window x:Class="Demo.Window4"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Demo.BLL"
Title="Data Convert" Height="266" Width="300">
<Window.Resources>
<local:CategoryToSourceConverter x:Key="cts" />
<local:StateToNullableBoolConverter x:Key="snb" />
</Window.Resources>
<StackPanel Background="LightBlue">
<ListBox Name="listBoxPlane" Height="160" Margin="5"/>
<Button Content="Load" Height="25" Name="Loadbutton" Margin="5,0" Click="Loadbutton_Click" />
<Button Content="Save" Height="25" Name="Savebutton" Margin="5,5" Click="Savebutton_Click" />
</StackPanel>
</Window>
XAML代码中添加了对程序集的引用并映射为名称空间local,以资源的形式创建两个Converter实例。ListBox是该例子的重点,需要为它添加显示数据的DataTemplate。如下:
<StackPanel Background="LightBlue">
<ListBox Name="listBoxPlane" Height="160" Margin="5">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Height="20" Width="20" Source="{Binding Path=category,Converter={StaticResource cts}}"/>
<TextBlock Text="{Binding Path=name}" Margin="80,0" Width="60"/>
<CheckBox IsChecked="{Binding Path=state,Converter={StaticResource snb}}" IsThreeState="True"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Load按钮的Click事件处理器负责把一组飞机的数据赋值给ListBox的ItemsSource属性,Save按钮的Click时间处理器负责把用户更改过得数据写入文件:
public partial class Window17 : Window
{
public Window17()
{
InitializeComponent();
}
/// <summary>
/// Load按钮事件处理器
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, RoutedEventArgs e)
{
List<Plane> infos = new List<Plane>() {
new Plane(){ category= Category.Bomber,name="B-1", state= State.Unknown},
new Plane(){ category= Category.Bomber,name="B-2", state= State.Unknown},
new Plane(){ category= Category.Fighter,name="F-22", state= State.Locked},
new Plane(){ category= Category.Fighter,name="Su-47", state= State.Unknown},
new Plane(){ category= Category.Bomber,name="B-52", state= State.Available},
new Plane(){ category= Category.Fighter,name="J-10", state= State.Unknown},
};
this.listBox1.ItemsSource = infos;
}
/// <summary>
/// Save按钮事件处理器
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button2_Click(object sender, RoutedEventArgs e)
{
StringBuilder sb = new StringBuilder();
foreach (Plane item in listBox1.Items)
{
sb.AppendLine(string.Format("Categroy={0},State={1},Name={2}",item.category,item.state,item.name));
}
File.WriteAllText(@"D:\PlaneList.text",sb.ToString());
}
}
运行程序并单击CheckBox更改飞机的State,效果如下:
单击Save按钮打开D:\PlaneList.txt,如下所示: