本文將介紹以下內容:
WPF 數據綁定
數據顯示和分層數據
使用模板
輸入驗證
本文使用以下技術:
WPF、 XAML、C#
目錄
在代碼中綁定
使用模板
使用繼承的 DataContext
使用集合視圖
使用分層數據
使用多個控件顯示 XML 數據
使用多個控件顯示業務對象
一個用於顯示整個層次結構的控件
使 用分層數據模板
使用用戶輸入
通過 ValidationRules 驗證輸入
顯示驗證錯誤
通過 IDataErrorInfo 驗證輸入
結束語
在虛擬實驗室中進行試驗:
當 Windows® Presentation Foundation (WPF) 首次出現在 .NET 雷達上時,大多數文章和演示應用程序都 對其華麗的渲染引擎和 3D 性能大加宣揚。這些示例雖然讀起來引人入勝、玩起 來趣味橫生,但卻無法證明 WPF 在現實世界中的強大功能。那些在單擊後會突 然放出煙火的三維旋轉視頻固然很酷,但我們當中的大多數人都不會用它創建應 用程序。創建軟件來顯示和編輯大量復雜的業務或科學數據才是我們的衣食父母 。
讓人振奮的是,WPF 為管理顯示和編輯復雜數據提供了良好的支持。 在 2007 年 12 月刊的《MSDN® 雜志上,John Papa 撰寫了“WPF 中 的數據綁定”一文 (msdn.microsoft.com/magazine/cc163299),其中對 WPF 數據綁定的重要概念做了出色的介紹。在此,我將以 John 在上述數據點專 欄中講到的內容為基礎,探討一些更高級的數據綁定方案。研究過這些方案後, 您將了解到在大多數行業應用程序中達到常用數據綁定要求的各種方法。
在代碼中綁定
WPF 為桌面應用程序開發人員帶來的最大變化之一 就是廣泛地使用和支持聲明性編程。WPF 用戶界面和資源可通過使用基於 XML 的標准標記語言 - 可擴展應用程序標記語言 (XAML) 來聲明。大多數 WPF 數據 綁定的說明只展示了如何在 XAML 中使用綁定。由於在 XAML 中執行的所有操作 都可在代碼中實現,因此專業 WPF 開發人員學習如何以編程方式及聲明方式來 使用數據綁定就顯得尤為重要。
許多情況下,在 XAML 中聲明綁定更為 便利。隨著系統變得更加復雜和更加動態,有時在代碼中使用綁定更為適宜。在 繼續深入之前,讓我們首先回顧一下編程式數據綁定中涉及到的一些常用的類和 方法。
WPF 元素從 FrameworkElement 或實體框架 ContentElement 那 裡同時繼承了 SetBinding 和 GetBinding 表達式方法。它們恰好是在 BindingOperations 實用程序類中可調用相同名稱方法的便捷方法。下列代碼說 明了如何使用 BindingOperations 類將某個文本框的 Text 屬性綁定到另 一個對象的屬性上:
static void BindText(TextBox textBox, string property)
{
DependencyProperty textProp = TextBox.TextProperty;
if (!BindingOperations.IsDataBound (textBox, textProp))
{
Binding b = new Binding(property);
BindingOperations.SetBinding (textBox, textProp, b);
}
}
您可以使用此 處顯示的代碼輕松地取消綁定某個屬性:
static void UnbindText(TextBox textBox)
{
DependencyProperty textProp = TextBox.TextProperty;
if (BindingOperations.IsDataBound(textBox, textProp))
{
BindingOperations.ClearBinding(textBox, textProp);
}
}
通過清除綁定,您還可以移除目標屬性的綁定值。
在 XAML 中聲明數據綁定會隱藏某些基本細節。一旦您開始在代碼中使用綁定,這 些細節就會顯現出來。其中之一就是綁定源與目標之間的關系實際上是由 BindingExpression 類(而不是 Binding 自身)維持的。Binding 類中包含多 個 BindingExpressions 可共享的高級信息,但兩個綁定屬性之間的聯系是由基 礎表達式決定的。下列代碼說明了您如何使用 BindingExpression 以編程方式 檢查某個文本框的 Text 屬性是否經過了驗證:
static bool IsTextValidated(TextBox textBox)
{
DependencyProperty textProp = TextBox.TextProperty;
var expr = textBox.GetBindingExpression(textProp);
if (expr == null)
return false;
Binding b = expr.ParentBinding;
return b.ValidationRules.Any();
}
由於 BindingExpression 不知道其是否已驗證,您需要詢問其父項綁定。稍後我將分 析一下輸入驗證技術。
虛擬實驗室:高級 WPF 數據綁定
WPF 為 管理顯示和編輯復雜數據提供了良好的支持。只要使用幾行 XAML,您就可以顯 示分層數據結構或驗證用戶輸入。您可以在我們預置的虛擬實驗室中找到 WPF 數據綁定。所有內容都已安裝完成,准備就緒,包括本文中介紹的項目。只需根 據顯示的內容開始實驗和編碼即可。
使用模板
如用戶界面富有成 效,它提供的原始數據可以使用戶直觀地從中發現有意義的信息。這就是數據可 視化的本質。數據綁定就像一道數據可視化難題。幾乎所有最繁瑣的 WPF 程序 都需要一種更有效的數據提供方式,而不是簡單地將某個控件屬性綁定到數據對 象的某個屬性上。真實的數據對象具有多個相關值,這些不同的值應合並為一個 組織嚴密的直觀表示。這就是 WPF 使用數據模板的原因。
System.Windows.DataTemplate 類就是 WPF 中的一種模板。通常,模板 就像 WPF 框架使用的一種“俗套”,它創建的可視元素協助呈現那 些沒有固有直觀表示的對象。如某個元素嘗試顯示一個沒有固有直觀表示的對象 (如自定義業務對象),您可以通過賦予 DataTemplate 來告知元素如何呈現該 對象。
DataTemplate 會根據需要盡可能多地產生可視元素來顯示該數據 對象。這些元素使用數據綁定顯示該數據對象的屬性值。如果某個元素不知道如 何顯示通知其呈現的對象,就會對其調用 ToString 方法並在 TextBlock 中顯 示結果。
假定您有一個名為 FullName 的簡單類,用於存儲人名。您想 顯示一個姓名列表,並使每個人的姓氏突出顯示。要執行此操作,您需要創建一 個描述如何呈現 FullName 對象的 DataTemplate。圖 1 中所列代碼顯示了 FullName 類以及將顯示姓名列表的窗口的源代碼。
圖 1 使用 DataTemplate 顯示 FullNames
public class FullName
{
public string FirstName { get; set; }
public char MiddleInitial { get; set; }
public string LastName { get; set; }
}
public partial class WorkingWithTemplates : Window
{
// This is the Window's constructor.
public WorkingWithTemplates()
{
InitializeComponent();
base.DataContext = new FullName[]
{
new FullName
{
FirstName = "Johann",
MiddleInitial = 'S',
LastName = "Bach"
},
new FullName
{
FirstName = "Gustav",
MiddleInitial = ' ',
LastName = "Mahler"
},
new FullName
{
FirstName = "Alfred",
MiddleInitial = 'G',
LastName = "Schnittke"
}
};
}
}
如圖 2 所示,窗口的 XAML 文件中含有一個 ItemsControl 。它創建了一個用戶無法選擇或移除的項目列表。ItemsControl 向其 ItemTemplate 屬性分配一個 DataTemplate,利用它來呈現在窗口構造函數中創 建的各個 FullName 實例。您應該注意 DataTemplate 中的大部分 TextBlock 元素是如何將其 Text 屬性綁定到了它們所代表的 FullName 對象的屬性上的。
圖 2 使用 DataTemplate 顯示 FullName 對象
<!-- This displays the FullName objects. -->
<ItemsControl ItemsSource=" {Binding Path=.}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock FontWeight="Bold" Text="{Binding LastName}" />
<TextBlock Text=", " />
<TextBlock Text="{Binding FirstName}" />
<TextBlock Text=" " />
<TextBlock Text="{Binding MiddleInitial}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
當運行此演示應用程序時,其外觀類 似於圖 3 所示。通過使用 DataTemplate 來呈現名稱可以很容易地突出顯示每 個人的姓氏,因為相應的 TextBlock 的 FontWeight 均為粗體。這個簡單的示 例說明了 WPF 數據綁定與模板之間的基本關系。進一步探討該主題時,我將會 把這些功能組合成更強大的復雜對象可視化方法。
圖 3 由 DataTemplate 呈現的 FullNames
使用繼承的 DataContext
默認情況下,所有綁定均隱式綁定在某個元素的 DataContext 屬性上。因此,可以說是元素的 DataContext 引用了其數據源。 在 DataContext 的工作方式上,需要了解一些特殊的內容。在您了解了 DataContext 這個微妙的方面後,將極大地簡化復雜數據綁定用戶界面的設計。
並非必須設置元素的 DataContext 屬性才能引用數據源對象。如果元素 樹(從技術上講是邏輯樹)中某個祖先元素的 DataContext 被賦值,則該值將 自動被用戶界面中的每個後代元素所繼承。換言之,如果窗口的 DataContext 設置為引用 Foo 對象,則默認情況下,窗口中每個元素的 DataContext 都將引 用同一 Foo 對象。您可以輕松地為窗口中的任何元素分配一個不同的 DataContext 值,這會使該元素的所有後代元素都將繼承新的 DataContext 值 。這與 Windows 窗體中的環境屬性很相似。
在上一部分中,我討論了如 何使用 DataTemplates 創建可視化數據對象。圖 2 中由模板創建的元素將其屬 性綁定到某個 FullName 對象的屬性。這些元素隱式地綁定到其 DataContext 上。由 DataTemplate 創建的元素的 DataContext 引用模板所使用的數據對象 ,如 FullName 對象。
DataContext 屬性的值繼承中沒有任何神奇之處 。它只是利用了對 WPF 中內置繼承依賴關系屬性的支持。任何依賴關系屬性都 可以是繼承屬性,在向 WPF 的依賴關系屬性系統注冊該屬性時,只需在提供的 元數據中指定標記即可。
繼承依賴關系屬性的另一個示例是 FontSize, 它含有所有元素。如果您在窗口中設置了 FontSize 依賴關系屬性,則默認情況 下,該窗口中的所有元素都將以該大小顯示其文本。沿元素樹向下傳播 FontSize 值所使用的基礎結構與傳播 DataContext 的基礎結構相同。
在面向對象環境中的“繼承”表示子類繼承其父類的成員,而這裡使 用的術語“繼承”有所不同。屬性值繼承僅指值在運行時沿著元素樹 向下的傳播。在面向對象含義中,類當然可以繼承那些支持值繼承的依賴關系屬 性。
使用集合視圖
當 WPF 控件綁定到數據集合上時,它們不會 直接綁定在集合本身上。而是隱式地綁定在自動封裝該集合的視圖上。該視圖可 實現 ICollectionView 界面,可以是若干具體實現之一,如 ListCollectionView。
一個集合視圖有多項職責。它可跟蹤集合中的當 前項,該項通常會轉換為列表控件中的活動/選定項。集合視圖還提供在列表內 排序、篩選和歸組項目的一般方法。可以圍繞集合將多個控件綁定到同一視圖上 ,以便它們形成彼此並列的關系。以下代碼顯示了 ICollectionView 的一些功 能:
// Get the default view wrapped around the list of Customers.
ICollectionView view = CollectionViewSource.GetDefaultView(allCustomers);
// Get the Customer selected in the UI.
Customer selectedCustomer = view.CurrentItem as Customer;
// Set the selected Customer in the UI.
view.MoveCurrentTo(someOtherCustomer);
所有列表 控件(如列表框、組合框和列表視圖)必須將它們的 IsSynchronizedWithCurrentItem 屬性設置為 true,以與集合視圖的 CurrentItem 屬性保持同步。抽象的 Selector 類定義了該屬性。如果未 設置為 true,選擇列表控件中的某項將不會更新該集合視圖的 CurrentItem, 向 CurrentItem 分配新值也不會在列表控件中有所反映。
使用分層數據
現實生活中到處都是分層數據。客戶下達多重訂單、分子由很多個原子 組成、部門由多名員工組成,太陽系包含一系列的天體。您肯定對這種常見的主 從復合排列非常熟悉。
WPF 提供了多種使用分層數據結構的方法,分別 適用於不同的情形。從本質上看,這是在選擇使用多個控件顯示數據,還是在一 個控件中顯示多層數據。接下來,我就要講述這兩種方法。
使用多個控 件顯示 XML 數據
一種極為常見的分層數據處理方法就是利用單獨的控件 顯示各個層級。例如,假設我們有一個表示客戶、訂單和訂單詳細信息的系統。 在此情況下,我們可能需要一個組合框來顯示客戶,用列表框顯示所有選定的客 戶訂單,然後用 ItemsControl 顯示所選訂單的相關詳細信息。這是一種顯示分 層數據的不錯的方法,極易在 WPF 中實現。
根據我之前描述的場景,圖 4 顯示了某個數據的簡化示例,它封裝在 WPF XmlDataProvider 組件中,可供 應用程序處理。一個類似於圖 5 的用戶界面將顯示該數據。注意客戶和訂單是 可選的,而訂單的詳細信息是存在於只讀列表中的。這一點非常有意義,因為可 視對象應僅在影響應用程序的狀態時或處於可編輯狀態時才可供選擇。
圖 4 客戶、訂單和訂單詳細信息 XML 層次結構
<XmlDataProvider x:Key="xmlData">
<x:XData>
<customers >
<customer name="Customer 1">
<order desc="Big Order">
<orderDetail product="Glue" quantity="21" />
<orderDetail product="Fudge" quantity="32" />
</order>
<order desc="Little Order">
<orderDetail product="Ham" quantity="1" />
<orderDetail product="Yarn" quantity="2" />
</order>
</customer>
<customer name="Customer 2">
<order desc="First Order">
<orderDetail product="Mousetrap" quantity="4" />
</order>
</customer>
</customers>
</x:XData>
</XmlDataProvider>
圖 5 顯示 XML 數據的一種方法
圖 6 中的 XAML 介紹了如何使用這些不 同的控件來顯示剛才所示的分層數據。此窗口不需要任何代碼,它完全存在於 XAML 中。
圖 6 將分層 XML 數據綁定到 UI 上的 XAML
<Grid DataContext=
"{Binding Source={StaticResource xmlData},
XPath=customers/customer}"
Margin="4"
>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<!-- CUSTOMERS -->
<DockPanel Grid.Row="0">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Customers" />
<ComboBox
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}"
>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding XPath=@name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</DockPanel>
<!-- ORDERS -->
<DockPanel Grid.Row="1">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Orders" />
<ListBox
x:Name="orderSelector"
DataContext="{Binding Path=CurrentItem}"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding XPath=order}"
>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding XPath=@desc}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<!-- ORDER DETAILS -->
<DockPanel Grid.Row="2">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold"
Text="Order Details" />
<ItemsControl
DataContext=
"{Binding ElementName=orderSelector, Path=SelectedItem}"
ItemsSource="{Binding XPath=orderDetail}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run>Product:</Run>
<TextBlock Text="{Binding XPath=@product}" />
<Run>(</Run>
<TextBlock Text=" {Binding XPath=@quantity}" />
<Run>) </Run>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
</Grid>
注意,在這裡廣泛使用了短 XPath 查詢通知 WPF 在哪裡獲取綁定值。綁定類向您提供 XPath 屬性,您可以為其分配 XmlNode.SelectNodes 方法支持的任何 XPath 查詢。實質上,WPF 是使用該方 法來執行 XPath 查詢的。遺憾的是,這意味著由於 XmlNode.SelectNodes 當前 不支持使用 XPath 函數,WPF 數據綁定也不支持它們。
客戶組合框以及 訂單列表框都綁定到 Xpath 查詢(由根網格的 DataContext 綁定執行)的合成 節點集上。列表框的 DataContext 將自動返回集合視圖的 CurrentItem,該集 合視圖封裝為網格的 DataContext 生成的 XmlNodes 集合。換言之,列表框的 DataContext 是當前選定的 Customer。由於該列表框的 ItemsSource 隱式地綁 定到其自己的 DataContext(因為沒有指定任何其他源)上,且其 ItemsSource 綁定會執行 XPath 查詢以從 DataContext 中獲取 <order> 元素,因此 ItemsSource 將有效地綁定到選定客戶的訂單列表上。
請記住,在綁定 到 XML 數據時,您實際上是綁定到由對 XmlNode.SelectNodes 的調用創建的對 象上。如果不仔細,您就會最終將多個控件綁定到邏輯上等效但實際不同的多組 XmlNodes 上。這是由於每次調用 XmlNode.SelectNodes 都會生成一組新的 XmlNodes,即使您每次都是將相同的 XPath 查詢傳遞給同一 XmlNode 也是如此 。這是需要對 XML 數據綁定特別注意的一點,在綁定業務對象時,可將其忽略 。
使用多個控件顯示業務對象
假設您現在要綁定到上一示例的同 一數據中,但數據以業務對象而不是 XML 的形式存在。這會對您綁定數據結構 各個層次的方法有何影響?使用的技術會有何相同或不同之處?
圖 7 中 的代碼顯示了一個簡單類,它用於創建業務對象,其中存儲著我們將要綁定的數 據。這些類構成的邏輯架構與前一部分中 XML 數據所使用的架構相同。
圖 7 用於創建業務對象層次結構的類
public class Customer
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
public override string ToString()
{
return this.Name;
}
}
public class Order
{
public string Desc { get; set; }
public List<OrderDetail> OrderDetails { get; set; }
public override string ToString()
{
return this.Desc;
}
}
public class OrderDetail
{
public string Product { get; set; }
public int Quantity { get; set; }
}
圖 8 所示為 這些對象顯示窗口的 XAML。它與圖 6 中看到的 XAML 很相似,但其中一些重要 的差異需要注意。XAML 中沒有顯示出是該窗口的構造函數創建了數據對象並設 置了 DataContext,而不是 XAML 將其作為資源進行引用。請注意,其中沒有任 何控件顯式設置了 DataContext。它們全部都繼承了同一個 DataContext,該 DataContext 是一個 List<Customer> 實例。
圖 8 將分層業務對象綁定到 UI 中的 XAML
<Grid Margin="4">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<!-- CUSTOMERS -->
<DockPanel Grid.Row="0">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Customers"
/>
<ComboBox
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Path=.}"
/>
</DockPanel>
<!-- ORDERS -->
<DockPanel Grid.Row="1">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Orders" />
<ListBox
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Path=CurrentItem.Orders}"
/>
</DockPanel>
<!-- ORDER DETAILS -- >
<DockPanel Grid.Row="2">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold"
Text="Order Details" />
<ItemsControl
ItemsSource="{Binding Path=CurrentItem.Orders.CurrentItem.
OrderDetails}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run>Product:</Run>
<TextBlock Text="{Binding Path=Product}" />
<Run>(</Run>
<TextBlock Text=" {Binding Path=Quantity}" />
<Run>) </Run>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
</Grid>
綁定到業務對象 (而非 XML)時,另一個明顯的區別是,承載訂單詳細信息的 ItemsControl 不 需要綁定到訂單列表框的 SelectedItem。該方法在 XML 綁定中是必要的,因為 對於列表項來自本地 XPath 查詢的列表,沒有任何通用的方式可以用來引用它 的當前項。
在綁定到業務對象(而不是 XML)時,通常是按照所選項的 嵌套級別來綁定的。ItemsControl 的 ItemsSource 綁定利用了這個便捷的特點 ,在綁定路徑中兩次指定 CurrentItem: 一次是針對所選客戶,一次是針對所 選訂單。如前文所述,CurrentItem 屬性是封裝數據源的基礎 ICollectionView 的成員。
就 XML 和業務對象之間在工作方式上的差異而言,還有一點需 要注意。由於 XML 示例綁定到 XmlElements,因此必須提供 DataTemplates 以 解釋如何呈現客戶和訂單。在綁定到自定義業務對象時,只需覆蓋 Customer 類 和 Order 類的 ToString 方法,並允許 WPF 為這些對象顯示該方法的輸出即可 避免這種開銷。這一竅門僅夠處理具有簡單文本表示的對象。在處理復雜的數據 對象時,使用這種便捷的技術可能不適用。
一個用於顯示整個層次結構 的控件
到現在為止,您只了解了通過在不同的控件中顯示層次結構的每 個層級來展示分層數據的方法。但通常需要在同一控件中顯示分層數據的所有層 級,這對數據處理是有幫助的,也是必要的。此方法的典型例子就是 TreeView 控件,該控件支持顯示和浏覽任意層級的嵌套數據。
您可通過以下兩種 方式用各項來填充 WPF TreeView。一種方法以代碼或 XAML 方式手動添加項, 另一種方法是通過數據綁定創建這些項。
下面的 XAML 表明了如何通過 XAML 將一些 TreeViewItem 手動添加到 TreeView:
<TreeView>
<TreeViewItem Header="Item 1">
<TreeViewItem Header="Sub-Item 1" />
<TreeViewItem Header="Sub-Item 2" />
</TreeViewItem>
<TreeViewItem Header="Item 2" />
</TreeView>
如控件始終都顯示小型靜態項組,在 TreeView 中手動創建項的方法是很實用的。如果需要顯示大量會隨時間變化的 數據,就必須使用更具動態性的方法。此時,您有兩種方法可供選擇。您可以編 寫這樣的代碼:從頭至尾檢查整個數據結構,根據找到的數據對象創建 TreeViewItem,然後將這些項添加到 TreeView。或者利用分層數據模板,讓 WPF 代您完成所有工作。
使用分層數據模板
您可以用聲明的方式 解釋 WPF 應如何通過分層數據模板呈現分層數據。利用 HierarchicalDataTemplate 類這一工具可以彌補復雜數據結構與該數據的直觀 表示之間的缺口。它與常用 DataTemplate 非常相似,但還允許您指定數據對象 子項的來源。您還可以為 HierarchicalDataTemplate 提供一個用於呈現這些子 項的模板。
假定您現在要在一個 TreeView 控件中顯示圖 7 中展現的數 據。該 TreeView 控件看上去可能有些類似於圖 9。實現此控件需要使用兩個 HierarchicalDataTemplate 和一個 DataTemplate。
圖 9 在 TreeView 中顯示整個數據層次結構
這兩個分層模板可顯示 Customer 對象和 Order 對象。由於 OrderDetail 對象沒有任何子項,您可以 通過非分層 DataTemplate 呈現這些對象。TreeView 的 ItemTemplate 屬性會 將該模板用於 Customer 類型的對象,因為 Customer 是包含在 TreeView 根層 級下的數據對象。圖 10 中所列的 XAML 表明了如何將所有這些方式有機地結合 在一起。
圖 10 支持 TreeView 顯示的 XAML
<Grid>
<Grid.DataContext>
<!--
This sets the DataContext of the UI
to a Customers returned by calling
the static CreateCustomers method.
-->
<ObjectDataProvider
xmlns:local="clr- namespace:VariousBindingExamples"
ObjectType=" {x:Type local:Customer}"
MethodName="CreateCustomers"
/>
</Grid.DataContext>
<Grid.Resources>
<!-- ORDER DETAIL TEMPLATE -->
<DataTemplate x:Key="OrderDetailTemplate">
<TextBlock>
<Run>Product:</Run>
<TextBlock Text="{Binding Path=Product}" />
<Run>(</Run>
<TextBlock Text="{Binding Path=Quantity}" />
<Run>)</Run>
</TextBlock>
</DataTemplate>
<!-- ORDER TEMPLATE -->
<HierarchicalDataTemplate
x:Key="OrderTemplate"
ItemsSource="{Binding Path=OrderDetails}"
ItemTemplate="{StaticResource OrderDetailTemplate}"
>
<TextBlock Text="{Binding Path=Desc}" />
</HierarchicalDataTemplate>
<!-- CUSTOMER TEMPLATE -->
<HierarchicalDataTemplate
x:Key="CustomerTemplate"
ItemsSource=" {Binding Path=Orders}"
ItemTemplate=" {StaticResource OrderTemplate}"
>
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
</Grid.Resources>
<TreeView
ItemsSource="{Binding Path=.}"
ItemTemplate="{StaticResource CustomerTemplate}"
/>
</Grid>
我為 Grid(包含 TreeView )的 DataContext 分配了一個 Customer 對象集合。這在 XAML 中通過使用 ObjectDataProvider 即可實現,它是從 XAML 調用方法的一種便捷方式。由於 DataContext 是在元素樹中自上而下地繼承,因此 TreeView 的 DataContext 會引用這組 Customer 對象。這就是我們可以為其 ItemsSource 屬性提供一個 "{Binding Path=.}" 綁定的原因,通過這種方式可以表明 ItemsSource 屬性被綁定到 TreeView 的 DataContext。
如果沒有分配 TreeView 的 ItemTemplate 屬性,則 TreeView 將僅顯示頂層的 Customer 對 象。由於 WPF 不知道如何呈現 Customer,因此它會對每個 Customer 都調用 ToString,並為每項都顯示該文本。它無法確定每個 Customer 都有一個與其關 聯的 Order 對象列表,且每個 Order 都有一個 OrderDetail 對象列表。由於 WPF 無法理解您的數據架構,因此您必須向 WPF 解釋架構,使它能正確呈現數 據結構。
向 WPF 解釋數據的結構和外觀是 HierarchicalDataTemplates 的份內工作。此演示中所使用的模板包含的可視元素樹非常簡單,就是其中帶有 少量文本的 TextBlocks。在更復雜的應用程序中,模板可能包含交互式的旋轉 3D 模型、圖像、矢量繪圖、復雜的 UserControl 或任何其他可視化基礎數據對 象的 WPF 內容。
需要特別注意聲明模板的順序。必須先聲明一個模板後 才能通過 StaticResource 擴展對其進行引用。這是由 XAML 閱讀器規定的要求 ,該要求適用於所有資源,而不僅僅是模板。
或者可通過使用 DynamicResource 擴展來引用模板,在這種情況下,模板聲明的詞匯順序無關緊 要。但使用 DynamicResource 引用(而不是 StaticResource 引用)總會帶來 一些運行時開銷,因為它們要監控資源系統的更改。由於我們不會在運行時替換 模板,因此這筆開銷是多余的,最好使用 StaticResource 引用並恰當地安排模 板聲明的順序。
使用用戶輸入
對大多數程序而言,顯示數據僅僅 是這場較量的一半。另一個巨大的挑戰是分析、接受和拒絕用戶輸入的數據。理 想狀態下,所有用戶都始終輸入符合邏輯且准確的數據,那麼這會是一項簡單的 任務。但在現實生活中,卻根本不是這麼回事。現實中的用戶會出現打字錯誤、 忘記輸入所需的值、在錯誤的位置輸入值、刪除不應刪除的記錄、添加本不該有 的記錄,人都會犯錯誤。
作為開發人員和架構師,我們的工作就是與無 法避免的錯誤和惡意用戶輸入作斗爭。WPF 綁定基礎結構支持輸入驗證。在本文 接下來的幾節中,我將講述如何充分利用 WPF 對驗證的支持,以及如何向用戶 顯示驗證錯誤消息。
通過 ValidationRules 驗證輸入
WPF 的第 一個版本包含在 Microsoft® .NET Framework 3.0 之內,只能有限地支持 輸入驗證。Binding 類具有 ValidationRules 屬性,它可存儲任意數量的 ValidationRule 派生類。這些規則中的每一個都包含一些可通過測試查看綁定 值是否有效的邏輯。
那時,WPF 僅隨一個被稱為 ExceptionValidationRule 的 ValidationRule 子類出現。開發人員可將該規則 添加到綁定的 ValidationRules 中,它將捕捉在數據源更新過程中拋出的異常 ,從而允許 UI 顯示異常的錯誤消息。這種輸入驗證方法的有效性極富爭議,人 們認為良好用戶體驗的基礎是避免不必要地向用戶洩露技術細節。對於大多數用 戶而言,數據分析異常的錯誤消息通常過於專業,對不起,這個話題有點離題。
假設您有一個表示時間段的類,例如這裡看到的簡單 Era 類:
public class Era
{
public DateTime StartDate { get; set; }
public TimeSpan Duration { get; set; }
}
如果您希望允許用戶編輯某個時間段的開始日期和持續 時間,您可以使用兩個文本框控件,然後將它們的 Text 屬性綁定到 Era 實例 的屬性上。由於用戶可在文本框內輸入任何他想輸入的文本,您無法確保輸入的 文本可轉換為 DateTime 或 TimeSpan 的實例。在這種情況下,您可以使用 ExceptionValidationRule 報告數據轉換錯誤,然後在用戶界面上顯示這些轉換 錯誤。圖 11 中所列的 XAML 展示了完成此任務的方式。
Figure 11 一個表示時間段的簡單類
<!-- START DATE -- >
<TextBlock Grid.Row="0">Start Date:</TextBlock>
<TextBox Grid.Row="1">
<TextBox.Text>
<Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<ExceptionValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<!-- DURATION -- >
<TextBlock Grid.Row="2">Duration:</TextBlock>
<TextBox
Grid.Row="3"
Text=" {Binding
Path=Duration,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnExceptions=True}"
/>
這兩個文本 框展示了在 XAML 中將 ExceptionValidationRule 添加到綁定的 ValidationRules 中的兩種方法。“開始日期”文本框使用詳細的屬 性元素語法顯式地添加規則。“持續時間”文本框使用簡寫語法,將 綁定的 ValidatesOnExceptions 屬性設置為 true。這兩類綁定都將其 UpdateSourceTrigger 屬性設置為 PropertyChanged,這樣每次為文本框的 Text 屬性賦新值時都會進行驗證,而不是空等控件。該程序的屏幕快照如圖 12 所示。
圖 12 ExceptionValidationRule 顯示驗證錯誤
顯示驗證錯誤
如 圖 13 所示,“Duration”(持續時間)文本框中包含一個無效值。 其包含的字符串無法轉換為 TimeSpan 實例。該文本框的工具提示顯示了一則錯 誤消息,控件的右側出現一個小的紅色錯誤圖標。這種行為並不會自動發生,但 很容易實現和自定義。
圖 13 向用戶呈現輸入驗證錯誤
<!--
The template which renders a TextBox
when it contains invalid data.
-->
<ControlTemplate x:Key="TextBoxErrorTemplate">
<DockPanel>
<Ellipse
DockPanel.Dock="Right"
Margin="2,0"
ToolTip="Contains invalid data"
Width="10" Height="10"
>
<Ellipse.Fill>
<LinearGradientBrush>
<GradientStop Color="#11FF1111" Offset="0" />
<GradientStop Color="#FFFF0000" Offset="1" />
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<!--
This placeholder occupies where the TextBox will appear.
-->
<AdornedElementPlaceholder />
</DockPanel>
</ControlTemplate>
<!--
The Style applied to both TextBox controls in the UI.
-->
<Style TargetType="TextBox">
<Setter Property="Margin" Value="4,4,10,4" />
<Setter
Property="Validation.ErrorTemplate"
Value="{StaticResource TextBoxErrorTemplate}"
/>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip">
<Setter.Value>
<Binding
Path="(Validation.Errors)[0].ErrorContent"
RelativeSource="{x:Static RelativeSource.Self}"
/>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
靜態 Validation 類通過使用某些附加的屬性和靜態方法在控件與其包含的任何驗證 錯誤之間形成關系。您可以在 XAML 中引用這些附加屬性,就用戶界面向用戶提 供輸入驗證錯誤的方法創建說明,將其做為標記。圖 13 中的 XAML 負責解釋如 何呈現上一個示例中兩個文本框控件的輸入錯誤消息。
圖 13 中 Style 的目標是 UI 中文本框的所有實例。它對文本框應用了三種設置。第一個 Setter 影響文本框的 Margin 屬性。Margin 屬性可設置為適當的值,以提供足 夠的空間在右側顯示錯誤圖標。
Style 中的下一個 Setter 分配 ControlTemplate,在包含無效數據時它負責呈現文本框。它將附加的 Validation.ErrorTemplate 屬性設置為在 Style 之上聲明的 ControlTemplate 。當 Validation 類報告文本框有一處或多處驗證錯誤時,該文本框將隨該模板 一同呈現。這裡就是紅色錯誤圖標的來源,如圖 12 所示。
Style 還包 含一個用於監控文本框上附加的 Validation.HasError 屬性的 Trigger。當 Validation 類為文本框將附加的 HasError 屬性設置為 true 時,Style 的 Trigger 激活並向文本框分配一條工具提示。工具提示的內容綁定為異常的錯誤 消息,該異常在嘗試將文本框的文本解析為源屬性的數據類型實例時拋出。
通過 IDataErrorInfo 驗證輸入
在引入 Microsoft .NET Framework 3.5 後,WPF 對輸入驗證的支持得到了顯著改進。ValidationRule 方法對於簡單的應用程序很有用,但現實中的應用程序需要處理復雜的真實數據 和業務規則。將業務規則編碼到 ValidationRule 對象中不僅僅是將代碼捆綁到 WPF 平台上,還不允許業務邏輯在其所屬位置:業務對象中存在!
很多 應用程序都有一個業務層,該層的一組業務對象中包含復雜的業務處理規則。針 對 Microsoft .NET Framework 3.5 編譯時,您可以充分利用 IDataErrorInfo 接口使 WPF 詢問業務對象它們是否處於有效狀態。這就不必在與業務層分離的 對象中放置業務邏輯,並允許您創建獨立於 UI 平台的業務對象。由於 IDataErrorInfo 接口已延續多年,這也使得您可以更容易地重新使用舊版 Windows 窗體或 ASP.NET 應用程序中的業務對象。
假設您需要為范圍以 外的某個時間段提供驗證,以確保用戶的文本輸入可轉換為源屬性的數據類型。 時間段的起始日期不能為將來,因為我們無法了解尚不存在的時間段。要求某個 時間段至少持續一毫秒也是有意義的。
這些類型的規則與業務邏輯的一 般觀點相同,因為它們都是域規則的示例。最好在存儲其狀態的域對象中實現這 些域規則。圖 14 中所列的代碼顯示了 SmartEra 類,該類通過 IDataErrorInfo 接口公開驗證錯誤消息。
圖 14 IDataErrorInfo 公開驗證錯誤消息
public class SmartEra
: System.ComponentModel.IDataErrorInfo
{
public DateTime StartDate { get; set; }
public TimeSpan Duration { get; set; }
#region IDataErrorInfo Members
public string Error
{
get { return null; }
}
public string this[string property]
{
get
{
string msg = null;
switch (property)
{
case "StartDate":
if (DateTime.Now < this.StartDate)
msg = "Start date must be in the past.";
break;
case "Duration":
if (this.Duration.Ticks == 0)
msg = "An era must have a duration.";
break;
default:
throw new ArgumentException(
"Unrecognized property: " + property);
}
return msg;
}
}
#endregion // IDataErrorInfo Members
}
從 WPF 用戶界面使用 SmartEra 類的驗證支持非常簡單。您唯一要做的就是告知綁定它們應使用其綁 定對象上的 IDataErrorInfo 接口。您可以通過兩種方法執行此操作,如圖 15 所示。
圖 15 使用驗證邏輯
<!-- START DATE -->
<TextBlock Grid.Row="0">Start Date:</TextBlock>
<TextBox Grid.Row="1">
<TextBox.Text>
<Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<ExceptionValidationRule />
<DataErrorValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<!-- DURATION -- >
<TextBlock Grid.Row="2">Duration:</TextBlock>
<TextBox
Grid.Row="3"
Text=" {Binding
Path=Duration,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True,
ValidatesOnExceptions=True} "
/>
與顯式或隱式地向綁定的 ValidationRules 集合中添加 ExceptionValidationRule 相似,您可以將 DataErrorValidationRule 直接添加到綁定的 ValidationRules 中,也可以將 ValidatesOnDataErrors 屬性設置為 true。兩種方法所產生的實際效果相同; 綁定系統會查詢數據源的 IDataErrorInfo 接口是否有驗證錯誤。
結束 語
很多開發人員都喜歡 WPF 對數據綁定的豐富支持,這是有一定原因的 。在 WPF 中使用綁定時功能如此強大、普及程度如此之深,很多軟件開發人員 因此不得不重新審視他們對數據與用戶界面關系的看法。在諸多核心功能的協作 下,WPF 可支持復雜的數據綁定方案,如模板、樣式和附加屬性。
只要 使用幾行 XAML,您就可以表達想如何顯示分層數據結構或如何驗證用戶輸入。 在高級環境下,您可以通過編程方式訪問綁定系統以使用其全部功能。借助這樣 一款您可控制自如的強大基礎結構,對於創建現代化業務應用程序的開發人員而 言,創建出色的用戶體驗和引人注目的數據可視化這一長期目標觸手可及。
Josh Smith 對使用 WPF 創建美好的用戶體驗充滿了熱情。他在 WPF 社 區的出色工作為其贏得了 Microsoft MVP 的稱號。Josh 就職於體驗設計組的 Infragistics。閒暇之余,他喜歡拉小提琴、讀歷史書,還喜歡和他的女朋友逛 紐約。您可以訪問 Josh 的博客,地址是 joshsmithonwpf.wordpress.com。
本文配套源碼