目錄
- 1. 定義
- 2. 背景
- 3. Binding源
- 3.1. 使用Data Context作為Binding的源
- 3.2. 使用LINQ檢索結果作為Binding的源
- 4. Binding對數據的轉換和校驗
- 4.1. 需求
- 4.2. 實現步驟
- 4.3. 值轉換和校驗的好處
- 4.3.1. 數據轉換的好處
- 4.4. 數據校驗的好處
- 4.5. 原理
- 4.5.1. 值轉換器原理
- 4.5.2. 數據校驗原理
前面的博文我們討論了Binding的Path知道了如何在一個對象身上尋找數據。本篇繼續看如何為Binding指定Source。
1. 定義
Binding
,出于方便業界一直使用Binding
一詞的音譯,即“綁定”。我理解Binding
更注重表達它是一種像橋梁一樣的關聯關系。WPF中,正是在這段橋梁上我們有機會為往來流通的數據做很多事情。
Binding
在源與目標之間架起了溝通的橋梁,默認情況下數據既能夠通過Binding
送達目標,也能夠從目標返回源(收集用戶對數據的修改)。
有時候數據只需要展示給用戶、不允許用戶修改,這時候可以把Binding模式更改為從源向目標的單向溝通。Binding
還支持從目標向源的單向溝通以及只在Binding
關系確立時讀取一次數據,這需要我們根據實際情況去選擇。
控制Binding
數據流向的屬性是Mode
, 它的類型是BindingMode
枚舉。
BindingMode
可取值為:
TwoWay
OneWay
OnTime
OneWayToSource
Default
這里的Default
值是指Binding
的模式會根據目標的實際情況來確定,比如若是可編輯的(如TextBox.Text
屬性),Default就采用雙向模式;
若是只讀的(如TextBlock.Text
)則采用單向模式。
2. 背景
讓我們回歸程序的本質。程序的本質是數據加算法,用戶給進一個輸入,經過算法的處理程序會反饋一個輸出。
這里,數據處于程序的核心地位。反過頭來再看“UI驅動程序”,數據處于被動地位,總是在等待程序接收來自UI的消息/事件后被處理或者算法完成處理后被顯示。
如何在GUI編程時把數據的地位由被動變主動、讓數據回歸程序的核心呢?這就是WPF中的Data Binding
的背景。
WPF具有這種能力的關鍵是它引入了Data Binding
概念以及與之配套的Dependency Property
系統和DataTemplate
。
在從傳統的Windows Form
遷移到WPF之后,對于一個三層程序而言,數據存儲層由數據庫和文件系統來構建,數據傳輸和處理仍然使用.NET Framwork
的 ADO.NET
等基本類(與Windows Form
等開發一樣),展示層則使用WPF類庫來實現,而展示層與邏輯層的溝通就使用Data Bindin
g`來實現。
可見Data Binding
在WPF系統中起到的是數據高速公路的作用。有了這條高速公路,加工好的數據會自動送達用戶界面加以顯示,被用戶修改過的數據也會自動傳回邏輯層, 一旦數據被加工好又會被送達用戶界面……程序的邏輯層就像一個強有力的引擎不停運轉,用加工好的數據 驅動程序的用戶界面以文字、圖形、動畫等形式把數據顯示出來——這就是“數據驅動UI”。
3. Binding源
Binding的源是數據的來源,所以,只要一個對象包含數據并能通過屬性把數據暴露出來,它就能當作Binding的源來使用。包含數據的對象比比皆是,但必須為Binding的Source指定合適的對象Binding才能正確工作,常見的辦法有:
- 把普通CLR類型單個對象指定為Source:包括.NETFramework自帶類型的對象和用戶自定義類型的對象。如果類型實現了INotifyPropertyChanged接口,則可通過在屬性的set語句里激發PropertyChanged事件來通知Binding數據已被更新。
- 把普通CLR集合類型對象指定為Source:包括數組、List、ObservableCollection等集合類型。實際工作中,我們經常需要把一個集合作為ItemsControl派生類的數據源來使用,一般是把控件的ItemsSource屬性使用Binding關聯到一個集合對象上。
- 把ADO.NET數據對象指定為Source:包括DataTable和DataView等對象。
- 使用XmlDataProvider把XML數據指定為Source:XML作為標準的數據存儲和傳輸格
式幾乎無處不在,我們可以用它表示單個數據對象或者集合;一些WPF控件是級聯式的
(如TreeView和Menu),我們可以把樹狀結構的XML數據作為源指定給與之關聯的Binding。
- 把依賴對象(DependencyObject)指定為Source:依賴對象不僅可以作為Binding的目標,同時也可以作為Binding的源。這樣就有可能形成Binding鏈。依賴對象中的依賴屬性可以作為Binding的Path。
- 把容器的DataContext指定為Source(WPFDataBinding的默認行為):有時候我們會遇到這樣的情況——我們明確知道將從哪個屬性獲取數據,但具體把哪個對象作為Binding源還不能確定。這時候,我們只能先建立一個Binding、只給它設置Path而不設置Source,讓這個Binding自己去尋找Source。這時候,Binding會自動把控件的DataContext當作自己的Source(它會沿著控件樹一層一層向外找,直到找到帶有Path指定屬性的對象為止)。
- 通過ElementName指定Source:在C#代碼里可以直接把對象作為Source賦值給Binding,但XAML無法訪問對象,所以只能使用對象的Name屬性來找到對象。
- 通過Binding的RelativeSource屬性相對地指定Source:當控件需要關注自己的、自己容器的或者自己內部元素的某個值就需要使用這種辦法。
- 把ObjectDataProvider對象指定為Source:當數據源的數據不是通過屬性而是通過方法暴露給外界的時候,我們可以使用這兩種對象來包裝數據源再把它們指定為Source。
- 把使用LINQ檢索得到的數據對象作為Binding的源。
3.1. 使用Data Context作為Binding的源
前面的例子都是把單個CLR類型對象指定為Binding的Source,方法有兩種——把對象賦值給Binding.Source屬性或把對象的Name賦值給Binding.ElementName。
DataContext屬性被定義在FrameworkElement類里,這個類是WPF控件的基類,這意味著所有WPF控件(包括容器控件)都具備這個屬性。
如前所述,WPF的UI布局是樹形結構,這棵樹的每個結點都是控件,由此我們推出另一個結論——在UI元素樹的每個結點都有DataContext。
這一點非常重要,因為當一個Binding只知道自己的Path而不知道自己的Soruce時,它會沿著UI元素樹一路向樹的根部找過去,
每路過一個結點就要看看這個結點的DataContext是否具有Path所指定的屬性。如果有,那就把這個對象作為自己的Source;如果沒有,那就繼續找下去;
如果到了樹的根部還沒有找到,那這個Binding就沒有Source,因而也不會得到數據。
先創建一個名為Student的類,它具有Id、Name、Age三個屬性:
public class Student{public int Id { get; set; }public string Name { get; set; }public int Age { get; set; }}
<Window x:Class="WpfApp1.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="clr-namespace:WpfApp1"Title="WPF 綁定進階" Height="450" Width="800"><StackPanel Background="LightBlue" Margin="10"><!-- DataContext 設置為 Student 對象 --><StackPanel.DataContext><local:Student Id="6" Name="Tim" Age="29" /></StackPanel.DataContext><!-- 綁定到 Student 的屬性 --><TextBox Text="{Binding Path=Id, Mode=TwoWay}" Margin="5" /><TextBox Text="{Binding Path=Name, Mode=TwoWay}" Margin="5" /><TextBox Text="{Binding Path=Age, Mode=TwoWay}" Margin="5" /></StackPanel>
</Window>
這個UI布局可以下面樹狀圖來表示:
在實際工作中DataContext的用法是非常靈活的。比如:
(1)當UI上的多個控件都使用Binding關注同一個對象時,不妨使用DataContext。
(2)當作為Source的對象不能被直接訪問的時候——比如B窗體內的控件想把A窗體內的控件當作自己的Binding源時,
但A窗體內的控件是private訪問級別,這時候就可以把這個控件(或者控件的值)作為窗體A的DataContext(這個屬性是public訪問級別的)從而暴露數據。
形象地說,這時候外層容器的DataContext就相當于一個數據的“制高點”,只要把數據放上去,別的元素就都能看見。
另外,DataContext本身也是一個依賴屬性,我們可以使用Binding把它關聯到一個數據源上。
3.2. 使用LINQ檢索結果作為Binding的源
自3.0版開始,.NET Framework開始支持LINQ(Language-IntegratedQuery,語言集成查詢),
使用LINQ,我們可以方便地操作集合對象、DataTable對象和XML對象而不必動輒就把好幾層foreach循環嵌套在一起卻只是為了完成一個很簡單的任務。
LINQ查詢的結果是一個IEnumerable類型對象,而IEnumerable又派生自IEnumerable,所以它可以作為列表控件的ItemsSource來使用。
我們先來看查詢集合對象。要從一個已經填充好的 List對象中檢索出所有名字以字母T 開頭的學生,代碼如下:
<StackPanel Background="LightBlue" Margin="10"><ListView x:Name="listViewStudents" Height="143" Margin="5"><ListView.View><GridView><GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding Id}" /><GridViewColumn Header="Name" Width="100" DisplayMemberBinding="{Binding Name}" /><GridViewColumn Header="Age" Width="80" DisplayMemberBinding="{Binding Age}" /></GridView></ListView.View></ListView><Button Content="Load" Height="25" Margin="5" Click="Button_Click" /></StackPanel>
public partial class MainWindow : Window{public MainWindow(){InitializeComponent();}private void Button_Click(object sender, RoutedEventArgs e){List<Student> stuList = new List<Student>{new Student { Id = 0, Name = "Tim", Age = 29 },new Student { Id = 1, Name = "Tom", Age = 28 },new Student { Id = 2, Name = "Kyle", Age = 27 },new Student { Id = 3, Name = "Tony", Age = 26 },new Student { Id = 4, Name = "Vina", Age = 25 },new Student { Id = 5, Name = "Mike", Age = 24 },};this.listViewStudents.ItemsSource = from stu in stuList where stu.Name.StartsWith("T") select stu;}}
4. Binding對數據的轉換和校驗
前面我們已經知道,Binding的作用就是架在Source與Target之間的橋梁,數據可以在這座橋梁的幫助下來流通。
就像現實世界中的橋梁會設置一些關卡進行安檢一樣,Binding這座橋上也可以設置關卡對數據的有效性進行檢驗,不僅如此,當Binding兩端要求使用不同的數據類型時,我們還可以為數據設置轉換器。
WPF中通過**值轉換器(IValueConverter)和數據校驗(IDataErrorInfo或INotifyDataErrorInfo)**來實現數據的轉換和校驗。
這種機制使得數據在顯示和更新時更加靈活和安全。
4.1. 需求
假設我們有一個Person類,包含以下屬性:
Age:用戶的年齡(整數)。
IsAdult:一個布爾值,表示用戶是否成年(年齡是否大于等于18)。
我們需要實現以下功能:
數據轉換:將Age屬性的值轉換為布爾值IsAdult,并在UI中顯示。
數據校驗:確保用戶輸入的年齡是有效的(例如,年齡必須大于0)。
4.2. 實現步驟
(1) 創建Person類
Person類實現INotifyPropertyChanged接口,用于支持數據綁定的更新,并實現IDataErrorInfo接口用于數據校驗。
文件:Person.cs
using System.ComponentModel;namespace WpfDataBindingExample{public class Person : INotifyPropertyChanged, IDataErrorInfo{private int age;public int Age{get => age;set{if (age != value){age = value;OnPropertyChanged(nameof(Age));OnPropertyChanged(nameof(IsAdult)); // 通知依賴屬性更新}}}public bool IsAdult => Age >= 18;// 實現 INotifyPropertyChanged 接口public event PropertyChangedEventHandler PropertyChanged;protected virtual void OnPropertyChanged(string propertyName){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}// 實現 IDataErrorInfo 接口public string Error => null; // 不需要整體校驗public string this[string columnName]{get{switch (columnName){case nameof(Age):if (Age <= 0)return "年齡必須大于0";break;}return null; // 無錯誤}}}
}
(2) 創建值轉換器
創建一個值轉換器,將Age轉換為布爾值IsAdult。
文件:AgeToAdultConverter.cs
using System;using System.Globalization;using System.Windows.Data;namespace WpfDataBindingExample{public class AgeToAdultConverter : IValueConverter{public object Convert(object value, Type targetType, object parameter, CultureInfo culture){if (value is int age){return age >= 18;}return false;}public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture){throw new NotImplementedException(); // 不需要反向轉換}}
}
(3) XAML文件
在XAML中,使用Binding將Person對象的屬性綁定到UI,并使用值轉換器和數據校驗。
文件:MainWindow.xaml
<Window x:Class="WpfDataBindingExample.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="clr-namespace:WpfDataBindingExample"Title="Data Binding with Conversion and Validation" Height="200" Width="400"><Window.Resources><local:AgeToAdultConverter x:Key="AgeToAdultConverter" /></Window.Resources><StackPanel Margin="10"><TextBox x:Name="AgeTextBox" Width="100" Margin="5" VerticalContentAlignment="Center"Text="{Binding Age, Mode=TwoWay, ValidatesOnDataErrors=True}" /><TextBlock Text="是否成年:" Margin="5" /><TextBox IsReadOnly="True" Background="LightGray" Width="100" Margin="5" VerticalContentAlignment="Center"Text="{Binding IsAdult, Converter={StaticResource AgeToAdultConverter}}" /></StackPanel>
</Window>
(4) 代碼后臺
在代碼后臺中,初始化Person對象并設置為DataContext。
文件:MainWindow.xaml.cs
using System.Windows;namespace WpfDataBindingExample{public partial class MainWindow : Window{public MainWindow(){InitializeComponent();DataContext = new Person { Age = 20 }; // 初始化數據}}
}
4.3. 值轉換和校驗的好處
4.3.1. 數據轉換的好處
- 靈活性:值轉換器允許在綁定過程中對數據進行任意轉換,例如格式化、邏輯判斷等。
- 解耦:將數據轉換邏輯從UI代碼中分離出來,便于維護和復用。
4.4. 數據校驗的好處
- 用戶體驗:在用戶輸入無效數據時,及時給出反饋,提升用戶體驗。
- 數據完整性:確保綁定到模型的數據始終符合業務規則,避免無效數據進入后端邏輯。
- 安全性:防止用戶輸入非法數據,減少潛在的安全風險。
4.5. 原理
4.5.1. 值轉換器原理
IValueConverter接口定義了Convert和ConvertBack方法。
Convert方法用于將源數據轉換為目標數據。
ConvertBack方法用于將目標數據轉換回源數據(可選實現)。
在XAML中通過Binding的Converter屬性指定值轉換器。
4.5.2. 數據校驗原理
IDataErrorInfo接口允許在數據綁定時對屬性進行校驗。
如果校驗失敗,Binding會自動將錯誤信息顯示在UI上(例如,輸入框顯示紅色邊框)。
校驗邏輯在this[string columnName]屬性中實現,返回錯誤信息或null。
通過上述實現,我們可以在WPF中靈活地處理數據綁定的轉換和校驗,提升應用程序的用戶體驗和數據安全性。