Windows Phone云应用开发实践之(三)——OData
Windows Phone云应用开发实践(三)——OData
开放数据协议(OData)
概述
开放数据协议(Open Data Protocol ,简称OData)基于实体和关系模型,使您能够以具象状态传输 (REST) 资源的样式访问数据。通过使用 Windows Phone的OData客户端库,Windows Phone应用程序可以使用标准HTTP协议来执行查询,甚至创建、更新和删除数据服务中的数据。客户端可以生成对任何支持OData协议的服务的 HTTP 请求,并且可以将响应源中的数据转换为对象。
OData客户端库的两个主要类为DataServiceContext 类和DataServiceCollection<(Of <(<'T>)>)>类。DataServiceContext类封装对特定数据服务执行的操作。基于OData的服务是无状态的。但是,DataServiceContext 在与数据服务的交互之间和应用程序的不同执行阶段中维持客户端上实体的状态。这使得客户端能够支持更改跟踪和标识管理之类的功能。对于异步访问用于Silverlight的WCF数据服务客户端和.NET Framework中所含WCF数据服务客户端中提供的OData服务,Windows Phone的OData客户端库提供了相同的功能。
移动设备应用程序依赖于远程服务提供数据源, Windows Azure平台正是为Windows Phone应用程序扮演数据源服务的角色,而OData是Windows Azure平台的主要数据访问机制。Windows Azure平台的各种组件提供OData数据服务,包括Windows Azure存储、Microsoft SQL Azure和Windows Azure DataMarket。
动手实践——分页显示OData数据
本节的主要内容包括:
- 绑定数据
- 分页和导航
- 管理应用的状态
- 生成客户端数据类
OData On Phone应用是Netflix在线影片租赁DVD信息显示的Windows Phone客户端应用。
- 绑定数据
OData On Phone应用在MVVM设计模式实现数据绑定和多个页面之间的导航。在MVVM设计模式下,将DataServiceContext公开为视图模型(ViewModel),返回DataServiceCollection 实例给任何动态绑定视图模型的控件。下面的对象关系图表示本例中的MainViewModel类。
下面的代码是定义MainViewModel类的DataServiceContext和DataServiceCollection属性。
Project: ODataWinPhoneQuickstart File: MainViewModel.cs
// Defines the root URI of the data service.
private static readonly Uri rootUri = new Uri("http://odata.netflix.com/v1/Catalog/");
// Define the typed DataServiceContext.
private NetflixCatalog _context;
// Define the binding collection for Titles.
private DataServiceCollection<Title> _titles;
// Gets and sets the collection of Title objects from the feed.
public DataServiceCollection<Title> Titles
{
get { return _titles; }
private set
{
// Set the Customers collection.
_titles = value;
// Register a handler for the LoadCompleted callback.
_titles.LoadCompleted += OnTitlesLoaded;
// Raise the PropertyChanged events.
NotifyPropertyChanged("Titles");
}
}
MainViewModel类本身被作为应用程序根类(app)的一个静态属性,代码如下。
Project: ODataWinPhoneQuickstart File: App.xaml.cs
static MainViewModel _viewModel = null;
// A static ViewModel used by the views to bind against.
public static MainViewModel ViewModel
{
get
{
// Delay creation of the view model until we need it.
if (_viewModel == null)
{
_viewModel = new MainViewModel();
}
return _viewModel;
}
}
在视图模型中设置页面的数据上下文(DataContext),实现将ListBox控件与MainViewModel类的DataServiceCollection属性的绑定,本例中DataServiceCollection属性返回影片的标题。在下面的XAML示例显示ListBox控件和几个其他元素绑定与MainViewModel的属性绑定。
Project: ODataWinPhoneQuickstart File: App.xaml.cs
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<ListBox Margin="0,0,-12,0" ItemsSource="{Binding Titles}"
SelectionChanged="OnSelectionChanged" Height="Auto">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,17" Width="432" Orientation="Horizontal">
<Image Source="{Binding Path=StreamUri}"
Height="75" Width="50" />
<StackPanel Orientation="Vertical">
<StackPanel.Resources>
<converter:TruncateSynopsis x:Key="synopsis" />
</StackPanel.Resources>
<TextBlock Text="{Binding Path=ShortName}" TextWrapping="Wrap"
Style="{StaticResource PhoneTextLargeStyle}"/>
<TextBlock Text="{Binding Path=ShortSynopsis,
Converter={StaticResource synopsis}}"
TextWrapping="Wrap" Margin="12,-6,12,0"
Style="{StaticResource PhoneTextSubtleStyle}"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
<StackPanel Grid.Row="2" Orientation="Horizontal">
<StackPanel Orientation="Vertical">
<TextBlock Name="PagingInfo" Text="{Binding PagesLoadedText}" Margin="12,10,10,0"
Width="270" Style="{StaticResource PhoneTextNormalStyle}" Height="30"
TextAlignment="Center" />
<TextBlock Name="NetflixBranding" Text="...delivered by Netflix" Margin="12,5,10,5"
Width="270" Style="{StaticResource PhoneTextNormalStyle}" Height="30"
TextAlignment="Left" />
</StackPanel>
<Button Name="MoreButton" Content="Next Page"
HorizontalAlignment="Right" Width="185" Height="80" Click="MoreButton_Click" />
</StackPanel>
当MainPage.xaml加载时,视图模式(ViewModel)检查IsDataLoaded属性。如果IsDataLoaded属性为false则调用LoadData方法为绑定的控件加载数据。
Project: ODataWinPhoneQuickstart File: MainViewModel.cs
// Used to determine whether the data is loaded.
public bool IsDataLoaded { get; private set; }
// Loads data when the application is initialized.
public void LoadData()
{
// Instantiate the context and binding collection.
_context = new NetflixCatalog(_rootUri);
// Use the public property setter to generate a notification to the binding.
Titles = new DataServiceCollection<Title>(_context);
// Load the data.
Titles.LoadAsync(GetQuery());
}
当调用LoadAsync方法时,Windows Phone应用发送异步查询请求至OData服务,OData服务返回Atom的查询结果。当Windows Phone应用收到的OData服务返回数据时,LoadCompleted的响应事件OnTitlesLoaded被执行,将返回的数据集合绑定到列表(ListBox)框控件中的对象实例。本例中OData返回的影片标题的集合。本例的Netflix的目录中,标题实体是媒体的资源链接,即标题类的StreamUri属性调用GetReadStreamUri获取媒体资源的链接URI。
建议调用GetReadStreamUri方法获取多媒体资源的链接。
Project: ODataWinPhoneQuickstart File: Title.cs
// Extend the Title class to bind to the media resource URI.
public partial class Title
{
// Returns the media resource URI for binding.
public Uri StreamUri
{
get
{
// Get the URI for the media resource stream.
return App.ViewModel.GetReadStreamUri(this);
}
}
}
- 分页和导航
使用DataServiceQuery类实现分页的LINQ查询。OData有两种方式可实现分页,客户端可采用Take和Skip的LINQ方法获取返回的逻辑分页,以此来限制返回数据的条数。这些LINQ查询方法被OData库翻译为$stop和$skip的系统查询操作。当然OData数据服务本身也可实现限制数据实体条数,在数据服务的响应中包含有延续标记(continuation token),延续标记可用于从数据服务中获取下一页的数据项。
默认情况下,OData源将为源返回ATOM的表示形式,当从Web浏览器访问时,结果将是ATOM源。如果请求的接受标头改为"application/json",结果将是JSON源形式的相同数据。
下面显示的GetQuery 方法,返回强类型DataServiceQuery类实例。在此示例中,页面大小固定且在获取数据后计算每页显示的数据条目的数量。当我们加载第一页时,调用IncludeTotalCount方法计算数据实体的条数,IncludeTotalCount方法中使用$inlinecount=allpages系统查询选项获取总查询结果的总数量。
Project: ODataWinPhoneQuickstart File: MainViewModel.cs
// Private method that returns the page-specific query.
private DataServiceQuery<Title> GetQuery()
{
// Get a query for the Titles feed from the context.
DataServiceQuery<Title> query = _context.Titles;
if (_currentPage == 0)
{
// If this is the first page, then also include a count of all titles.
query = query.IncludeTotalCount();
}
// Add paging to the query.
query = query.Skip(_currentPage * _pageSize)
.Take(_pageSize) as DataServiceQuery<Title>;
return query;
}
当收到响应时,客户端执行LoadCompleted的事件响应的处理函数OnTitlesLoaded。服务器驱动页面加载时,DataServiceCollection的继承属性返回一个延续标记。下面的方法执行LoadCompleted事件响应,加载服务器驱动的分页数据。
Project: ODataWinPhoneQuickstart File: MainViewModel.cs
private void OnTitlesLoaded(object sender, LoadCompletedEventArgs e)
{
if (e.Error == null)
{
// Make sure that we load all pages of the Customers feed.
if (Titles.Continuation != null)
{
Titles.LoadNextPartialSetAsync();
}
// Set the total page count, if we requested one.
if (e.QueryOperationResponse.Query
.RequestUri.Query.Contains("$inlinecount=allpages"))
{
_totalCount = (int)e.QueryOperationResponse.TotalCount;
}
IsDataLoaded = true;
// Update the pages loaded text binding.
NotifyPropertyChanged("PagesLoadedText");
}
else
{
// Display the error message in the binding.
this.Message = e.Error.Message;
}
}
当用户点击"下一页"导航按钮,在MainPage.xaml中显示下一个逻辑页的数据。MoreButton_Click函数实现响应用户的触控操作。
Project: ODataWinPhoneQuickstart File: MainViewModel.cs
private void MoreButton_Click(object sender, RoutedEventArgs e)
{
if (App.ViewModel.IsDataLoaded)
{
// Navigate to the next page of data.
this.NavigationService.Navigate(
new Uri("/MainPage.xaml?page=" + (App.ViewModel.CurrentPage + 1), UriKind.Relative));
}
}
"下一步"导航按钮的本质上在是MainPage.xaml中重新加载的下一逻辑页的数据,重载OnNavigatedTo方法实现加载特定逻辑页的数据。
Project: ODataWinPhoneQuickstart File: MainViewModel.cs
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (!App.ViewModel.IsDataLoaded)
{
App.ViewModel.LoadData();
}
else
{
if (this.NavigationContext.QueryString.Count == 1)
{
// Get the value of the requested page.
int page = int.Parse(this.NavigationContext.QueryString["page"]);
// Check to see if the page is currently loaded.
if (page != App.ViewModel.CurrentPage)
{
// Load data for the specific page.
App.ViewModel.LoadData(page);
}
}
else
{
// If there is no query parameter we are at the first page.
App.ViewModel.LoadData(0);
}
}
}
当用户点击ListBox控件中的标题时,页面将导航至TitlesDetailPage.xaml显示影片的详细信息。
Project: ODataWinPhoneQuickstart File: MainViewModel.cs
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selector = (Selector)sender;
if (selector.SelectedIndex == -1)
{
return;
}
this.NavigationService.Navigate(
new Uri("/TitleDetailsPage.xaml?selectedIndex=" + selector.SelectedIndex, UriKind.Relative));
selector.SelectedIndex = -1;
}
在TitleDetailsPage.xaml中的OnNavigatedTo方法中,读取导航的上下文NavigationContext读取selectedIndex变量,获知显示集合的索引。
Project: ODataWinPhoneQuickstart File: TitleDetailsPage.cs
protected override void OnNavigatedTo(NavigationEventArgs e)
{
string indexAsString = this.NavigationContext.QueryString["selectedIndex"];
int index = int.Parse(indexAsString);
this.DataContext = this.currentTitle
= (Title)App.ViewModel.Titles[index];
}
- 管理应用的状态
当用户导航离开时,应用程序通常会进入休眠状态。在此状态下,应用程序被保留在内存中,如果用户返回到应用程序,它几乎立即就可以恢复。然而,应用程序也有可能会被操作系统被终止,为了使应用程序能够正确地重新加载数据,快速恢复用户离开时的状态,那么对象状态信息和属性必须存储在视图模型的状态词典中。Windows Phone的OData 客户端支持序列化DataServiceContext和DataServiceCollection集合,以便将此数据可以存储在状态词典中的DataServiceState类。下面的SaveState方法实现保存应用程序的状态,以便在应用程序重新激活时可以快速恢复。
Project: ODataWinPhoneQuickstart File: MainViewModel.cs
// Return a collection of key-value pairs to store in the application state.
public List<KeyValuePair<string, object>> SaveState()
{
if (App.ViewModel.IsDataLoaded)
{
List<KeyValuePair<string, object>> stateList
= new List<KeyValuePair<string, object>>();
// Create a new dictionary to store binding collections.
var collections = new Dictionary<string, object>();
// Add the current Titles binding collection.
collections["Titles"] = App.ViewModel.Titles;
// Store the current context and binding collections in the view model state.
stateList.Add(new KeyValuePair<string, object>(
"DataServiceState", DataServiceState.Serialize(_context, collections)));
stateList.Add(new KeyValuePair<string, object>("CurrentPage", CurrentPage));
stateList.Add(new KeyValuePair<string, object>("TotalCount", TotalCount));
return stateList;
}
else
{
return null;
}
}
应用程序被停用时,下面的方法处理应用程序停用(Deactivated)事件,调用视图模式的SaveState方法保存应用程序的运行状态。
Project: ODataWinPhoneQuickstart File: App.xaml.cs
// Code to execute when the application is deactivated (sent to background).
// This code will not execute when the application is closing.
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
if (App.ViewModel.IsDataLoaded)
{
// Store each key-value pair in the state dictionary.
foreach (KeyValuePair<string, object> item in App.ViewModel.SaveState())
{
PhoneApplicationService.Current.State[item.Key] = item.Value;
}
}
}
当应用程序重新激活时,视图模型将会从状态词典中检索出来并被重新加载。下面的应用程序激活事件重新加载应用程序之前的状态。
Project: ODataWinPhoneQuickstart File: App.xaml.cs
// Code to execute when the application is activated (brought to foreground).
// This code will not execute when the application is first launched.
private void Application_Activated(object sender, ActivatedEventArgs e)
{
object viewModelState;
if (PhoneApplicationService.Current.State.TryGetValue("ViewModelState", out viewModelState))
{
App.ViewModel.RestoreState(viewModelState as IDictionary<string, object>);
}
}
在下面的RestoreState方法中,DataServiceState对象和其他存储的数据是从全局状态词典中获取,用来初始化视图模型。
Project: ODataWinPhoneQuickstart File: MainViewModel.cs
// Restores the view model state from the supplied state dictionary.
public void RestoreState(IDictionary<string, object> dictionary)
{
// Create a dictionary to hold any stored binding collections.
object titles;
object stateAsString;
if (dictionary.TryGetValue("DataServiceState", out stateAsString))
{
// Rehydrate the DataServiceState object from the serialization.
DataServiceState state =
DataServiceState.Deserialize((string)stateAsString);
if (state.RootCollections.TryGetValue("Titles", out titles))
{
// Initialize the application with data from the DataServiceState.
App.ViewModel.LoadData((NetflixCatalog)state.Context,
(DataServiceCollection<Title>)titles);
// Restore other view model data.
_currentPage = (int)dictionary["CurrentPage"];
_totalCount = (int)dictionary["TotalCount"];
}
}
}
- 生成客户端数据类
Visual Studio中的添加服务引用对话框将引用添加到任何公开的OData 数据服务,此工具连接数据服务并生成继承于DataServiceContext的数据类和数据容器。以下是添加Netflix OData服务引用的对话框。
图 添加OData数据服务