- 让数据“为我所用”的Converter
- 让数据“干干净净”的Validation
- 集合控件与集合数据的Binding
- 偷懒专用的数据中转站——DataContext
我们用到的关卡就是“数据转换器”(Data Converter)。Converter实际上就是一个类,它这个类有个要求——它需要实现IValueConverter这个接口。这个接口的内容非常简单——只有两个方法,它们分别是:
- Convert方法:按照你的要求,把从数据源传来的数据转成你想要的数据
- ConvertBack方法:如果Binding是TwoWay的,那么数据目标会回传经用户改动后的数据,这时候你就不得不把数据转换回数据源里的格式——大多数情况下,它是Convert方法的逆运算,具体情况还要具体分析。
- [ValueConversion(typeof(string),
typeof(bool?))] //数据的源类型是string,目标类型是bool? - class
ConverterYN2TF : IValueConverter - {
-
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) -
{ -
string str = System.Convert.ToString(value); -
switch (str) -
{ -
case "Y": -
return true; -
case "N": -
return false; -
default: -
return null; -
} -
} -
-
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) -
{ -
bool? b = System.Convert.ToBoolean(value); -
switch (b) -
{ -
case true: -
return "Y"; -
case false: -
return "N"; -
default: -
return "Null"; -
} -
} - }
- checkBox1.IsThreeState
= true; - Binding
binding = new Binding("Text"); - binding.Source
= textBox1; - binding.Converter
= new ConverterYN2TF(); // 设定Converter - this.checkBox1.SetBinding(CheckBox.IsCheckedProperty,
binding);
下面给出一个例子:我们以一个Slider为数据源,它的滑块可以从Value=0滑到Value=100;同时,我们以一个TextBox为数据目标,并通过Validation限制它只能将20到35之间的数据传回数据源。现实当中恐怕很少有这么干的,我们这个例子只是为了说明校验的使用方法:)
若要创建一个自定义的校验条件,需要声明一个类,并让这个类派生自ValidationRule类。ValidationRule只有一个名为Validate的方法需要我们实现,这个方法的返回值是一个ValidationResult类型的实例——这个实例携带着两个信息:
- bool类型的IsValid属性告诉Binding回传的数据是否合法
- object类型(一般是存储一个string)的ErrorContent属性告诉Binding一些信息,比如当前是进行什么操作而出现的校验错误等等,一般我会把这些信息写进Log文件里
实现好的类是这样的:
- public
class MyValidationRule : ValidationRule - {
-
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo) -
{ -
double d = 0.0; -
if (double.TryParse((string)value, out d) && d >= 20 && d <= 35) -
{ -
return new ValidationResult(true, "OK"); -
} -
else -
{ -
return new ValidationResult(false, "Error"); -
} -
} - }
- Binding
binding = new Binding("Value"); - binding.Source
= slider1; - binding.UpdateSourceTrigger
= UpdateSourceTrigger.PropertyChanged; - binding.ValidationRules.Add(new
MyValidationRule()); // 加载校验条件 - textBox1.SetBinding(TextBox.TextProperty,
binding);
- public
Window1() - {
-
InitializeComponent(); -
List stuList = new List() -
{ -
new Student{StuNum=1, Name="Tim", Age=28}, -
new Student{StuNum=2, Name="Ma Guo", Age=25}, -
new Student{StuNum=3, Name="Yan", Age=25}, -
new Student{StuNum=4, Name="Xaiochen", Age=28}, -
new Student{StuNum=5, Name="Miao miao", Age=24}, -
new Student{StuNum=6, Name="Ma Zhen", Age=24} -
}; -
this.listBox1.ItemsSource = stuList; -
this.listBox1.DisplayMemberPath = "Name"; -
//this.listBox1.SelectedValuePath = "StuNum"; -
this.stuNumTextBox.SetBinding(TextBox.TextProperty, new Binding("SelectedItem.StuNum") { Source = this.listBox1 }); -
this.nameTextBox.SetBinding(TextBox.TextProperty, new Binding("SelectedItem.Name") { Source = this.listBox1 }); -
this.ageTextBox.SetBinding(TextBox.TextProperty, new Binding("SelectedItem.Age") { Source = this.listBox1 }); - }
WPF也为我们准备了一个用来放置数据的“制高点”——DataContext。
怎么理解这个数据制高点呢?让我们接着看上面的程序。现在客户的需求又变了:要求在窗体里显示两个ListBox,一个里面显示学生列表,一个里面显示老师列表,选中任何一个ListBox里的项,下面的TextBox都显示相应的详细信息。
这时候我们遇到困难了!因为一个UI元素不可能binding到两个数据源上啊!怎么办呢?这时候DataContext就派上用场了。
- <</span>Window
x:Class="CollectionBinding.Window1" -
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" -
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" -
Title="水之真谛" Height="300" Width="300"> -
<</span>StackPanel> -
<</span>ListBox Name="stuListBox" Margin="5" Height="70" Background="LightBlue"/> -
<</span>ListBox Name="tchrListBox" Margin="5" Height="70" Background="LightPink"/> -
<</span>TextBox Name="idTextBox" Margin="5" Background="LightGreen"/> -
<</span>TextBox Name="nameTextBox" Margin="5" Background="LightGreen"/> -
<</span>TextBox Name="ageTextBox" Margin="5" Background="LightGreen"/> -
</</span>StackPanel> - </</span>Window>
- interface
ISchoolMember - {
-
int ID { get; set; } -
string Name { get; set; } -
int Age { get; set; } - }
- class
Student : ISchoolMember - {
-
public int ID { get; set; } -
public string Name { get; set; } -
public int Age { get; set; } - }
- class
Teacher : ISchoolMember - {
-
public int ID { get; set; } -
public string Name { get; set; } -
public int Age { get; set; } - }
- public
Window1() - {
-
InitializeComponent(); -
List stuList = new List() -
{ -
new Student{ID=1, Name="Tim", Age=28}, -
new Student{ID=2, Name="Ma Guo", Age=25}, -
new Student{ID=3, Name="Yan", Age=25}, -
}; -
List tchrList = new List() -
{ -
new Teacher{ID=1, Name="Ma Zhen", Age=24}, -
new Teacher{ID=2, Name="Miao miao", Age=24}, -
new Teacher{ID=3, Name="Allen", Age=26} -
}; -
stuListBox.ItemsSource = stuList; -
tchrListBox.ItemsSource = tchrList; -
stuListBox.DisplayMemberPath = "Name"; -
tchrListBox.DisplayMemberPath = "Name"; -
stuListBox.SelectionChanged += (sender, e) => { this.DataContext = this.stuListBox.SelectedItem; }; -
tchrListBox.SelectionChanged += (sender, e) => { this.DataContext = this.tchrListBox.SelectedItem; }; -
this.idTextBox.SetBinding(TextBox.TextProperty, new Binding("ID")); -
this.nameTextBox.SetBinding(TextBox.TextProperty, new Binding("Name")); -
this.ageTextBox.SetBinding(TextBox.TextProperty, new Binding("Age")); - }
让我们来仔细品尝这段代码:
stuListBox.SelectionChanged
tchrListBox.SelectionChanged
这两句是两个Lambda表达式,实际上就是两个事件处理函数的缩写——让下游程序员不用跳转就知道两个ListBox在各自的SelectionChanged事件发生时都做什么事情。我们这里做的事情就是:哪个ListBox的选中项改变了,那就把选中的数据放到窗体的DataContext属性里,隐含地,就把前一个数据给挤走了。
有意思的是最后三句:在为三个TextBox设置Binding的时候,我没有提供数据源——但程序一样work,为什么呢?前面我说了,DataContext是“制高点”,当一个元素发现自己有Binding但这个Binding没有Source时,它就会“向上看”——它自然会看到制高点上的数据,这时候它会拿这个数据来试一试,有没有Binding所指示的Path——有,就拿来用;没有,就再往上层去找,也就是找更高的制高点
实际项目中,我会根据数据的影响范围来选择在哪一级上设置DataContext,以及把什么对象设置为DataContext。比如:一个ListBox里的SelectedItem需要被包含它的Grid里的其它元素共享,我就可以把ListBox.SelectedItem设置为Grid的DataContext,而没必要把ListBox设置为最顶层Window的DataContext——原则就是“范围正好,影响最小”。
这个玩意儿个人觉得最好少用,或者用的范围小,否则调试或者维护非常麻烦。