Silverlight ——数据访问

数据访问选项

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

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

  • Web服务: SOAP, ASP.NET web services (ASMX), WCF services, POX, REST 终端
  • 套接字: 网络套接字通信(Network Socket Communication)
  • 文件: 通过Web请求访问静态内容

套接字

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

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

文件访问

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

此外,你还可以通过Silverlight使用标准HTTP命令来读写网上的纯文本文件以及XML文件信息。下面是使用这些方法的一些辅助资料。

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

Technorati 标签: Silverlight,数据访问,入门

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开发人员你可能会在有些时候遇到这个问题,晚学不如早学,这里有一些资料:

在我们的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状态码。

更多内容:

下面是一个调用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.Navigation"
8          xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data" 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了,所以新的搜索数据将会被添加到上面,它将会自动以排序的形式显示在用户界面上。

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

 

http://msdn.microsoft.com/zh-cn/ff432734.aspx

posted @ 2012-06-18 16:00  路灯下的诗人  阅读(474)  评论(0编辑  收藏  举报