目前為止,我們已經簡單的處理了對象。然而,這並不是數據的唯一來源; XML和突然想到的相關數據庫,都是流行的選擇。更進一步地,由於XML或
相關數據庫並不能存儲數據為.NET對象,某些轉換可能需要支持數據綁定, 正如你會想到的,需要數據源對象上的.NET屬性。而且即使我們可以直接在xaml 中聲明對象,仍然希望有一個層間接地從其他源中拉數據,甚至於將這個工作交 給一個工作線程,如果說取回是一個呆板的操作。
簡而言之,為了對象的轉換和加載,我們希望間接的而不是直接的聲明方式 。對於這個間接方式,我們必須致力於IDataSource接口的實現,其中一種就是 數據對象源。
4.4.1數據對象源
一種對IDataSource接口的實現是,為所有的操作提供一個間接的層,這些操 作用於生成要綁定到的對象。例如,如果我們想要在Web上加載一組Person對象 ,我們需增強一些代碼中的邏輯,如示例4-34。
示例4-34
namespace PersonBinding {
public class Person : INotifyPropertyChanged {}
public class People : ObservableCollection<Person> {}
public class RemotePeopleLoader : People {
public RemotePeopleLoader( ) {
// Load people from afar
}
}
}
在示例4-34中,RemotePeopleLoader類從People集合類中派生,在構造器中 檢索數據,因為對象數據源希望它創建的對象是一個集合,正如示例4-35。
示例4-35
<Window.Resources>
<ObjectDataSource
x:Key="Family"
TypeName="PersonBinding.RemotePeopleLoader"
Asynchronous="True" />
</Window.Resources>
<Grid DataContext="{StaticResource Family}">
<ListBox ItemsSource="{Binding}" >
</Grid>
ObjectDataSource元素通常位於資源塊中,按名稱在xaml的其他位置中使用 。TypeName屬性引用了集合類的完整的限定名稱。
WPF中的大部分具有type參數的類,如DataTemplate元素的DataType屬性,在 設置中帶上type擴展標記,這包括類,命名空間和使用mapping語法的編譯集信 息。
<!-- set up DataTemplate for Bar.Quux in assembly foo -- >
<?Mapping
XmlNamespace="local"
ClrNamespace="Bar" Assembly="foo" /><Window xmlns="local">
<Window.Resources>
<DataTemplate
DataType="{x:Type local:Quux}"></DataTemplate>
</Window.Resources>
</Window>
然而,ObjectDataSource以自己的方式設置type的信息。
<!-- set up ObjectDataSource for Bar.Quux in assembly foo -->
<ObjectDataSource x:Key="foo" TypeName="Bar.Quux, foo" />
願望是美好的,現實是殘酷的。在RTM版本之前,兩種技術都是合理的。
伴隨著對象數據源擔當數據和綁定之間的中介者,我們需要更新代碼,當我 們遍歷People集合時(現在是一個基本類RemotePeopleLoader,但是仍然是 Person對象的容器),正如示例4-36所示。
示例4-36
public partial class Window1 : Window {
ICollectionView GetFamilyView( ) {
IDataSource
ds = (IDataSource)this.FindResource("Family");
People people = (People)ds.Data;
return BindingOperations.GetDefaultView(people);
}
void birthdayButton_Click(object sender, RoutedEventArgs e) {
ICollectionView view = GetFamilyView( );
Person person = (Person)view.CurrentItem;
++person.Age;
MessageBox.Show();
}
void addButton_Click(object sender, RoutedEventArgs e) {
IDataSource ds = (IDataSource)this.FindResource("Family");
People people = (People)ds.Data;
people.Add(new Person("Chris", 35));
}
}
由於Family資源現在是一個ObjectDataSource,本身就是IDataSource接口的 實現,在示例4-26中,當我們需要People集合的時候,我們將Family中的資源轉 換為IdataSource,並從Data屬性中拉出這個集合。
即使數據源對象通過Data屬性暴露他的數據,這並不意味著你必須綁定它。 如果你注意到示例4-35,我們仍然像從前一樣綁定了列表框。
<!--do not bind to Path=Data -->
<ListBox ItemSource=”{Binding}” …>
這樣做的原因是WPF對IDataSource提供內建的支持,因此沒有必要間接地這 樣做。
4.4.1.1異步數據遍歷
在示例4-35中,我們應用了Asynchronous屬性,這是最有趣的一塊功能:數 據源對象提供給我們所欠缺的——當我們直接在xaml中聲明對象圖的時候。
當Asynchronous屬性設置為true時(默認為false),通過TypeName創建詳細 對象的任務就交給工作線程處理,當遍歷過數據,僅僅在UI線程表現綁定。這與 綁定到數據並不一樣——數據是在網絡流中遍歷到的,但是這總比當一個長時間 的遍歷發生時阻塞了UI線程要好。
4.4.1.2傳遞參數
除Asynchronous屬性外,數據源對象還提供了Parameters屬性,這是一個逗 號分隔的字符串列表,作為一個字符串傳遞到由數據源對象創建的類型中。例如 ,如果我們要傳遞一組URL參數,用來嘗試遍歷其中的數據,我們可以使用 Parameters參數如示例4-37。
示例4-37
<ObjectDataSource
x:Key="Family"
TypeName="PersonBinding.RemotePeopleLoader"
Asynchronous="True"
Parameters="http://sellsbrothers.com/sons.dat, http://sellssisters.com/daughters.dat" />
在示例4-37中,我們已經添加了一個包含兩個URL的列表,這將被轉換為調用 RemotePeopleLoader有兩個參數的構造函數,如示例4-38
示例4-38
namespace PersonBinding {
public class RemotePeopleLoader : People {
public RemotePeopleLoader(string url1, string url2) {
// Load People from afar using two URLs
}
}
不幸的是,如果我們把其他的數據類型放入由數據源對象的Property屬性支 持的參數列表,如整型,這將不會被轉換,即使構造函數擁有適當的類型是有效 的;數據源對象只支持創建帶有無參或有參構造函數的對象。如果每一個數據都 必須轉換,你就不得不這麼做了。
4.4.2 XMLDataSource
正如我提及的,對象是僅由數據綁定支持的,但數據究竟不僅僅存為對象。 實際上,大部分數據並不存儲為對象。一種日益流行的方法是把數據存儲到XML 。例如,示例4-39顯示了我們的家庭數據,表示以XML的形式。
示例4-39
<!-- family.xml -->
<Family xmlns="">
<Person Name="Tom" Age="9" />
<Person Name="John" Age="11" />
<Person Name="Melissa" Age="36" />
</Family>
這個文件作為可執行應用程序,在同樣的文件夾中是有效的,我們能夠使用 XmlDataSource綁定到它,正如示例4-40所示。
示例4-40
<!-- Window1.xaml -->
<Window >
<Window.Resources>
<XmlDataSource
x:Key="Family"
Source="family.xml"
XPath="/Family/Person" />
</Window.Resources>
<Grid DataContext="{StaticResource Family}">
<ListBox ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock TextContent="{Binding XPath=@Name}" />
<TextBlock TextContent=" (age: " />
<TextBlock TextContent="{Binding XPath=@Age}" />
<TextBlock TextContent=")" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock >Name:</TextBlock>
<TextBox Text="{Binding XPath=@Name}" />
<TextBlock >Age:</TextBlock>
<TextBox Text="{Binding XPath=@Age}" />
</Grid>
</Window>
注意到,XmlDataSource的使用,帶著一個相對的URL指向family.xml文件, 這個Xpath表達式在Family根元素下推出Person元素。在XAML文件中唯一改變的 是,使用ObjectDataSource綁定Name和Age到TextBox控件,而我們使用Xpath表 達式代替了Path表達式
*對Xpath語法的說明草果了本書的范圍,一個好的參考書目是,Essential XML Quick Reference by Aaron Skonnard and Martin Gudgin (Addison Wesley)。
4.4.2.1 XML數據島
如果你恰好在編譯期知道你的數據,XML數據源也以同樣的方式支持“數據島 ”:由XAML直接創建對象,如示例4-41所示。
示例4-41
<XmlDataSource x:Key="Family" XPath="/Family/Person">
<Family xmlns="">
<Person Name="Tom" Age="9" />
<Person Name="John" Age="11" />
<Person Name="Melissa" Age="36" />
</Family>
</XmlDataSource>
在示例4-41中,我們將XmlDataSource元素下的內容復制到了family.xml中, 去掉Source屬性而保留了Xpath表達式。
盡管如此,既然我們使用XML替代了對象數據,我們示例中的操作需要改動, 如訪問和改變當前項(正如我們對Birthday按鈕的實現),添加新項,排序和過 濾。簡而言之,任何我們使用Person對象集合的地方,都需要改動。另一方面, 在數據項之間移動的一系列方法ICollectionView.MovingCurrentToXxx()繼續工 作的很好,我們的AgeToForegroundValueConverter也是這樣。
IValueConverter.Convert的實現可以繼續工作,因為我們對對象的字符串值 進行語法解析,而不是直接將其轉換為Int32。在Person對象的情形中使用轉換 是首選的,因為Age是Int32類型的,對其進行語法分析是不必要的。盡管如此, 在XML以及我們的應用程序缺少XSD的情形,Age是一個String類型,因此解析它 就是必要的了。
4.4.2.2 XML數據源和訪問數據項
為了訪問和操作XML數據源,取代你的自定義類型實例,你可以使用位於 System.Xml命名空間的XMLElement實例,正如示例4-42所示。
示例4-42
// Window1.xaml.cs
namespace PersonBinding {
public partial class Window1 : Window {
ICollectionView GetFamilyView( ) {
IDataSource ds = (IDataSource)this.FindResource ("Family");
IEnumerable people = (IEnumerable)ds.Data;
return BindingOperations.GetDefaultView(people);
}
void birthdayButton_Click(object sender, RoutedEventArgs e) {
ICollectionView view = GetFamilyView( );
XmlElement person = (XmlElement)view.CurrentItem;
person.SetAttribute("Age",
(int.Parse(person.Attributes["Age"].Value) + 1).ToString( ));
MessageBox.Show(
string.Format(
"Happy Birthday, {0}, age {1}!",
person.Attributes["Name"].Value,
person.Attributes["Age"].Value),
"Birthday");
}
}
}
在示例4-42中,首先要注意的是GetFamilyView的實現,我們不再直接尋找 People集合,而是實現由Xml數據源提供的IEnumerable接口。IEnumerable 是.NET中你能擁有的最簡單接口,仍然有一個集合——是GetdefaultView方法所 需要的。
還要注意示例4-42中集合視圖的CurrentItem屬性,是一個XmlElement實例。 為了增加age,我們訪問元素的Age屬性,取出它的值,將其解析為一個整型,增 加它的值,再將這個整型轉換為String類型,設置為當前元素的新的Age屬性值 。顯示每一個屬性不過是對成對屬性的訪問。
4.4.2.3 XML數據源以及添加數據項
當添加(或移除)一個數據項時,最好訪問XmlDataSource自身,從而可以訪 問Document屬性來創建和添加新元素,正如示例4-43。
示例4-43
void addButton_Click(object sender, RoutedEventArgs e) {
XmlDataSource xds = (XmlDataSource)this.FindResource("Family");
XmlElement person = xds.Document.CreateElement("Person");
person.SetAttribute("Name", "Chris");
person.SetAttribute("Age", "35");
xds.Document.ChildNodes[0].AppendChild(person);
}
這裡,我們使用了XmlDataSource來獲取XmlDocument,以及使用XmlDodument 來創建一個叫做Person的新元素(使之符合其余Person元素),設置Name和Age 屬性,以及在Family根元素下添加這個元素(在頂級Document對象上 ChildNodes[0]是有效的)。
4.4.2.4 XML數據源以及排序
對Xml數據源的條目進行排序,大概會想起我們要使用XmlElements進行處理 ,正如示例4-44。
示例4-44
class PersonSorter : IComparer {
public int Compare(object x, object y) {
XmlElement lhs = (XmlElement)x;
XmlElement rhs = (XmlElement)y;
// Sort Name ascending and Age descending
int nameCompare =
lhs.Attributes["Name"].Value.CompareTo(
rhs.Attributes["Name"].Value);
if( nameCompare != 0 ) {
return nameCompare;
}
return int.Parse(rhs.Attributes["Age"].Value) -
int.Parse(lhs.Attributes["Age"].Value);
}
}
void sortButton_Click(object sender, RoutedEventArgs e) {
ListCollectionView view = (ListCollectionView)GetFamilyView( );
// Managing the view.Sort collection would work, too
if( view.CustomSort == null ) {
view.CustomSort = new PersonSorter( );
}
else {
view.CustomSort = null;
}
}
在示例4-44中,我們進行了排序,正如先前一樣,但是我們從Name和Age屬性 中拉出數據並適當的進行轉換。
4.4.2.5 XML數據源以及過濾
XML的過濾機制非常像對象的過濾,只是我們使用XmlElements進行處理,正 如示例4-45。
示例4-45
void filterButton_Click(object sender, RoutedEventArgs e) {
ICollectionView view = GetFamilyView( );
if( view.Filter == null ) {
view.Filter = delegate(object item) {
return
int.Parse(((XmlElement)item).Attributes["Age"].Value) >= 18;
};
}
else {
view.Filter = null;
}
}
這裡我們的過濾器使用了匿名委托,將每一個數據項轉換為一個XmlElement 元素來進行過濾。
4.4.3相關數據源
目前的版本,WPF沒有直接支持綁定到相關的數據庫,而且間接的支持范圍並 不是很廣。作為WPF一個關於當前狀態的綁定到相關數據的示例,我建議WinFX SDK示例提名為“Binding with Data in an ADO DataSet Sample”
4.4.4自定義數據源
如果你願意利用為遍歷對象提供的間接數據源,但是沒有一個內嵌數據源會 使你滿意,一個自定義的IDataSource實現應該會獲得成功。例如,代替創建 RemotePersonLoader集合來加載或移除家庭數據(在集合的構造函數中添加集合 項,無論如何都有點做作),我們將要創建一個自定義的IDataSource實現,來 達到這一點,如示例4-46。
示例4-46
namespace PersonBinding {
public class Person : INotifyPropertyChanged {}
public class People : ObservableCollection<Person> {}
public class RemotePeopleSource : IDataSource {
People people = null;
public RemotePeopleSource( ) {
// Load People from afar
// Let data binding know we've got data
if( DataChanged != null ) {
DataChanged(this, EventArgs.Empty);
}
}
// IDataSource Members
// Gets the underlying data object
public object Data {
get { return people; }
}
// Occurs when a new data object becomes available
// Especially handy for async object retrieval
public event EventHandler DataChanged;
// Refreshes the data source object using the most current
// values for the object's configuration properties
public void Refresh( ) {
// Not needed in our case
}
}
}
在示例4-46中,通過創建一個People集合的實例,我們已經實現了 IDataSource接口,而且,在構造函數中,在一個神秘的數據遍歷過程之後,我 們激發了一個事件,讓數據綁定知道我們已經得到了數據,還有再次檢查Data屬 性。這個協議特別有用——一旦你進行異步的數據遍歷(像對象數據源那樣)。
如果你的數據源通過自定義屬性,像Asynchronous,一個或更多屬性可以在 運行期被改變。如果你已經得到了多個影響數據遍歷的屬性,你可能不想開始搜 索新數據直到Refresh方法被調用,你可能開始於一個屬性的改變,但是在客戶 端有機會改變其他的屬性之前。