《Programming WPF》翻译 第4章 4.数据源
目前为止,我们已经简单的处理了对象。然而,这并不是数据的唯一来源;XML和突然想到的相关数据库,都是流行的选择。更进一步地,由于XML或
相关数据库并不能存储数据为.NET对象,某些转换可能需要支持数据绑定,正如你会想到的,需要数据源对象上的.NET属性。而且即使我们可以直接在xaml中声明对象,仍然希望有一个层间接地从其他源中拉数据,甚至于将这个工作交给一个工作线程,如果说取回是一个呆板的操作。
简而言之,为了对象的转换和加载,我们希望间接的而不是直接的声明方式。对于这个间接方式,我们必须致力于IDataSource接口的实现,其中一种就是数据对象源。
4.4.1数据对象源
一种对IDataSource接口的实现是,为所有的操作提供一个间接的层,这些操作用于生成要绑定到的对象。例如,如果我们想要在Web上加载一组Person对象,我们需增强一些代码中的逻辑,如示例4-34。
示例4-34
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
<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语法的编译集信息。
<?Mapping
XmlNamespace="local"
ClrNamespace="Bar" Assembly="foo" /><Window xmlns="local">
<Window.Resources>
<DataTemplate
DataType="{x:Type local:Quux}"></DataTemplate>
</Window.Resources>
</Window>
然而,ObjectDataSource以自己的方式设置type的信息。
<ObjectDataSource x:Key="foo" TypeName="Bar.Quux, foo" />
愿望是美好的,现实是残酷的。在RTM版本之前,两种技术都是合理的。
伴随着对象数据源担当数据和绑定之间的中介者,我们需要更新代码,当我们遍历People集合时(现在是一个基本类RemotePeopleLoader,但是仍然是Person对象的容器),正如示例4-36所示。
示例4-36
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,我们仍然像从前一样绑定了列表框。
<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
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
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 xmlns="">
<Person Name="Tom" Age="9" />
<Person Name="John" Age="11" />
<Person Name="Melissa" Age="36" />
</Family>
这个文件作为可执行应用程序,在同样的文件夹中是有效的,我们能够使用XmlDataSource绑定到它,正如示例4-40所示。
示例4-40
<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
<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
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.3XML数据源以及添加数据项
当添加(或移除)一个数据项时,最好访问XmlDataSource自身,从而可以访问Document属性来创建和添加新元素,正如示例4-43。
示例4-43
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
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
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
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方法被调用,你可能开始于一个属性的改变,但是在客户端有机会改变其他的属性之前。