在应用程序之间共享数据的另一种方法是使用应用程序服务。应用程序服务可以与调用Web服务相媲美,但对用户的系统而言,服务是本地的。多个应用程序可以访问相同的服务,这是在应用程序之间共享信息的方式。应用服务和Web服务之间的一个重要区别是,用户不需要使用这个特性进行交互,而可以在应用程序中完成。
样例应用程序AppServices使用服务缓存Book对象。调用服务,可以检索Book对象的列表,把新Book对象添加到服务中。
应用程序包含多个项目:
- 一个.NET标准库(BooksCacheModel)定义了这个应用程序的模型:Book类。为了便于传输数据,提供一个扩展方法,把Book对象转换为JSON,把JSON转换为Book对象。这个库在所有其他项目中使用。
- 第二个项目(BooksCacheService)是一个Windows运行库组件,定义了book服务本身。这种服务需要在后台运行,因此实现了一个后台任务。
- 后台任务需要注册到系统中。这个项目是一个Windows应用程序BooksCacheProvider。
- 调用应用程序服务的客户机应用程序是一个Windows应用程序BooksCacheClient。
下面看看这些部分。
1. 创建模型
移动库BooksCacheModel包含Book类、利用NuGet包Newtonsoft.Josn转换到JSON的转换器以及存储库。
Book类定义了Title和Publisher属性:
public class Book
{
public string Title { get; set; }
public string Publisher { get; set; }
}
BookdRepository类包含Book对象的内存缓存,允许用户通过AddBook方法添加book对象,使用Books属性返回所有缓存的书。为了查看一本书,不需要添加新书,初始化把一本书添加到列表中:
public class BooksRepository
{
private readonly List<Book> _books = new List<Book>
{
new Book
{
Title = "Professional C# 7 and .NET Core",
Publisher = "Wrox Press"
}
};
public IEnumerable<Book> Books => _books;
public void AddBook(Book book) => _books.Add(book);
public static BooksRepository Instance { get; } = new BooksRepository();
}
因为通过应用程序服务发送的数据需要序列化,所以扩展类BookExtensions定义了一些扩展方法,把Book对象和Book对象列表转换为JSON字符串,反之亦然。给应用程序服务传递一个字符串是很简单的。扩展方法利用了NuGet包Newtonsoft.Json中可用的类JsonConvert:
public static class BookExtensions
{
public static string ToJson(this Book book) => JsonConvert.SerializeObject(book);
public static string ToJson(this IEnumerable<Book> books) => JsonConvert.SerializeObject(books);
public static Book ToBook(this string book) => JsonConvert.DeserializeObject<Book>(book);
public static IEnumerable<Book> ToBooks(this string books) =>
JsonConvert.DeserializeObject<IEnumerable<Book>>(books);
}
2. 为应用程序服务连接创建后台任务
现在进入这个示例应用程序的核心:应用程序服务。需要把应用服务实现为WIndows Runtime组件库,通过实现接口IBackgroundTask把它实现为一个后台任务。Windows后台任务可以在后台运行,不需要用户交互。
有不同种类的后台任务可用。后台任务的启动可以基于定时器的间隔、Windows推送通知、位置信息、蓝牙设备连接或其他事情。
类BooksCacheTask是一个应用程序服务的后台任务。接口IBackgroundTask定义了需要实现的Run方法。在实现代码中,定义了请求处理程序,来接收应用程序服务的连接:
using BooksCacheModel;
using System;
using Windows.ApplicationModel.AppService;
using Windows.ApplicationModel.Background;
using Windows.Foundation.Collections;
namespace BooksCacheService
{
public sealed class BooksCacheTask : IBackgroundTask
{
private BackgroundTaskDeferral __taskDeferral;
public void Run(IBackgroundTaskInstance taskInstance)
{
__taskDeferral = taskInstance.GetDeferral();
taskInstance.Canceled += OnTaskCanceled;
var trigger = taskInstance.TriggerDetails as AppServiceTriggerDetails;
AppServiceConnection connection = trigger.AppServiceConnection;
connection.RequestReceived += OnRequestReceived;
}
private void OnTaskCanceled(IBackgroundTaskInstance sender,
BackgroundTaskCancellationReason reason)
{
__taskDeferral?.Complete();
}
}
}
在OnRequestReceived处理程序的实现代码中,服务可以读取请求,且需要提供回应。接收到的请求都包含在AppServiceReqeustReceivedEventArgs的Request.Message属性中。Message属性返回一个ValueSet对象。ValueSet是一个字典,其中包含键及其相应的值。这里的服务需要一个command键,其值是GET或POST。GET命令返回一个包含所有书籍的列表,而POST命令要求把额外的键book和一个JSON字符串作为Book对象表示的值。根据收到的消息,调用GetBooks或AddBook辅助方法。通过调用SendResponseAsync把从这些消息返回的结果返回给调用者:
private async void OnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
{
AppServiceDeferral deferral = args.GetDeferral();
try
{
ValueSet message = args.Request.Message;
ValueSet result = null;
switch (message["command"].ToString())
{
case "GET":
result = GetBooks();
break;
case "POST":
result = AddBook(message["book"].ToString());
break;
default:
break;
}
await args.Request.SendResponseAsync(result);
}
finally
{
deferral?.Complete();
}
}
GetBooks方法使用BooksRepository获得JSON格式的所有书籍,它创建了一个ValueSet,其键为result:
private ValueSet GetBooks()
{
var result = new ValueSet();
result.Add("result", BooksRepository.Instance.Books.ToJson());
return result;
}
AddBook方法使用存储库添加一本书,并返回一个ValueSet,其中的键是result,值是ok:
private ValueSet AddBook(string book)
{
BooksRepository.Instance.AddBook(book.ToBook());
return new ValueSet { ["result"] = "ok" };
}
3. 注册应用程序服务
现在需要通过操作系统注册应用程序服务。为此,创建一个正常UWP应用程序,它引用了BooksCacheService。在此应用程序中,必须在package.appxmanifest中定义一个声明(见下图)。在应用程序声明列表中添加一个应用程序服务并指定名字。需要设置到后台任务的入口点,包括名称空间和类名。
对于客户端应用程序,需要package.appxmanifest定义的应用程序名和包名。为了查看包名,可以查看PackageMinifestEditor的Package选项卡,也可以以编程方式调用Package.Current.Id.FamilyName。为了便于在启动应用程序时查看这个名字,把它写入属性PackageFamilyName,该属性绑定到用户界面的一个控件上:
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
PackageFamilyName = Package.Current.Id.FamilyName;
}
public string PackageFamilyName { get; set; }
}
当运行这个应用程序时,它会注册后台任务,并显示客户端应用程序需要的包名。
4. 调用应用程序服务
在客户端应用程序中,现在可以调用应用程序服务。客户端应用程序BooksCacheClient的主要部分用视图模型实现。Book属性绑定在UI中,显示从服务返回的所有书籍。这个集合用GetBooksAsync方法填充。GetBooksAsync使用GET命令创建一个ValueSet,使用SendMessageAsync辅助方法发送给应用程序服务。这个辅助方法返回一个JSON字符串,该字符串转换为一个Book集合,用于填充Books属性的ObservableCollection:
using BooksCacheModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Windows.ApplicationModel.AppService;
using Windows.Foundation.Collections;
using Windows.UI.Popups;
namespace BooksCacheClient.ViewModels
{
public class BooksViewModel
{
private const string BookServiceName = "com.CNinnovation.BooksCache";
private const string BookPackageName = "5879354e-a245-401c-86b1-15be4ee5c3c4_5xw1kvsej99w4";
public ObservableCollection<Book> Books { get; } = new ObservableCollection<Book>();
public async void GetBooksAsync()
{
var message = new ValueSet();
message.Add("command", "GET");
string json = await SendMessageAsync(message);
IEnumerable<Book> books = json.ToBooks();
foreach (var book in books)
{
Books.Add(book);
}
}
}
}
PostBookAsync方法创建了一个Book对象,序列化为JSON,通过VlaueSet把它发送给SendMessageAsync方法:
public string NewBookTitle { get; set; }
public string NewBookPublisher { get; set; }
public async void PostBookAsync()
{
var message = new ValueSet();
message.Add("command", "POST");
string json = new Book
{
Title = NewBookTitle,
Publisher = NewBookPublisher
}.ToJson();
message.Add("book", json);
string result = await SendMessageAsync(message);
}
与应用程序服务相关的客户代码包含在SendMessageAsync方法中。其中创建了一个AppServiceConnection。连接使用完后,通过using语句销毁,以关闭它。为了把连接映射到正确的服务上,需要提供AppServiceName和PackageFamilyName属性。设置这些属性后,通过调用方法OpenAsync来打开连接。只有成功地打开连接,才能在调用方法中发送请求和接收到返回的ValueSet。AppServiceConnection方法SendMessageAsync把请求发送给服务,返回一个AppServiceResponse对象。响应包含来自服务的结果,相应的处理如下:
private async Task<string> SendMessageAsync(ValueSet message)
{
using (var connection = new AppServiceConnection())
{
connection.AppServiceName = BookServiceName;
connection.PackageFamilyName = BookPackageName;
AppServiceConnectionStatus status = await connection.OpenAsync();
if (status == AppServiceConnectionStatus.Success)
{
AppServiceResponse response = await connection.SendMessageAsync(message);
if (response.Status == AppServiceResponseStatus.Success &&
response.Message.ContainsKey("result"))
{
string result = response.Message["result"].ToString();
return result;
}
else
{
await ShowServiceErrorAsync(response.Status);
}
}
else
{
await ShowConnectionErrorAsync(status);
}
return string.Empty;
}
}
private async Task ShowServiceErrorAsync(AppServiceResponseStatus status)
{
string error = null;
switch (status)
{
case AppServiceResponseStatus.Success:
error = "Service did not return the expected result";
break;
case AppServiceResponseStatus.Failure:
error = "Service failed to answer";
break;
case AppServiceResponseStatus.ResourceLimitsExceeded:
error = "Service exceeded the resource limits and was terminated";
break;
case AppServiceResponseStatus.Unknown:
error = "Unknow error";
break;
case AppServiceResponseStatus.RemoteSystemUnavailable:
error = "Remote System Unavailable";
break;
case AppServiceResponseStatus.MessageSizeTooLarge:
error = "Message Size Too Large";
break;
default:
break;
}
await new MessageDialog(error).ShowAsync();
}
private async Task ShowConnectionErrorAsync(AppServiceConnectionStatus status)
{
string error = null;
switch (status)
{
case AppServiceConnectionStatus.AppNotInstalled:
error = "The Book Cache service is not instanlled.Deploy the BooksCacheProvider";
break;
case AppServiceConnectionStatus.AppUnavailable:
error = "The Book Cache Service is not available. Maybe an update is in progress, " +
"or hte app location is not available";
break;
case AppServiceConnectionStatus.AppServiceUnavailable:
error = "The Book Cache Service is not available";
break;
case AppServiceConnectionStatus.Unknown:
error = "Unknow error";
break;
case AppServiceConnectionStatus.RemoteSystemUnavailable:
error = "Remote System Unavailable";
break;
case AppServiceConnectionStatus.RemoteSystemNotSupportedByApp:
error = "Remote System Not Supported By App";
break;
case AppServiceConnectionStatus.NotAuthorized:
error = "Not Authorized";
break;
default:
break;
}
await new MessageDialog(error).ShowAsync();
}
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel Spacing="10">
<Button Content="GetBooks" Tapped="{x:Bind ViewModel.GetBooksAsync}"/>
<TextBox Header="Title" Text="{x:Bind ViewModel.NewBookTitle,Mode=TwoWay}"/>
<TextBox Header="Publisher" Text="{x:Bind ViewModel.NewBookPublisher,Mode=TwoWay}"/>
<Button Content="Add Book" Tapped="{x:Bind ViewModel.PostBookAsync}"/>
</StackPanel>
<ListView ItemsSource="{x:Bind ViewModel.Books}" Grid.Column="1">
<ListView.ItemTemplate>
<DataTemplate x:DataType="bookscachemodel:Book">
<StackPanel>
<TextBlock Text="{x:Bind Title}"/>
<TextBlock Text="{x:Bind Publisher}"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
在构建解决方案,部署提供程序和客户机应用程序后,就可以启动客户机应用程序来调用服务。还可以创建多个客户机应用程序,来调用相同的服务。
运行提供程序,注册后台任务后,可以运行客户端应用程序,获取图书,并添加新的图书,如下图所示。