5. 应用程序服务

在应用程序之间共享数据的另一种方法是使用应用程序服务。应用程序服务可以与调用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>

 在构建解决方案,部署提供程序和客户机应用程序后,就可以启动客户机应用程序来调用服务。还可以创建多个客户机应用程序,来调用相同的服务。

运行提供程序,注册后台任务后,可以运行客户端应用程序,获取图书,并添加新的图书,如下图所示。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值