- 讓資料“為我所用”的Converter
- 讓資料“幹幹淨淨”的Validation
- 集合控件與集合資料的Binding
- 偷懶專用的資料中轉站——DataContext
Converter
我們用到的關卡就是“資料轉換器”(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";
- }
- }
- }
使用這個類的方法是将Binding執行個體的Converter屬性設定為這個類的一個執行個體:
- checkBox1.IsThreeState = true;
- Binding binding = new Binding("Text");
- binding.Source = textBox1;
- binding.Converter = new ConverterYN2TF(); // 設定Converter
- this.checkBox1.SetBinding(CheckBox.IsCheckedProperty, binding);
Validation
下面給出一個例子:我們以一個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);
集合Binding
我們得準備一個用來存放資料的集合,對于這個集合有一個特殊的要求,那就是,這個集合一定要是 實作了IEnumerable接口的集合 。為什麼呢?原因很簡單,實作了IEnumerable接口就意味着這個集合裡的元素是可枚舉的,可枚舉就意味着這個集合裡的元素是同一個類型的(至少具有相同的父類),元素是同一個類型的就意味着在每個元素中我都能找到同樣的屬性。
如果客戶要求顯示所有資訊,那這種“簡裝版”的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 });
- }
這回,我們使用的是ListBox的SelectedItem屬性——每當我們選中ListBox(包括其它ItemsControl)中的一個Item時,ListBox都會“默默地”自動從資料源集合裡選出與目前選中Item相對應的那個條目,作為自己的SelectedItem屬性值。而且,上面這個例子裡我們使用到了“多級路徑”—— "SelectedItem.Age",實際項目中,你可以一路“點”下去,直到取出你想要的值。
DataContext
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>
相應地,我們重構了一下Student類和Teacher類,讓它們趨于一緻:
- 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; }
- }
現在讓我們看看DataContext是怎麼玩兒的:
- 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 += (sender, e) => { this.DataContext = this.stuListBox.SelectedItem; };
tchrListBox.SelectionChanged += (sender, e) => { this.DataContext = this.tchrListBox.SelectedItem; };
這兩句是兩個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——原則就是“範圍正好,影響最小”。
這個玩意兒個人覺得最好少用,或者用的範圍小,否則調試或者維護非常麻煩。