Silverlight入门:第三部分 数据访问

现在我们已经有了一个基础布局,同时也在上面放了一些控件,接下来让我 们开始获取数据。因为我们想要搜索Twitter上的内容,所以我们需要充分使用 他们提供的Web服务API。在继续之前,我要先说明在本次的应用程序中,我们不 会自己建立一个数据库或其它数据源,但是我要指出的是你有很多种方式可以通 过Silverlight访问数据。

数据访问选项

对于在Silverlight中访问数据,初学者的误解之一就是他们在Silverlight 中寻找ADO.NET类库。别找了,找不到的。记住,Silverlight是部署在互联网上 的客户端技术,你不能要求一个浏览器插件去直接访问你的数据库……除非你想 把数据库直接暴露在网络上。我们都知道绝对不能这么做。

所以比较可行的方法是在服务层上暴露数据。这也是Silverlight进行数据通 信的方式。这里有一些主要的访问手段:

* Web服务: SOAP, ASP.NET web services (ASMX), WCF services, POX, REST 终端

* 套接字: 网络套接字通信(Network Socket Communication)

* 文件: 通过Web请求访问静态内容

套接字

套接字可能是最先进的数据访问终端。你需要有一个套接字主机,并且在我 写这篇文章的时候,还需要通过指定的端口范围通信。如果这些你都可以接受, 那么这是非常有效且强大的数据访问方式。但如果你的应用程序是公开面向网络 的,那么我并不认为这种方式可以成为主流。就我的观点,这种方式更加通常被 用于商业应用。这里有一些套接字的资料:

* 软件开发工具包(SDK)文档

* 通过套接字访问数据 (Dan Wahlin, MVP) – 这是Silverlight2的资料, 但不会影响你理解概念

在决定使用它之前,你必须先真正理解你的部署方案,不要盲目地使用它。

文件访问

Silverlight可以与本地文件或网络文件进行交互。对于访问本地文件,虽然 应用程序无法直接访问文件系统,但是仍然可以通过OpenFileDialog和 SaveFileDialog来让用户自己保存数据流到本地,从而进行读写操作。

此外,你还可以通过Silverlight使用标准HTTP命令来读写网上的纯文本文件 以及XML文件信息。

你可能发现自己正是用这种方式来保存程序设置数据或者进行简单的数据访 问的。

Web服务(Web Services)

这是Silverlight访问数据的核心——通过服务层。Silverlight支持在VS中 使用我们熟悉的添加服务引用的方式来访问基础ASP.NET Web Services(ASMX)或 者基于WCF的服务,并且还将会为你生成强类型的代理代码。

另外,你还可以通过标准HTTP协议访问POX(Plain old XML)或基于REST的 终端。理解这些不同服务类型的应用场合,通常是开发人员学会了解自己的项目 最适合什么数据访问方式的最佳途径。

上面的第三项.NET RIA服务是一个新的框架,旨在使数据访问更加简单方便 。链接的视频将会为你介绍那个主题。如果你有一个与你的Silverlight应用程 序放在一起的数据库,那么如果想要让这个数据库为你的Silverlight应用程序 服务的话,.NET RIA服务是最好的方法。异步访问

所有在Silverlight中进行的数据访问都是异步的。这可能是专门从事Web开 发的开发人员跃跃欲试的另一个领域。比如在服务器端,这样的写法看起来可能 很合理:

1 MyWebService svc = new MyWebService();
2 string  foo = svc.GetSomeValue();
3 MyTextBox.Text = foo;

在Sliverlight中你就不能用这种同步调用的做法了。对于那些没有作过异步 编程的开发人员来说可能会觉得迷茫,但这是值得学习的,因为通过它你会成为 一名更优秀的开发人员。在Silverlight中,上面的伪代码应该被写成这样:

1 MyWebService svc = new MyWebService();
2  svc.Completed += new CompletedHandler(OnCompleted);
3  svc.GetSomeValue();
4
5 void OnCompleted(object sender,  EventArgs args)
6 {
7     MyTextBox.Text =  args.Result;
8 }

注意到你在一个Completed事件处理器中使用服务返回的结果,这个调用模式 你将会在基本数据访问中经常看到。

跨域数据访问

因为Silverlight是Web客户端技术,所以它运行于浏览器的安全沙箱(Sand Box)里,并在访问策略上有所限制。限制的其中之一就是跨域访问。除非某个 服务设置成允许跨域调用,否则如果你的应用程序与想要调用的服务处于不同的 域名,你是没有办法访问它的。设置这个的途径通常是通过跨域策略文件。 Silverlight与其它富客户端插件一样,都要遵循这种策略。作为 Silverlight 开发人员你可能会在有些时候遇到这个问题,晚学不如早学,这里有一些资料:

Silverlight中的跨域策略文件

跨域策略文件助手

跨域访问的疑难解答以及有用的工具

我的Silverlight无法访问我的服务!

跨域通信概述

在我们的Twitter应用程序中,我们实际上是在访问承载在其它地方的服务, 所以我们需要遵循这些协议。幸运的是,Twitter查询API通过他们的跨域策略文 件开启了跨域访问(http://search.twitter.com/crossdomain.xml)。 Twitter的其它功能并没有开启跨域访问,这就是为什么你现在不能通过 Silverlight直接访问Twitter内容。在这种情况下你将通过自己的服务来调用那 些代理服务,使得你可以通过Silverlight的策略文件开启跨域访问。头晕吗? 其实它比听起来简单多了。

广泛流传的说法:你需要将Silverlight与Adobe跨域策略文件放在服务端以 开启跨域访问。这个说法是不正确的,我常常看见有人说“我已经准备好了 crossdomain.xml和clientaccesspolicy.xml,但它仍然无法工作。”,如果你 为Silverlight创建一个跨域访问服务,你只需要clientaccesspolicy.xml文件 格式就行了,这也是我们最先找到的,同时对Silverlight来说也最灵活、最安 全。

现在我们已经获得了总体的概念,让我们来访问数据吧!调用Twitter API

Twitter 搜索API是一个简单的基于API的REST——我们只想在应用程序中调 用GET请求。他们提供的格式符合Atom规范(Atom Specification),使得我们 的工作容易得多,因为这是基础格式,同时受到Silverlight框架库的直接支持 。当用户在查询输入框中输入内容,并点击查询按钮以后,我们将会调用这个 API。让我们像在第一章创建Hello World示例的那样,为搜索按钮关联一个事件 。在我们的Search.xaml页面,我为搜索按钮添加了一个事件。添加按钮事件处 理器到查询按钮,并且将方法名称命名为SearchForTweets:

1 <Button x:Name="SearchButton" Width="75"  Content="SEARCH" Click="SearchForTweets" />

在 VS中,如果你右击方法名称,你就能导航到事件处理器,并且它会在代码 页自动为你生成代码存根。在这个方法中,我们将寻找并使用符合我们标准的 Twitter API。因为这个API是一个简单的REST GET调用,所以我们准备使用简单 的WebClient Silverlight API。这是最简单的网络API,允许你通过GET/POST命 令来读写数据,前提是你不需要改变头记录。在这之前我还准备了一些成员变量 用于追踪并监视我们的查询条目。

1 const string SEARCH_URI =  "http://search.twitter.com/search.atom?q={0}&since_id={1}";
2  private string _lastId = "0";
3 private bool _gotLatest =  false;

现在我们可以建立Twitter搜索功能了。记得我曾经提到在Silverlight中的 网络访问是异步的吗?现在就让我们来体验一下。我们将在 WebClient中使用 OpenRead API。因为整个过程是异步的,所以我们需要建立一个Completed事件 处理器,通过它来接收返回数据并且执行我们需要的操作,像是这样:

1   private void SearchForTweets(object sender,  RoutedEventArgs e)
2   {
3       WebClient proxy =  new WebClient();
4       proxy.OpenReadCompleted += new  OpenReadCompletedEventHandler(OnReadCompleted);
5        proxy.OpenReadAsync(new Uri(string.Format(SEARCH_URI,  HttpUtility.UrlEncode(SearchTerm.Text), _lastId)));
6   } 
7
8   void OnReadCompleted(object sender,  OpenReadCompletedEventArgs e)
9   {
10       throw  new NotImplementedException();
11 }

注意到我们首先创建了一个WebClient实例,然后我们设置了一个Completed 事件处理器,最后我们调用了OpenReadAsync方法并传入了一个URI。我们在 Completed事件处理器中获取的返回结果(e.Result)将会是数据流。因为我们 准备处理返回数据并作数据绑定,所以我们准备创建一个可以用于表示搜索结果 的结构的本地类(实体类)。我把它的名字命名为TwitterSearchResult.cs并且 放在项目的Model 文件夹下。

1   using System;
2   using  System.Windows.Media;
3
4   namespace  TwitterSearchMonitor.Model
5   {
6       public class  TwitterSearchResult
7       {
8            public string Author { get; set; }
9            public string Tweet { get; set; }
10         public  DateTime PublishDate { get; set; }
11          public string ID { get; set; }
12         public  ImageSource Avatar { get; set; }
13     }
14 }

我们可以在实体类的内部构造输出结果或绑定数据。

其它网络选项:HttpWebRequest和ClientHttp

我们还可以使用另外两个网络API来访问Twitter API,它们分别是 HttpWebRequest和ClientHttp。我们通常通过WebClient来使用HttpWebRequest ,因为它是围绕这个API制作的简单包装器。但如果你需要对请求的头部作更加 精细的控制,那么你应该使用HttpWebRequest。它们都用到了浏览器的网络协议 栈,因此也就带来了一些局限性,那就是不能接收所有的HTTP状态码或是使用一 些扩展变量(PUT/DELETE)。Silverlight还推出了一个使用自定义网络协议栈 的ClientHttp,使你可以使用更多的变量,还能接收到除了200、404之外的其它 HTTP状态码。更多内容:

* 如何:指定浏览器或客户端 HTTP 处理(原版)

下面是一个调用ClientHttp的例子:

1 private void SearchForTweets(object sender,  RoutedEventArgs e)
2 {
3     bool httpBinding =  WebRequest.RegisterPrefix ("http://search.twitter.com",
WebRequestCreator.ClientHttp);
4      WebClient proxy = new WebClient();
5      proxy.OpenReadCompleted += new OpenReadCompletedEventHandler (OnReadCompleted);
6     proxy.OpenReadAsync(new Uri (string.Format(SEARCH_URI, HttpUtility.UrlEncode (SearchTerm.Text))));
7 }

注意,我们并没有真的在应用程序中使用这个方法,而仅仅只是向你展示它 。调用RegisterPrefix方法意味着我们想用ClientHttp网络协议栈来替代浏览器 的网络协议栈。在上面的例子中,我们仅仅传入了Twitter的搜索域名,但是实 际上我们可以传入任何HTTP请求。

这些对你来说是需要在应用程序中考虑的额外内容。

开始通过智能对象绑定一些简单的数据

因为我们的应用程序需要监视Twitter上面的搜索内容,所以我们需要将结果 绑定到一个对象集合中,然后再只对那个集合进行操作(在这个例子中,我们所 做的是向里面添加内容)。为此,我们要使用Silverlight中两个有用的对象: ObservableCollection<T> 和PagedCollectionView。 ObservableCollection是一种当它的元素被改动时(包括新增、删除、修改)可 以自动提供通知的集合类型,PagedCollectionView将帮助我们为对象自动排序 。

我们在项目中定义这些成员变量:

1 ObservableCollection<TwitterSearchResult>  searchResults = new  ObservableCollection<TwitterSearchResult>();
2  PagedCollectionView pcv;

现在我们已经定义完成员变量了,接下来让我在控件的构造器中初始化 PagedCollectionView,好让它尽可能快地可用。我们还需要在XAML中将用户界 面绑定到元素上面。不在你的用户控件的构造器中做任何控制用户界面的操作是 一个好做法(在我们的例子中是Search.xaml)。正因

如此,我们将在构造器中添加一个Loaded事件处理器,并且在那里执行初始 化数据绑定。构造器和事件处理器中的内容应该像是这样:

1 public Search()
2 {
3      InitializeComponent();
4
5     pcv = new  PagedCollectionView(searchResults);
6      pcv.SortDescriptions.Add(new System.ComponentModel.SortDescription ("PublishDate",  System.ComponentModel.ListSortDirection.Ascending));
7
8      Loaded += new RoutedEventHandler(Search_Loaded);
9 } 
10
11 void Search_Loaded(object sender, RoutedEventArgs  e)
12 {
13     SearchResults.ItemsSource = pcv;
14  }

注意到我们在Loaded事件处理器中将已经排好序的PagedCollectionView(我 们在第6行调用了SortDescription)赋值给了DataGrid(名称叫SearchResults )的ItemsSource属性。现在我们的用户界面已经绑定到了 PagedCollectionView,也许我们应该填充数据了。记住它实际上是我们的 ObservableCollection<TwitterSearchResult>的数据的视图,所以我们 要向其中添加项目以查看变化。填充ObservableCollection

现在回到之前创建的OnReadCompleted方法,我们现在要填充 ObservableCollection到这个方法中:

1   void OnReadCompleted(object sender,  OpenReadCompletedEventArgs e)
2   {
3       if  (e.Error == null)
4       {
5            _gotLatest = false;
6           XmlReader rdr =  XmlReader.Create(e.Result);
7
8            SyndicationFeed feed = SyndicationFeed.Load(rdr);
9
10          foreach (var item in feed.Items)
11          {
12             searchResults.Add(new  TwitterSearchResult() { Author = item.Authors[0].Name, ID =  GetTweetId(item.Id), Tweet = item.Title.Text, PublishDate =  item.PublishDate.DateTime.ToLocalTime(), Avatar = new BitmapImage (item.Links[1].Uri) });
13             _gotLatest =  true;
14         }
15
16          rdr.Close();
17     }
18     else
19    {
20          ChildWindow errorWindow = new ErrorWindow (e.Error);
21         errorWindow.Show();
22      }
23 }
24
25 private string GetTweetId(string  twitterId)
26 {
27     string[] parts =  twitterId.Split(":".ToCharArray());
28     if (!_gotLatest) 
29     {
30         _lastId = parts [2].ToString();
31     }
32     return parts [2].ToString();
33 }

这里做了几件事,首先,e.Result包含了我们搜索成功所返回的数据流,如 果搜索失败了,我们将使用我们的导航模板提供的ErrorWindow模板。 _gotLatest成员变量可以帮助我们跟踪我们是否需要重新设置最大值(我们只想 返回最新的查询结果)。之后我们将获得的数据流加载到 XmlReader,以便于用 SyndicationFeed类来解析它。SyndicationFeed在 System.ServiceModel.Syndication类库中,所以你不得不在项目中添加引用。 它的内置方法可以处理像是RSS、Atom这样的已知聚合格式。

注意:System.ServiceMode.Syndication使得你的项目还要依赖其它程序集 。这个类库虽然不小,但却也很方便。谨慎地使用它,你应该了解使用它的时机 和原因。我们在这里使用它是为了让你感觉到它带来的开发效率和功能。还有另 一种方法(常见于聚合新闻阅读器)就是仅使用LINQ to SQL,在读取以后查询 结果XDocument。同样,出于演示的目的,我想指出作为强类型的类, SyndicationFeed能提升你的开发效率。

更多关于读取聚合数据的资料:

* 处理聚合数据

一旦我们有了已经加载数据的SyndicationFeed,我们只需要遍历它然后将新 的Twitter搜索结果添加到 ObservableCollection对象里。你会注意到我们为了 之后绑定更加简单,将图片的URL转换成了图片源(ImageSource)。此外,我们 还解析出Twitter的ID,用于设置首个结果(最新的结果),并把它作为上次查 询之后最新的ID(_LastId)。为用户提供回馈

在最后一步,我们想确保当我们执行操作的时候(搜索),用户可以得到一 些回馈。幸运的是这很容易通过ActivityControl做到,在我写这篇文章的时候 ,ActivityControl是.NET RIA Services模板的一部分,但你可以从大卫的投票 博客获取它。你需要自己编译这个控件,然后在项目中添加对它的引用(如果你 下载我们的示例应用程序源代码,那么它已经被编译好并被包含在了类库文件夹 中)。

添加引用以后,你就可以像我们第二部分中添加DataGrid那样在Search.xaml 中添加xaml标记。我们添加这个控件作为根控件,然后把DataGrid作为它的子控 件包含在其中。最后我们的Search.xaml文件看起来应该像这样:

1   <navigation:Page
2            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
5            xmlns:mc="http://schemas.openxmlformats.org/markup- compatibility/2006"
6           mc:Ignorable="d"
7            xmlns:navigation="clr- namespace:System.Windows.Controls;assembly=System.Windows.Controls.Nav igation"
8           xmlns:data="clr- namespace:System.Windows.Controls;assembly=System.Windows.Controls.Dat a" x:Class="TwitterSearchMonitor.Views.Search"
9            xmlns:activity="clr- namespace:System.Windows.Controls;assembly=ActivityControl"
10          d:DesignWidth="640" d:DesignHeight="480"
11          Title="Twitter Search Page">
12      <activity:Activity x:Name="ActivityIndicator">
13            <Grid x:Name="LayoutRoot">
14                <Grid.RowDefinitions>
15                    <RowDefinition Height="32"/>
16                    <RowDefinition/>
17                </Grid.RowDefinitions>
18
19                <StackPanel HorizontalAlignment="Left" Margin="0,- 32,0,0" VerticalAlignment="Top" Grid.Row="1"  Orientation="Horizontal">
20                    <TextBox x:Name="SearchTerm" FontSize="14.667"  Margin="0,0,10,0" Width="275" TextWrapping="Wrap"/>
21                    <Button x:Name="SearchButton"  Width="75" Content="SEARCH" Click="SearchForTweets" />
22                </StackPanel>
23                <data:DataGrid x:Name="SearchResults"  Margin="0,8,0,0" Grid.Row="1"/>
24            </Grid>
25     </activity:Activity>
26  </navigation:Page>

在开始搜索处还需要添加一行代码:

1 ActivityIndicator.IsActive = true;

在OnReadCompleted事件完成以后添加:

1 ActivityIndicator.IsActive = false;

这样终端用户就能看到一个滚动的进度条。现在让我们按F5(运行程序), 并且输入一些搜索条件来看看效果:

在DataGrid中显示的搜索结果视图:

添加一些计时器

既然我们要做一个监视服务,那么我们当然需要让程序自动刷新查询结果。 Silverlight提供了几种不同的方式来触发自动活动。我们准备在应用程序中使 用DispatcherTimer,它只是一个在指定时间间隔触发事件的计时器。我们将添 加一个成员变量:

1 DispatcherTimer _timer;

然后在构造器中初始化我们的计时器并传入一个事件处理器:

1 double interval = 30.0;
2
3 _timer = new  DispatcherTimer();
4 #if DEBUG
5 interval = 10.0;
6  #endif
7 _timer.Interval = TimeSpan.FromSeconds(interval);
8  _timer.Tick += new EventHandler(OnTimerTick);

现在我们要重构一些代码。我们想去掉时间事件的SearchForTweets方法。我 们将使用Visual Studio的重构工具把SearchForTweets的方法提取到新的方 法"SearchForTweetsEx"中,我们还将为它生成一个叫做 OnTimerTick的事件处 理器。我们还要修改Loaded事件来启动计时器,同时还初始化搜索(注意在调试 模式下间隔为10秒,否则为30秒)。我们重构完成的Search.xaml看起来应该像 这样:

1     using System;
2     using  System.Collections.ObjectModel;
3     using System.Net;
4      using System.Net.Browser;
5     using  System.ServiceModel.Syndication;
6     using  System.Windows;
7     using System.Windows.Browser;
8      using System.Windows.Controls;
9     using  System.Windows.Data;
10   using  System.Windows.Media.Imaging;
11   using  System.Windows.Navigation;
12   using  System.Windows.Threading;
13   using System.Xml;
14    using TwitterSearchMonitor.Model;
15
16   namespace  TwitterSearchMonitor.Views
17   {
18       public  partial class Search : Page
19       {
20            const string SEARCH_URI =  "http://search.twitter.com/search.atom?q={0}&since_id= {1}";
21           private string _lastId =  "0";
22           private bool _gotLatest =  false;
23            ObservableCollection<TwitterSearchResult> searchResults = new  ObservableCollection<TwitterSearchResult>();
24            PagedCollectionView pcv;
25            DispatcherTimer _timer;
26
27           public  Search()
28           {
29                InitializeComponent();
30
31              //  set interval value for Timer tick
32                double interval = 30.0;
33
34               _timer = new DispatcherTimer();
35                #if DEBUG
36               interval = 10.0;
37                #endif
38                _timer.Interval = TimeSpan.FromSeconds(interval);
39                _timer.Tick += new EventHandler (OnTimerTick);
40
41              // initialize  our PagedCollectionView with the ObservableCollection
42               // and add default sort
43               pcv = new PagedCollectionView(searchResults);
44               pcv.SortDescriptions.Add(new  System.ComponentModel.SortDescription("PublishDate",  System.ComponentModel.ListSortDirection.Descending));
45
46                Loaded += new RoutedEventHandler (Search_Loaded);
47           }
48
49            void OnTimerTick(object sender, EventArgs e)
50            {
51                SearchForTweetsEx();
52           }
53
54            void Search_Loaded(object sender, RoutedEventArgs  e)
55           {
56                SearchResults.ItemsSource = pcv; // bind the DataGrid
57                _timer.Start(); // start the timer
58                SearchForTweetsEx(); // do the  initial search
59           }
60
61            // Executes when the user navigates to this  page.
62          protected override void  OnNavigatedTo(NavigationEventArgs e)
63          {
64           }
65
66          private void  SearchForTweets(object sender, RoutedEventArgs e)
67           {
68              SearchForTweetsEx();
69           }
70
71          ///  <summary>
72          /// Method that actually  does the work to search Twitter
73          ///  </summary>
74          private void  SearchForTweetsEx()
75          {
76               if (!string.IsNullOrEmpty(SearchTerm.Text))
77               {
78                    _timer.Stop(); // stop the timer in case the search takes  longer than the interval
79                   ActivityIndicator.IsActive = true; // set the visual indicator  80
81                  // do the work to  search twitter and handle the completed event
82                    WebClient proxy = new WebClient();
83                    proxy.OpenReadCompleted += new  OpenReadCompletedEventHandler(OnReadCompleted);
84                   proxy.OpenReadAsync(new Uri(string.Format (SEARCH_URI, HttpUtility.UrlEncode(SearchTerm.Text),  _lastId)));
85              }
86           }
87
88          /// <summary>
89           /// Method that fires after our SearchForTweetsEx  runs and gets a result
90          ///  </summary>
91          /// <param  name="sender"></param>
92          ///  <param name="e"></param>
93          void  OnReadCompleted(object sender, OpenReadCompletedEventArgs e)
94           {
95              if (e.Error  == null)
96              {
97                   _gotLatest = false; // reset the latest  detector
98                  XmlReader rdr =  XmlReader.Create(e.Result); // load stream into a  reader
99
100                SyndicationFeed  feed = SyndicationFeed.Load(rdr); // load syndicated feed  (Atom)
101
102                // parse each  item adding it to our ObservableCollection
103                 foreach (var item in feed.Items)
104                 {
105                     searchResults.Add(new TwitterSearchResult() { Author =  item.Authors[0].Name, ID = GetTweetId(item.Id), Tweet =  item.Title.Text, PublishDate =  item.PublishDate.DateTime.ToLocalTime(), Avatar = new BitmapImage (item.Links[1].Uri) });
106                     _gotLatest = true; // reset the fact that we already have  the max id needed
107                } 
108
109                rdr.Close(); // close  the reader
110            }
111             else
112            {
113                 // initialize our ErrorWindow with exception  details
114                ChildWindow errorWindow  = new ErrorWindow(e.Error);
115                 errorWindow.Show();
116            }
117             ActivityIndicator.IsActive = false; // reset the  UI
118            _timer.Start(); // reset the  timer
119        }
120
121        ///  <summary>
122        /// Parses out the Tweet ID  from the tweet
123        /// </summary>
124         /// <param  name="twitterId"></param>
125        ///  <returns></returns>
126        private string  GetTweetId(string twitterId)
127        {
128             string[] parts = twitterId.Split(":".ToCharArray ());
129            if (!_gotLatest)
130             {
131                _lastId =  parts[2].ToString();
132            }
133             return parts[2].ToString();
134        } 
135     }
136 }

当我们的搜索页面被载入时,将会启动一个计时器,同时初始化搜索。然后 当刷新间隔到了以后,将会重新搜索。但是记住,程序将搜索上一次搜索之后的 新记录,而不会重新读取所有的记录。因为已经将DataGrid绑定到 ObservableCollection了,所以新的搜索数据将会被添加到上面,它将会自动以 排序的形式显示在用户界面上。

我们还增加了一些检查,确保搜索条件不为空值。

总结

在本部分我们取得了很大的进展。我们建立起了一个调用第三方服务的服务 ,通过绑定把它关联到DataGrid,添加了一个计时器用于自动获取服务。我们可 以结束了,但是我们不能——DataGrid并不是我们想展示给最终用户的界面。让 我们进入第四部分,我们要做一些数据模板,并为你介绍XAML绑定语法。

Powered By D&J (URL:http://www.cnblogs.com/Areas/)
原文地址:https://www.cnblogs.com/Areas/p/2169791.html