《Programming WPF》翻译 第4章 4.数据源

目前为止,我们已经简单的处理了对象。然而,这并不是数据的唯一来源;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}” >

这样做的原因是WPFIDataSource提供内建的支持,因此没有必要间接地这样做。

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绑定NameAgeTextBox控件,而我们使用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对象的情形中使用转换是首选的,因为AgeInt32类型的,对其进行语法分析是不必要的。尽管如此,在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.3XML数据源以及添加数据项

当添加(或移除)一个数据项时,最好访问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元素),设置NameAge属性,以及在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中,我们进行了排序,正如先前一样,但是我们从NameAge属性中拉出数据并适当的进行转换。

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方法被调用,你可能开始于一个属性的改变,但是在客户端有机会改变其他的属性之前。

posted @ 2008-04-04 12:46  包建强  Views(1126)  Comments(3Edit  收藏  举报