文件与数据

1. 文件与目录

(1) 文件/目录操作的相关类型

与文件/目录操作有关的类型主要分布在Windows.Storage命名空间及该命名空间下的子命名空间中。下面列出了与文件/目录操作有关的几个重要类型。

  • StorageFile 表示一个文件类型,通过该类,可以对某个文件进行一些常用操作,例如重命名、删除、获取基本属性等
  • StorageFolder 表示一个目录实例,可以对某个目录进行重命名、复制、删除等操作
  • KnownFolders 一个静态类型,公开一系列静态属性,通过这些属性可以直接获取特殊目录的引用,如图片库、音乐库、视频库等
  • FileIO 公开一系列静态方法,以方便对文件进行读写操作,简化了对文件进行操作的步骤,该类针对StorageFile类实例而设计
  • PathIO 同上,公开一系列便捷的方法来读写文件,与FileIO不同的是,PathIO是面向路径的,即在读写文件时不需要创建StorageFile实例,只需要通过字符串指定文件的路径就可以了

(2) 读写本地文件

当应用程序安装完成后,操作系统会为每个应用分配独立的文件夹,每个应用程序只能访问自己的文件夹。这个文件夹也叫本地目录,可以通过ApplicationData类的LocalFolder属性获得与本地目录相关的StorageFolder实例,随后应用程序就可以使用该StorageFolder对象来读写文件了。
下面示例将演示如何读写本地文件。示例首先将一个随机生成的字节序列写入本地文件,然后再从已保存的文件中读出这些字节序列。

  1. MainPage页面的布局如下XAML所示:
        <StackPanel Spacing="10">
            <TextBlock Text="写入文件" FontSize="36"/>
            <Button x:Name="writeBtn" Content="将字节序列写入文件" Tapped="writeBtn_Tapped"/>
            <TextBlock x:Name="tbBytes" TextWrapping="Wrap" FontSize="24"/>
            <Line X1="0" Y1="0" X2="100" Y2="0" StrokeThickness="6" Stretch="Fill" Stroke="LightGray"/>
            <TextBlock Text="从文件读入" FontSize="36"/>
            <Button x:Name="readBt" Content="读取字节序列" Tapped="readBt_Tapped"/>
            <TextBlock x:Name="tbReadBytes" FontSize="24" TextWrapping="Wrap"/>
        </StackPanel>
  1. 分别处理两个Button控件的Tapped事件。
        private async void writeBtn_Tapped(object sender, TappedRoutedEventArgs e)
        {
            writeBtn.IsEnabled = false;
            //产生字节数组
            Random rand = new Random();
            byte[] data = new byte[8];
            rand.NextBytes(data);
            //从字节数组产生缓冲区对象
            IBuffer buffer = data.AsBuffer();
            //在本例目录中创建新文件
            StorageFile newFile = await local.CreateFileAsync("my.data", CreationCollisionOption.ReplaceExisting);
            //打开文件流
            using (IRandomAccessStream outputStream = await newFile.OpenAsync(FileAccessMode.ReadWrite))
            {
                //将缓冲区的内容写入流
                await outputStream.WriteAsync(buffer);
            }
            //显示已写入的字节序列
            tbBytes.Text = $"已向文件写入:{BitConverter.ToString(data)}";
            writeBtn.IsEnabled = true;
        }

        private async void readBt_Tapped(object sender, TappedRoutedEventArgs e)
        {
            readBt.IsEnabled = false;
            //获取文件
            var file = await local.GetFileAsync("my.data");
            if (file != null)
            {
                //用于存放读到的数据的缓冲区
                IBuffer buffer = null;
                //打开流
                using (var inputStream = await file.OpenReadAsync())
                {
                    //实例化缓冲区对象
                    buffer = WindowsRuntimeBuffer.Create((int)inputStream.Size);
                    //从流中读入数据,存放到缓冲区中
                    await inputStream.ReadAsync(buffer,buffer.Capacity,InputStreamOptions.None);
                }
                //显示读到的字节序列
                tbReadBytes.Text = $"读到的字节序列:{BitConverter.ToString(buffer.ToArray())}";
            }
            readBt.IsEnabled = true;
        }

在Running App中读写数据通常用到缓冲区对象,即IBuffer接口及实现了该接口的Buffer类。在声明变量时,一般使用IBuffer来作为类型标识。

            IBuffer buffer = data.AsBuffer();

有过.NET开发经历的开发者会明白,在托管代码中处理字节序列时,用得最多的是通过字节数组(byte[])作为临时缓冲区,而由于UWP中要用到IBuffer缓冲区对象,这就涉及到两种类型之间的转换。为了实现它们之间的互相转化,API库为它们定义了相关的扩展方法:

  • 调用byte[]实例的AsBuffer方法,可以返回IBuffer实例。
  • 调用IBuffer实例的ToArray方法,可以得到byte数组。

StorageFile类有两种方法打开文件流:OpenReadAsync方法打开的流只能用于读取,不能进行写操作,在读取文件时使用该方法较合适;OpenAsync方法既可以打开只读的流,也可以打开可读可写的流,这决定于传递给方法参数的FileAccessMode枚举的值。

(3) FileIO与PathIO

FileIO类与PathIO都公开了一系列方法,以便简捷地读写文件。如下所示,可以将这些方法按照读写的方向分为两组。
写入:

  • AppendLinesAsync 在文件原有内容的基础上追加一行或多行文本,每次写入的文本的结尾自动加上换行符。

  • AppendTextAsync 向文件的现有内容追加文本。

  • WriteBufferAsync 将缓冲区中的数据写入文件。

  • WriteBytesAsync 将字节数组写入文件。

  • WriteTextAsync 向文件写入文本,每次写入的内容会替换文件的现有内容。

  • WriteLinesAsync 向文件写入一行或多行文本,写入的文本末尾会自动加上换行符,文件的现有内容会被替换。
    读取:

  • ReadTextAsync 获取文件中所有文本内容。

  • ReadLinesAsync 从文件中读取一行或多行文本。

  • ReadBufferAsync 将从文件读取的数据存入到缓冲区对象中。

接下来用一个示例来演示FileIO和PathIO两个类的使用方法。
页面的布局XAML如下:

        <StackPanel Spacing="10">
            <TextBox x:Name="txtInput" Header="请输入内容:" TextWrapping="Wrap"/>
            <Button Tag="write" Content="写入文件" Tapped="Button_Tapped"/>
            <Button Tag="read" Content="读取文件" Tapped="Button_Tapped"/>
            <TextBox x:Name="txtOutput" IsReadOnly="True" Header="读出的内容:" TextWrapping="Wrap"/>
        </StackPanel>

在本示例中,将使用FileIO类将文本写进本地文件中,随后使用PathIO类从本地文件中读出文本内容。Button控件的Tapped事件处理程序如下:

        private async void Button_Tapped(object sender, TappedRoutedEventArgs e)
        {
            Button btn = sender as Button;
            btn.IsEnabled = false;
            if ((sender as Button).Tag.ToString() is "write")
            {
                if (txtInput.Text.Length < 1)
                {
                    return;
                }
                //获取本地目录的引用
                var local = ApplicationData.Current.LocalFolder;
                //创建新文件
                var newFile = await local.CreateFileAsync("data.txt",CreationCollisionOption.ReplaceExisting);
                //写入文件
                await FileIO.WriteTextAsync(newFile,txtInput.Text);
            }
            else
            {
                try
                {
                    //直接读取文件内容
                    txtOutput.Text = await PathIO.ReadTextAsync("ms-appdata:///local/data.txt");
                }
                catch (Exception ex)
                {
                    txtOutput.Text = ex.Message;
                }
            }
            btn.IsEnabled = true;
        }

从上面代码中可以看出,使用FileIO类和PathIO类来读写文件非常简便。要注意的是,在使用PathIO类读取文件时使用的是文件路径,本地文件夹的路径为:

ms-appdata:///local/

示例中的data.txt文件是放在本地目录下的,因此文件的路径为:

ms-appdata:///local/data.txt

还要注意:PahtIO类只能对已经存在的文件进行操作,如果文件不存在,则会发生异常。

(4) DataWriter与DataReader

这两个类都是以流对象作为载体而进行的读写操作。通过这两个类的封装,使流操作变得更加方便,功能上类似于FileIO类和PathIO类。但是,FileIO类和PathIO类所操作的目标是文件,而DataWriter类和DataReader类是面向流的,既可以用来读写文件,也可以用于读写内存数据,在网络通信中还可以读写网络数据。可见,DataWriter与DataReader适用的范围更广。
在写入数据时,可以使用DataWriter类公开的如WriteByte、WriteDateTime、WriteInt32、WriteString等方法;同理,在读取数据时,可以调用DataReader类的ReadByte、ReadDateTine、ReadString等方法。这些方法的优点在于封装了基础数据类型的处理,可以很方便地进行读写。不过要注意的是,读出数据的顺序应当与数据写入时的顺序相同。例如先写入一个byte类型的值,后写入一个int类型的值,在读出数据的时候,应该先读出byte类型的值,在读出int类型的值。
下面示例先用DataWriter类向本地文件写入数据,然后使用DataReader类将数据读出来。应用程序主页面的XAML代码如下:

        <StackPanel Spacing="10">
            <RichTextBlock FontSize="20" TextWrapping="Wrap">
                <Paragraph>
                    写入的内容及顺序如下:
                </Paragraph>
                <Paragraph>
                    <Span>1、bool值:true</Span>
                    <LineBreak/>
                    <Span>2、DateTime类型值:2020-12-20</Span>
                    <LineBreak/>
                    <Span>3、字符串:测试文本</Span>
                </Paragraph>
            </RichTextBlock>
            <Button Tag="write" Content="写入文件" Tapped="Button_Tapped"/>
            <Line X1="0" X2="20" Stretch="Fill" StrokeThickness="5" Stroke="LightYellow"/>
            <Button Tag="read" Content="读取内容" Tapped="Button_Tapped"/>
            <TextBlock x:Name="tbResult" FontSize="20" TextWrapping="Wrap"/>
        </StackPanel>

示例要向本地文件写入三段数据:第一段为bool类型的值ture;第二段数据为当前日期值;第三段数据是字符串"测试文本"。
Button控件的事件处理程序如下:

        private async void Button_Tapped(object sender, TappedRoutedEventArgs e)
        {
            if ((sender as Button).Tag.ToString() is "write")
            {
                //创建新文件
                var file = await localFolder.CreateFileAsync("demo.dat",CreationCollisionOption.ReplaceExisting);
                //打开文件流
                using (var outputStream = await file.OpenAsync(FileAccessMode.ReadWrite))
                {
                    //实例化DataWriter
                    DataWriter dw = new DataWriter(outputStream);
                    //设置默认编码格式
                    dw.UnicodeEncoding = UnicodeEncoding.Utf8;
                    //写入bool值
                    dw.WriteBoolean(true);
                    //写入日期时间值
                    DateTime dt = DateTime.Now;
                    dw.WriteDateTime(dt);
                    //写入字符串
                    string str = "测试文本";
                    //计算字符串的长度
                    uint len = dw.MeasureString(str);
                    //先写入字符串的长度
                    dw.WriteInt32((int)len);
                    //再写入字符串
                    dw.WriteString(str);
                    //以下方式必须调用
                    await dw.StoreAsync();
                    //解除DataWriter与流的关联
                    //dw.DetachStream();
                    dw.Dispose();
                }
                await new MessageDialog("保存成功。").ShowAsync();
            }
            else
            {
            //To do.
            }
        }

在调用CreateFileAsync方法创建新文件时,指定了CreationCollisionOption.ReplaceExisting枚举值,如果要创建的文件已经存在,则用新的文件替换原来的文件。
由于字符串的长度是可变的,所以在写入字符串的时候,要先确定待写入字符串的长度,先向流写入长度,再写入字符串。在读取的时候,要先读出字符串的长度,再读出完整的字符串。在所有数据都写完成后,一定要调用StoreAsync方法,该方法被调用后才会把数据写入到关联的流中。
上面代码在使用完DataWriter对象后调用了它的DetachStream方法,该方法的作用是使当前的DataWriter实例与关联的流分离,这样,在DataWriter对象被释放时,不会同时把关联的流对象也释放掉。但在本示例中是不必要的,因为在本例中,文件流在写入完成后就可以释放了,并不需要持久保存流的实例。
下面代码从刚才保存的本地文件中读出数据:


        private async void Button_Tapped(object sender, TappedRoutedEventArgs e)
        {
            if ((sender as Button).Tag.ToString() is "write")
            {
            //To do.
            }
            else
            {
                var file = await localFolder.GetFileAsync("demo.dat");
                if (file != null)
                {
                    StringBuilder builder = new StringBuilder();
                    builder.AppendLine("读到的内容:");
                    //打开文件流
                    using (var inputStream = await file.OpenReadAsync())
                    {
                        //实例化DataReader
                        DataReader dr = new DataReader(inputStream);
                        //读出时的编码格式要与写入时使用的编码格式相同
                        dr.UnicodeEncoding = UnicodeEncoding.Utf8;
                        //从流中加载所有数据
                        await dr.LoadAsync((uint)inputStream.Size);
                        //读出的顺序与写入的顺序相同
                        //读取bool值
                        bool b = dr.ReadBoolean();
                        builder.AppendLine(b.ToString());
                        //读取日期时间值
                        DateTimeOffset dt = dr.ReadDateTime();
                        builder.AppendLine(dt.ToString("yyyy-M-d HH:mm:ss"));
                        //读取字符串
                        //读取长度
                        uint len = (uint)dr.ReadInt32();
                        if (len > 0)
                        {
                            builder.Append(dr.ReadString(len));
                        }
                        dr.Dispose();
                    }
                    tbResult.Text = builder.ToString();
                }
            }
        }

2. 应用设置

应用程序设置通常用来读写一些简单且比较小型的数据。根据目前官方所提供的开发文档的描述,应用设置的值可支持以下类型的数据。

  • UInt8、Int6、UInt16、Int32、UInt32、Int64、UInt64、Single、Double
  • Boolean
  • Char16、String
  • DateTime、TiemSpan
  • GUID、Point、Size、Rect
  • ApplicationDataCompositeValue
    其中,ApplicationDataCompositeValue表示复合设置值,例如一个设置项的值是由多个值组成的,则可以使用ApplicationDataCompositeValue将这些值合并为一个值,再存入应用设置项中。该类实现了IDictionary<string,object>接口,表明ApplicationDataCompositeValue中的内容是以字典数据形式存在的。
    从上面所列出的内容可以看到,应用设置都是针对属于基础类型的数据值,如字符串、双进度数值、布尔值等。因为应用设置是存储在注册表中的,一般不应该存入大型数据。如果开发者要存储大型的数据,应当考虑使用文件或Sqlite(轻量级数据库)来存取。
    当应用程序安装后,和本地文件夹一样,操作系统会为每个应用程序分配一个独立的注册表根容器,用于存储应用设置。也就是说,应用设置的根容器在默认情况下已经由系统创建,所以ApplicationData类实例的LocalSettings属性返回的ApplicationDataContainer对象所包装的正是系统为当前应用程序分配的根容器。在根容器中可以直接存入应用设置,也可以在根容器下创建子容器,再在子容器中存入应用设置。
    了解Windows注册表的开发者应该知道,注册表的层次结构类似于文件目录结构,也是树形结构。因此在应用设置的存储结构中,容器类似于文件夹,而单个设置项就相当于一个文件。
    要在根容器下创建子容器,可以调用ApplicationDataContainer类的CreateContainer方法,注意容器的名字在父容器中必须是唯一的。当不需要容器时,可以通过DeleteContainer方法删除容器。ApplicationDataContainer.Containers属性将包含当前容器下的子容器列表。
    下面将通过一个示例来演示如何读写应用设置。本示例模拟一个简单的设置页面,当用户在控件上输入内容或 做出选择后将相关的数据保存到应用设置容器中。
    MainPage页面中的XAML代码如下:
        <StackPanel Spacing="10">
            <ComboBox x:Name="cmb" Header="你希望多少天提示一次?" SelectionChanged="cmb_SelectionChanged">
                <ComboBoxItem>5天</ComboBoxItem>
                <ComboBoxItem>8天</ComboBoxItem>
                <ComboBoxItem>15天</ComboBoxItem>
                <ComboBoxItem>1个月</ComboBoxItem>
            </ComboBox>
            <TextBox x:Name="txt1" Header="请设置一个别名:" LostFocus="txt1_LostFocus"/>
            <TextBox x:Name="txt2" Header="请指定一个关键词:" LostFocus="txt2_LostFocus"/>
            <ToggleSwitch x:Name="tgswitch" Header="是否开启自动获取?" OnContent="" OffContent="" Toggled="tgswitch_Toggled"/>
        </StackPanel>

后台代码中,相关的事件处理程序如下:

        const string SETTING_UPDATE_FREQUENCY = nameof(SETTING_UPDATE_FREQUENCY);
        const string SETTING_TEXT_A = nameof(SETTING_TEXT_A);
        const string SETTING_TEXT_B = nameof(SETTING_TEXT_B);
        const string SETTING_TOGGLE_VALUE = nameof(SETTING_TOGGLE_VALUE);
        ApplicationDataContainer root;
        public AppSettingsPage()
        {
            this.InitializeComponent();
            //将下拉列表框中选中项的索引存入应用设置中
            //获取根容器的引用
            root = ApplicationData.Current.LocalSettings;
        }
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
            //从应用设置中读取信息
            object cmIndex, text1, text2, toggleVal;
            if (root.Values.TryGetValue(SETTING_UPDATE_FREQUENCY,out cmIndex))
            {
                cmb.SelectedIndex = (int)cmIndex; 
            }
            if (root.Values.TryGetValue(SETTING_TEXT_A,out text1))
            {
                txt1.Text = text1 as string;
            }
            if (root.Values.TryGetValue(SETTING_TEXT_B,out text2))
            {
                txt2.Text = text2 as string;
            }
            if (root.Values.TryGetValue(SETTING_TOGGLE_VALUE,out toggleVal))
            {
                tgswitch.IsOn = (bool)toggleVal;
            }
        }
        private void cmb_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            int index = cmb.SelectedIndex;
            if (index > -1)
            {
                //向根容器写入新设置项
                root.Values[SETTING_UPDATE_FREQUENCY] = index;
            }
        }

        private void txt1_LostFocus(object sender, RoutedEventArgs e)
        {
            //将文本框中输入的文本存入设置中
            if (!string.IsNullOrWhiteSpace(txt1.Text))
            {
                root.Values[SETTING_TEXT_A] = txt1.Text;
            }
        }

        private void txt2_LostFocus(object sender, RoutedEventArgs e)
        {
            if (!string.IsNullOrWhiteSpace(txt2.Text))
            {
                root.Values[SETTING_TEXT_B] = txt2.Text;
            }
        }

        private void tgswitch_Toggled(object sender, RoutedEventArgs e)
        {
            //将ToggleSwitch控件的当前状态存入应用设置
            root.Values[SETTING_TOGGLE_VALUE] = tgswitch.IsOn;
        }

写入应用设置比较简单,先访问ApplicationData.Current静态属性得到适合当前应用程序的ApplicationData对象,接着访问该ApplicationData对象的LocalSettings属性,就可以获取到与设置根容器关联的ApplicationDataContainer对象,其中的Values属性就是当前容器中设置项的集合。
设置项是字典数据结构,即每个设置项都具有一个用于标识的唯一键(Key),并且有一个对应值(Value)。

3. 访问可移动存储

除了自身内置的存储器外,为了扩展存储空间,在PC/平板设备上可以使用如U盘、移动硬盘等设备,在手机设备上可以使用SD卡。
访问KnownFolders类的RemovableDevices静态属性就可以返回可移动存储所在的逻辑目录。该目录是相对的,它不是一个真实存在的目录。通常,它的相对路径为0\RemovableStorageDevices,它作为当前设备上所有可移动存储器的分组目录。即设备上所有真实的可移动存储器的根目录都位于该逻辑目录下。
例如,一台计算机上连接了两个U盘,那么RemovableDevices属性返回的逻辑目录下面就会有两个子目录,分别代表这两个U盘的根目录。
下面通过示例来说明一下访问可移动存储的方法。
页面布局的XAML如下:

        <StackPanel Spacing="10" Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
            <ComboBox x:Name="cmbRemovable" Header="请选择一个可移动存储:" DisplayMemberPath="DisplayName"/>
            <Rectangle Height="3" Fill="White"/>
            <TextBox x:Name="txtInput" Height="100" TextWrapping="Wrap" Header="请输入内容:"/>
            <Button Content="保存到可移动存储中" Tapped="Button_Tapped"/>
        </StackPanel>

名为cmRemovable的下拉列表控件用于由用户选择一个可移动存储目录。随后把TextBox中输入的文本以文本文件的形式保存到被选择的可移动存储中。Button控件的Tapped事件处理程序如下:

        private async void Button_Tapped(object sender, TappedRoutedEventArgs e)
        {
            //从下拉列表中取出被选项
            var selFolder = cmbRemovable.SelectedItem as StorageFolder;
            if (selFolder is null || txtInput.Text.Length == 0)
            {
                return;
            }
            //创建新文件
            var file = await selFolder.CreateFileAsync("abc.txt", CreationCollisionOption.ReplaceExisting);
            //向文件写入内容
            await FileIO.WriteTextAsync(file,txtInput.Text);
        }

在页面重写的OnNavigateTo方法中,获取当前设备上的所有可移动存储目录,然后作为ComboBox控件的数据源,以供用户选择。

        protected async override void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
            var rmFolder = KnownFolders.RemovableDevices;
            if (rmFolder != null)
            {
                cmbRemovable.ItemsSource = await rmFolder.GetFoldersAsync();
            }
        }

KnownFolders.RemovableDevices属性返回的目录并非可移动存储器的根目录,而是一个包装扩展存储器集合的虚拟容器,是一个逻辑目录。因此需要调用GetFoldersAsync方法获取其子目录,这些子目录列表将绑定到ComboBox控件上。
要让应用程序具备访问可移动存储的权限,需要打开清单文件,在Capabilities元素下加入以下XML:

  <Capabilities>
    <uap:Capability Name="removableStorage"/>
  </Capabilities>

4. StorageApplicationPermissions类

StorageApplicationPermissions类(位于Windows.Storage.AccessCache命名空间下)公开两个列表——MostRecentlyUsedList与FutureAccessList。这两个列表在使用方法上是差不多的(均以IStorageItemAccessList接口作为规范),只是其含义不同。MostRecentlyUsedList列表用于存放用户最近使用过的目录或文件;而FutureAccessList列表中所存放的项则表示应用程序将来可以轻松访问的文件或目录。
处于安全考虑,默认情况下应用程序可以直接访问的路径很少,通常只允许访问与应用程序关联的本地目录。除了特殊目录(如图片库、音乐库等),应用程序如果要访问其他路径则需要使用FolderPicker等选择器来进行选择。对于经常使用的路径,如果每次访问都要打开选择器界面来让用户做出选择,随后将用户所选择的路径加入到StorageApplicationPermissions类的列表中,应用程序会记住这些列表,以后应用程序就可以直接访问列表中的路径了。
下面用一个示例来演示StorageApplicationPermissions类的使用方法。示例主要实现两部分功能:首先通过目录选择器让用户选择一个目录,并将该目录保存到FutureAccessList列表中,以便于将来可以直接访问;随后应用程序将直接访问FutureAccessList列表中保存的目录,获取该目录中的文件列表并显示到界面上。
页面布局的XAML如下:

        <Pivot>
            <PivotItem Header="设置">
                <StackPanel Spacing="10">
                    <Button x:Name="selectFolder_btn" Content="选择要使用的目录" Tapped="selectFolder_btn_Tapped"/>
                    <TextBlock x:Name="tbMsg" FontSize="20" TextWrapping="Wrap"/>
                </StackPanel>
            </PivotItem>
            <PivotItem Header="查看文件">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="auto"/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <Button x:Name="showFiles_btn" Content="列出子文件" Tapped="showFiles_btn_Tapped"/>
                    <ListView x:Name="lvFiles" Grid.Row="1" DisplayMemberPath="DisplayName"/>
                </Grid>
            </PivotItem>
        </Pivot>

Button的Tapped事件处理程序如下:

        //选择要使用的目录
        private async void selectFolder_btn_Tapped(object sender, TappedRoutedEventArgs e)
        {
            FolderPicker picker = new FolderPicker();
            picker.FileTypeFilter.Add("*");
            var folder = await picker.PickSingleFolderAsync();
            if (folder != null)
            {
                StorageApplicationPermissions.FutureAccessList.Clear();
                //向访问列表添加项
                StorageApplicationPermissions.FutureAccessList.Add(folder);
                tbMsg.Text = $"已添加目录 {folder.Path} 到访问列表中";
            }
        }
        //获取目录子文件列表
        private async void showFiles_btn_Tapped(object sender, TappedRoutedEventArgs e)
        {
            if (StorageApplicationPermissions.FutureAccessList.Entries.Count == 0)
            {
                return;
            }
            showFiles_btn.IsEnabled = false;
            //获取访问列表项的标识
            string token = StorageApplicationPermissions.FutureAccessList.Entries[0].Token;
            //获得访问列表中存储的目录引用
            StorageFolder fd = await StorageApplicationPermissions.FutureAccessList.GetFolderAsync(token);
            if (fd != null)
            {
                //列出目录中的子文件
                IReadOnlyList<StorageFile> files = await fd.GetFilesAsync();
                //在ListView控件中显示文件列表
                lvFiles.Items.Clear();
                foreach (StorageFile file in files)
                {
                    lvFiles.Items.Add(file);
                }
            }
            showFiles_btn.IsEnabled = true;
        }

FutureAccessList.Add方法调用后会返回一个字符串,该字符串用来标识列表中的项,因此是唯一的。如果要得到列表中所有项的标识,可以访问StorageApplicationPermissions.FutureAccessList.Entries属性,每个项实体都由一个AccessListEntry结构的实例表示。其中,AccessListEntry结构的Token字段就是存储在访问列表中的项的标识字符串(FutureAccessList.Add方法所返回的字符串)。
当目录被添加到访问列表后,应用程序会自动记录,以后应用程序就可以直接访问这个目录了,而不需要让用户再做一次选择。

5. XML与JSON数据处理

XML与JSON格式的数据具备较高的标准性,不仅可以用于本地数据处理,还能在客户端与服务器之间、不同平台之间传输数据。许多Web服务的调用过程正是通过XML或JSON格式来传输数据的。

(1) 读写XML

Windows.Data.Xml.Dom命名空间下提供了若干类型,可以帮助开发者对XML文档进行读写、写入、筛选等操作。其中,XmlDocument类是比较核心的类型,它表示一个XML文档的实例,使用该类可以通过代码来构建XML文档。
在读取XML数据时,可以调用LoadFromUriAsync方法(从URI加载)或者LoadFromFileAsync方法(从文件加载)两个静态成员,直接返回XmlDocument对象实例,随后,就可以对XML文档进行读取和编辑操作。如果要创建全新的XML文档,可以在实例化XmlDocument类后,调用下面方法为文档创建各种类型的节点:

  • CreateComment:创建XML注释节点,返回类型为XmlComment。
  • CreateElement: 创建新的XML元素,调用后返回XmlElement实例。
  • CreateTextNode: 创建纯文本节点。
  • CreateAttribute: 创建XMLElement特性的name-value对,返回XmlAttribute实例,特性一般不单独使用,而是作为XmlElement的子节点。
  • CreateCDataSection: 创建CData节点,可以存储不被XML解析器分析的文本。

不管是XmlDocument类,还是XmlComment、XmlElement等类型的节点,它们都实现了同一个接口——IXmlNode,所以只需要调用A节点的AppendChild方法,并将B节点作为参数,就可以把B节点添加到A节点的子节点集合中。要从A节点的子节点集合中删除B节点,调用RemovableChild方法即可。
下面示例首先创建一个XML文档,并保存到本地文件夹中;然后从本地文件中将XML文档读出,并在应用程序界面上显示。
页面布局的XAML代码如下:

            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="auto"/>
                    <RowDefinition/>
                </Grid.RowDefinitions>
                <StackPanel Spacing="10" HorizontalAlignment="Center">
                    <Button Tag="write" Content="创建XML文件" Tapped="Button_Tapped"/>
                    <Button Tag="read" Content="读取XML文件" Tapped="Button_Tapped"/>
                </StackPanel>
                <TextBlock x:Name="tbXml" Grid.Row="1" FontSize="20" TextWrapping="Wrap" VerticalAlignment="Center" HorizontalAlignment="Center"/>
            </Grid>

TextBlock控件用于显示从文件中读取到的XML内容。
下面代码实现创建并保存XML文档:

                //创建XmlDocument实例
                XmlDocument xdoc = new XmlDocument();
                //创建根节点
                XmlElement root = xdoc.CreateElement("books");
                //将根节点追加到XML文档中
                xdoc.AppendChild(root);
                //创建子元素
                XmlElement book = xdoc.CreateElement("book");
                //设置特性值
                book.SetAttribute("ISBM", "100658425");
                //创建文本节点'
                XmlText text = xdoc.CreateTextNode("示例图书 ");
                //将文本节点添加到book节点中
                book.AppendChild(text);
                //将book节点添加到根节点上
                root.AppendChild(book);
                //创建新文件
                var xmlFile = await local.CreateFileAsync("test.xml",CreationCollisionOption.ReplaceExisting);
                //将新建的XML文档保存到文件
                await xdoc.SaveToFileAsync(xmlFile);

通过代码构建XML文档时,建议先规划一下XML文档的大致结构,尤其是对于层次较为复杂的XML文档,在编写代码时极容易出错,因此要特别谨慎。在XML文档构建完成后,直接调用SaveToFileAsync方法就可以将XML文档保存到文件。
下面代码将从保存的XML文档中读出XML内容,并在TextBlock控件上显示。

                //从本地文件中加载XML
                var xmlFile = await local.GetFileAsync("test.xml");
                XmlDocument xdoc = await XmlDocument.LoadFromFileAsync(xmlFile);
                //显示XML文档
                tbXml.Text = xdoc.GetXml();

(2) 操作JSON数据

JSON即JavaScript对象表示符合,可以用简单的文本来描述JavaScript语言所能识别的对象。由于JSON具备很强的通用性,可以在多个平台之间共享数据,因此被广泛用于网络数据传输中。Windows.Data.Json命名空间下包含一些让开发者能够轻松访问和处理JSON格式数据的类型。

  • IJsonValue接口:对封装JSON值的类型界定规范。其中,Stringfy方法可以返回某个JSON值的字符串表示形式。
  • JsonValue类:表示一个简单的JSON值,如字符串、数值等。通常通过该类所公开的静态方法来创建值。例如调用CreateNumberValue方法可以创建一个表示数值的JsonValue实例。
  • JsonArray类:表示JSON中的数组。由于该类除了实现IJsonValue接口外,还实现了IList接口,因此可以调用Add方法向JsonArray实例添加元素。
  • JsonObject类:表示一个单独的JSON对象,由于该类实现了IDictionary<TKey,TValue>接口,可以像操作字典数据一样使用它。

下面将通过一个示例演示如何读写JSON数据。示例根据应用界面上输入的内容,生成一个JSON对象,并以字符串的形式显示在用户界面上。
页面布局的XAML如下:

            <StackPanel Spacing="10">
                <TextBox x:Name="txtName" Header="姓名:"/>
                <TextBox x:Name="txtNo" Header="ID:"/>
                <TextBox x:Name="txtAge" Header="年龄:">
                    <TextBox.InputScope>
                        <InputScope>
                            <InputScope.Names>
                                <InputScopeName NameValue="Number"/>
                            </InputScope.Names>
                        </InputScope>
                    </TextBox.InputScope>
                </TextBox>
                <TextBox x:Name="txtCity" Header="城市:"/>
                <Button Content="生成并显示JSON数据" Tapped="Button_Tapped_1"/>
                <Line X1="0" X2="20" StrokeThickness="6" Stroke="White" Stretch="Fill"/>
                <TextBlock x:Name="tbJson" TextWrapping="Wrap" FontSize="20"/>
            </StackPanel>

Button事件处理程序如下,生成并呈现JSON对象。

        private void Button_Tapped_1(object sender, TappedRoutedEventArgs e)
        {
            //实例化JsonObject对象
            JsonObject obj = new JsonObject();
            //设置各字段的值
            obj["name"] = JsonValue.CreateStringValue(txtName.Text);
            obj["no"] = JsonValue.CreateStringValue(txtNo.Text);
            obj["city"] = JsonValue.CreateStringValue(txtCity.Text);
            if (double.TryParse(txtAge.Text,out var age))
            {
                obj["age"] = JsonValue.CreateNumberValue(age);
            }
            else
            {
                obj["age"] = JsonValue.CreateNumberValue(0);
            }
            //显示JSON对象的字符串表示形式
            string jstr = obj.Stringify();
            tbJson.Text = jstr;

            //如若从本地文件中读取json数据,则可以如下实现:
            //string json = null;//从文件中读取到的json字符串表示形式
            //if (JsonObject.TryParse(json,out JsonObject josnObj))
            //{
            //    // To do.
            //}
        }

由于JsonObject对象属于字典结构,除了可以调用Add方法来添加字段外,还可以像示例中一样,直接通过键索引来赋值:

obj[key] = value;

6. 数据共享

数据共享允许不同应用程序之间可以传递数据,这些数据包括文本、图像、文件等内容。应用程序可以通过以下两种方案实现数据共享:

  1. 剪贴板,即平时所说的"赋值"与"粘贴"操作。剪贴板作为一个公共桥梁,共享数据的发送方先将数据内容放到剪贴板上,其他应用程序可以通过访问剪贴板来获取数据。
  2. 直接传递数据。应用程序既可以充当数据的发送方,将数据共享给其他应用程序,也可以充当数据的接收方方,读取其他应用程序共享过来的数据。

不管开发者以何种方式共享数据,数据的内容都将使用DataPackage类来包装。DataPackage类公开了一系列方法用于向其中写入要共享的内容。

  • SetTextg: 向数据包写入文本内容。
  • SetUri: 写入URI地址。
  • SetWebLink: 写入Web连接,只能是以http和https开头的URI地址。
  • SetBitmap: 向数据包中写入图像内容。
  • SetHtmlFormat: 写入带HTML格式的内容。
  • SetRtf: 写入带RTF格式的文本。
  • SetStorageItems: 将文件或目录作为数据内容。

当应用程序接收共享数据时应当使用DataPackageView类,该类专用于读取共享数据内容。公开的一系列方法成员与上面所列方法相对应,都是以Get开头,例如GetTextAsync方法用于读取数据包中的文本内容。
有时候,接收数据的应用程序并不知道数据包中存放的是什么内容,为了让应用程序可以在读取数据之前能够验证数据格式的有效性,DataPackageView类公开AvailableFormats属性,该属性中包含一个字符串列表,表示该数据包中所存放的内容的格式,如果该列表为空,则表示数据包中没有数据。
也可以调用Contains方法来检测数据包中是否包含某种格式的数据,提供的参数由一个字符串来指定,可以通过StandardDataFormats类的静态属性来获得。如果数据包中包含指定格式的数据,则该方法返回True,否则返回False。

(1) 剪贴板

Clipboard是静态类,它直接公开两个方法以供开发者用来读写剪贴板中的数据:调用SetContent方法将数据放入剪贴板;当要从剪贴板中读取数据时则应该调用GetContent方法。要清空剪贴板上的内容请调用Clear方法。若要监听剪贴板的内容更新并做出响应,可以处理ContentChanged事件。
下面示例将演示如何在应用程序中读写剪贴板。
用户界面的布局XAML如下所示:

                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="auto"/>
                        <RowDefinition Height="auto"/>
                    </Grid.RowDefinitions>
                    <StackPanel Margin="15">
                        <TextBox x:Name="txtInput" Header="请输入文本内容:"/>
                        <Button Tag="copy" Content="复制到剪贴板" Tapped="Button_Tapped"/>
                    </StackPanel>
                    <StackPanel Grid.Row="1" Margin="15,40,15,15">
                        <Button Tag="paste" Content="从剪贴板粘贴" Tapped="Button_Tapped"/>
                        <TextBlock x:Name="tbPaste" FontSize="16"/>
                    </StackPanel>
                </Grid>

相关Button控件的事件处理程序如下,分别将文本内容放入剪贴板和从剪贴板读取数据:

        private async void Button_Tapped(object sender, TappedRoutedEventArgs e)
        {
            var btn = sender as Button;
            if (btn.Tag.ToString().Equals("copy"))
            {
                if (txtInput.Text.Length == 0)
                {
                    return;
                }
                //创建数据包 
                DataPackage package = new DataPackage();
                //向数据包写入内容
                package.SetText(txtInput.Text);
                //将内容放入剪贴板
                Clipboard.SetContent(package);
            }
            else
            {
                //获取数据包视图
                DataPackageView packageView = Clipboard.GetContent();
                //判断是否存在文本内容
                if (packageView.Contains(StandardDataFormats.Text))
                {
                    //读取内容
                    tbPaste.Text = await packageView.GetTextAsync();
                }
            }
        }

示例运行效果:剪贴板示例

(2) 向其他应用程序共享数据

要将当前应用程序中的数据共享给其他应用程序,可以通过DataTransferManager类来实现。通过调用静态的GetForCurrentView方法获得一个DataTransferManager实例,然后处理实例对象的DataRequested事件。当用于选择共享目标程序的用户界面出现时会发生该事件,开发者应当在该事件的处理代码中向DataPackage对象写入要共享的数据内容。
调用ShowShareUI静态方法后,会打开一个用户界面,用户可以在该界面上选择一个目标应用,随后会将共享数据发送给被选择的应用。
下面是一个简单的实例,用于把图片文件共享给其他应用。
用户界面布局XAML如下所示:

        <StackPanel Spacing="10">
            <Button x:Name="PickFile_btn" Content="选择图片文件" Tapped="PickFile_btn_Tapped"/>
            <Button Content="分享..." Tapped="Button_Tapped"/>
        </StackPanel>

首先获取DataTransferManager对象实例,并处理DataRequested事件,以设置要共享的内容:

        public MainPage()
        {
            this.InitializeComponent();
            DataTransferManager transferMgr = DataTransferManager.GetForCurrentView();
            //处理相关事件
            transferMgr.DataRequested += TransferMgr_DataRequested;
        }

选择一个图片文件:

        StorageFile file;
        private async void PickFile_btn_Tapped(object sender, TappedRoutedEventArgs e)
        {
            FileOpenPicker picker = new FileOpenPicker();
            picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
            picker.FileTypeFilter.Add(".jpg");
            picker.FileTypeFilter.Add(".jpeg");
            file = await picker.PickSingleFileAsync();
        }

然后打开选择共享目标界面:

        private void Button_Tapped(object sender, TappedRoutedEventArgs e)
        {
            DataTransferManager.ShowShareUI();
        }

下面代码处理DataRequested事件,写入要共享的内容:

        void TransferMgr_DataRequested(DataTransferManager sender,DataRequestedEventArgs args)
        {
            //设置要共享的数据内容
            var deferral = args.Request.GetDeferral();
            DataPackage package = args.Request.Data;
            if (file != null)
            {
                package.SetStorageItems(new List<StorageFile> { file});
                //设置标题属性
                package.Properties.Title = "分享图片";
            }
            //报告数据写入完成
            deferral.Complete();
        }

通过args.Request.Data属性就可以获取到用于包装数据的DataPackage对象,获取后就可以向其中写入需要共享的内容了。注意上面代码中通过GetDeferral方法返回一个等待对象(实际上是DataRequestDeferral类),该对象会延迟打开选择共享目标的用户界面,直到调用Complete方法报告操作完成,才会显示选择界面,在这个过程中应用程序代码可以写入要共享的数据。这样处理是为了避免数据在写入完成之前,过早地发送到其他应用程序,可确保数据的完整。
下面会创建另一个示例,用于接收共享的图片文件夹。

(3) 接收共享数据

如何让自己开发的应用程序能够接收其他应用共享的数据呢?只要将当前应用程序声明为支持共享目标即可。
如果应用程序支持共享目标功能,则当用户选择共享数据时,该应用程序就会出现在选择共享目标界面的应用列表中。当用户选择当前应用作为共享数据的接收者后,当前应用会被激活,并且App类中重写的OnShareTargetActivated方法会被调用,通过方法参数获取到一个ShareOperation对象,再通过该对象的Data属性得到用于读取共享数据的DataPackageView实例,之后开发者就可以根据实际情况读取数据了。
下面示例将实现一个简单的共享目标应用,该示例应用支持接收来自其他应用共享的.jpg图像文件。当示例应用接收共享内容时会将其他应用发送的文件复制到示例应用的本地目录中。
示例应用主页面的XAML代码如下,本页的主要功能是显示已经存入本地目录中的.jpg图像文件。

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <TextBlock FontSize="16" Text="以下为其他应用程序共享的图片:"/>
        <ListView x:Name="lvPics" Grid.Row="1" Margin="3,15,3,5">
            <ListView.ItemsPanel>
                <ItemsPanelTemplate>
                    <ItemsWrapGrid Orientation="Horizontal"/>
                </ItemsPanelTemplate>
            </ListView.ItemsPanel>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Image Source="{Binding}" Stretch="Uniform" Width="80" Height="80"/>
                </DataTemplate>
            </ListView.ItemTemplate>
            <ListView.ItemContainerStyle>
                <Style TargetType="ListViewItem">
                    <Setter Property="Background" Value="{ThemeResource ListViewItemSelectedForegroundThemeBrush}"/>
                    <Setter Property="Margin" Value="5"/>
                    <Setter Property="Padding" Value="6"/>
                </Style>
            </ListView.ItemContainerStyle>
        </ListView>
        <Button x:Name="completedBtn" Grid.Row="2" Margin="30" Content="接收完成" IsEnabled="False" Tapped="Button_Tapped"/>
    </Grid>

在App类中重写的OnShareTargetActivated方法中,通过args.ShareOperation获得一个ShareOperation实例,并作为页面参数,导航到MainPage页面。

        protected override void OnShareTargetActivated(ShareTargetActivatedEventArgs args)
        {
            base.OnShareTargetActivated(args);
            Frame rootFrame = SetRootFrame();
            rootFrame.Navigate(typeof(MainPage),args.ShareOperation);
            Window.Current.Activate();
        }
        Frame SetRootFrame()
        {
            Frame frame = Window.Current.Content as Frame;
            // 不要在窗口已包含内容时重复应用程序初始化,
            // 只需确保窗口处于活动状态
            // 创建要充当导航上下文的框架,并导航到第一页
            if (frame == null)
            {
                frame = new Frame();

                frame.NavigationFailed += OnNavigationFailed;
                // 将框架放在当前窗口中
                Window.Current.Content = frame;
            }
            return frame;
        }

在后台代码中,重写OnNavigatedTo方法,首先将接收到的文件复制到本地目录中,然后扫描应用本地目录下的.jpg文件,并显示在ListView控件中:

        protected async override void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
            opt = e.Parameter as ShareOperation;
            //读取数据
            if (opt != null)
            {
                DataPackageView packageView = opt.Data;
                if (packageView.Contains(StandardDataFormats.StorageItems))
                {
                    //报告开始接收
                    opt.ReportStarted();
                    try
                    {
                        var storageItems = await packageView.GetStorageItemsAsync();
                        var local = ApplicationData.Current.LocalFolder;
                        foreach (var storageItem in storageItems)
                        {
                            if (storageItem.IsOfType(StorageItemTypes.File))
                            {
                                //将文件复制到本地目录
                                var theFile = storageItem as StorageFile;
                                await theFile.CopyAsync(local, theFile.Name, NameCollisionOption.ReplaceExisting);
                            }
                        }
                        //报告接收完成
                        opt.ReportDataRetrieved();
                    }
                    catch (Exception ex)
                    {
                        opt.ReportError(ex.Message);
                    }
                }
                completedBtn.IsEnabled = true;
            }

            //查找本地目录下的.jpg文件
            StorageFolder localFolder = ApplicationData.Current.LocalFolder;
            var files = await localFolder.GetFilesAsync();
            var jpgFiles = from f in files
                           where f.FileType.ToLower().Contains("jpg")
                           select f;
            //显示图片文件列表
            lvPics.Items.Clear();
            foreach (var file in jpgFiles)
            {
                var bmp = new BitmapImage();
                bmp.DecodePixelWidth = 150;
                bmp.SetSource(await file.OpenReadAsync());
                lvPics.Items.Add(bmp);
            }
        }

当应用程序即将开始接收数据时,可以调用ShareOperation对象的ReportStarted方法来告诉共享发送方"当前应用即将开始接收数据";当数据被正确接收后,可以调用ReportDataRetrieved方法通知共享方“数据已经接收完成”;最后,调用ReportCompleted方法报告整个共享操作已经顺利结束。如果在接收处理共享数据过程中发生错误,可以调用ReportError方法报告错误,系统会将该错误消息反馈给用户。

        private void Button_Tapped(object sender, TappedRoutedEventArgs e)
        {
            //报告操作完成
            opt.ReportCompleted();
        }

最后一步是打开清单文件,声明"共享目标"定义,在Application节点下加入以下XML:

  <Applications>
    <Application Id="App"
      Executable="$targetnametoken$.exe"
      EntryPoint="接收共享数据.App">
      <uap:VisualElements>
      <!--...-->
      </uap:VisualElements>
      <Extensions>
        <uap:Extension Category="windows.shareTarget">
          <uap:ShareTarget>
            <uap:SupportedFileTypes>
              <uap:FileType>.jpg</uap:FileType>
              <uap:FileType>.jpeg</uap:FileType>
            </uap:SupportedFileTypes>
            <uap:DataFormat>StorageItems</uap:DataFormat>
          </uap:ShareTarget>
        </uap:Extension>
      </Extensions>
    </Application>
  </Applications>

SupportedFileTypes表示应用程序只接收.jpg和.jpeg类型的文件,DataFormat元素指定应用程序接收的数据格式为StorageItems(文件或者目录)。
示例运行效果:数据共享

7. 应用程序服务(App Service)

应用程序服务(App Service)提供一套可以被其他应用程序调用的后台任务,当其他应用程序调用App Service时,会激活服务应用程序的后台任务来处理数据,而服务应用程序本身可以不在前台运行。
访问App Service的客户端应用程序使用AppServiceConnection类(位于Windows.ApplicationModel.AppService命名空间)来调用服务应用。操作步骤如下:

  1. 实例化AppServiceConnection对象。
  2. 设置PackageFamilyName属性,指定调用服务的客户端的包名称,这是必须项。
  3. 设置AppServiceName属性,指定要调用的服务名,服务应用可以同时提供多项服务,因此需要明确指定服务名称。
  4. 调用OpenAsync方法发起连接。
  5. 连接成功后可以调用SendMessageAsync方法向服务应用发送数据,数据内容用ValueSet类来封装。当服务应用处理完成后,会从SendMessageAsync方法返回一个AppServiceResponse实例,在通过该AppServiceResponse实例的Message属性可以得到从服务应用返回的数据。

接下来通过一个示例来演示一下如何实现App Service。
App Service是通过一个后台任务来响应客户端调用的,因此需要向示例解决方案中添加一个新项目,项目类型为Windows运行时组件。
后台任务类的定义如下:

    public sealed class ServiceTask:IBackgroundTask
    {
        string serviceName = null;
        BackgroundTaskDeferral taskdef;
        public void Run(IBackgroundTaskInstance taskInstance)
        {           
            taskdef = taskInstance.GetDeferral();
            taskInstance.Canceled += OnCancel;
            //获取App Service连接相关的对象
            AppServiceTriggerDetails details = taskInstance.TriggerDetails as AppServiceTriggerDetails;
            if (details != null)
            {
                //获取服务名
                serviceName = details.Name;
                //获取连接对象
                AppServiceConnection connection = details.AppServiceConnection;
                //处理相关事件
                connection.RequestReceived += Connection_RequestReceived;
            }
        }
        void OnCancel(IBackgroundTaskInstance sender,BackgroundTaskCancellationReason reason)
        {
            taskdef.Complete();
        }
        async void Connection_RequestReceived(AppServiceConnection sender,AppServiceRequestReceivedEventArgs args)
        {
            var msgdef = args.GetDeferral();
            //获取参数
            if (args.Request.Message.ContainsKey("num1") is false || args.Request.Message.ContainsKey("num2") is false)
            {
                msgdef.Complete();
                taskdef.Complete();
                return;
            }
            int a = Convert.ToInt32(args.Request.Message["num1"]);
            int b = Convert.ToInt32(args.Request.Message["num2"]);
            int result = default(int);
            //判断计算类型 
            switch (serviceName)
            {
                case "Add":
                    result = a + b;
                    break;
                case "Sub":
                    result = a - b;
                    break;
                case "Mul":
                    result = a * b;
                    break;
                case "Div":
                    result = a / b;
                    break;
                default:
                    result = 0;
                    break;
            }
            //将计算结果发回给客户端
            ValueSet msg = new ValueSet();
            msg.Add("result", result);
            await args.Request.SendResponseAsync(msg);
            msgdef.Complete();
            taskdef.Complete();
        }
    }

该后台任务会根据客户端所请求的服务类型来进行简单的数学运算,如果客户端调用Add服务,就进行加法运算;如果调用的是Sub服务,就进行剪发运算。最后会将计算结果返回给客户端程序(通过调用args.Request属性返回的AppServiceRequest实例的SendResponseAsync方法将数据发回给客户端程序)。
用于封装数据的ValueSet类实现了IDictionary<String,Object>接口,说明它的数据结构为字典类型,Key为字符串类型,而Value可以是任意类型的数据。
在调用服务的客户端应用程序项目中引用刚刚完成的Windows运行时组件项目,然后打开清单文件,添加App Service声明。在Application节点下输入以下XML:

    <Application Id="App"
      Executable="$targetnametoken$.exe"
      EntryPoint="文件与数据.App">
      <uap:VisualElements>
      <!--...-->
      </uap:VisualElements>
      <Extensions>
        <uap:Extension Category="windows.appService" EntryPoint="BgServiceTasks.ServiceTask">
          <uap:AppService Name="Add"/>
        </uap:Extension>
        <uap:Extension Category="windows.appService" EntryPoint="BgServiceTasks.ServiceTask">
          <uap:AppService Name="Sub" />
        </uap:Extension>
        <uap:Extension Category="windows.appService" EntryPoint="BgServiceTasks.ServiceTask">
          <uap:AppService Name="Mul"/>
        </uap:Extension>
        <uap:Extension Category="windows.appService" EntryPoint="BgServiceTasks.ServiceTask">
          <uap:AppService Name="Div"/>
        </uap:Extension>
      </Extensions>
    </Application>

每个uap: Extension元素代表一个App Service,本示例一共公开了四个App Service,分别为Add(加法运算)、Sub(减法运算)、Mul(乘法运算)和Div(除法运算)。四个Extension元素的EntryPoint特性的值相同,即指向刚完成的后台任务类的名字,填写类名时一定要带上命名空间的名称。
由于客户端应用程序要调用服务应用程序的App Service时必须知道客户端应用程序的包名,因此在页面中使用一个TextBox控件,来显示包名,并稍后在后台代码中使用。
为了测试App Service的调用,需要一个客户端项目,主页面布局的XAML如下:

        <StackPanel Spacing="10">
            <TextBlock>
                应用程序包名称:
                <Run x:Name="runPackageName"/>
            </TextBlock>
            <ComboBox x:Name="cmServiceName" Header="App Service 名称:">
                <ComboBoxItem IsSelected="True">Add</ComboBoxItem>
                <ComboBoxItem>Sub</ComboBoxItem>
                <ComboBoxItem>Mul</ComboBoxItem>
                <ComboBoxItem>Div</ComboBoxItem>
            </ComboBox>
            <TextBox x:Name="txtNum1" Header="第一个操作数:"/>
            <TextBox x:Name="txtNum2" Header="第二个操作数:"/>
            <Button Content="计算" Tapped="Button_Tapped"/>
            <TextBlock>
                计算结果:
                <Run x:Name="runRes"/>
            </TextBlock>
        </StackPanel>

按钮的Tapped事件处理程序如下:

        private async void Button_Tapped(object sender, TappedRoutedEventArgs e)
        {
            //验证输入
            int n1, n2;
            if (!int.TryParse(txtNum1.Text,out n1) || !int.TryParse(txtNum2.Text,out n2))
            {
                return;
            }
            Button btn = sender as Button;
            btn.IsEnabled = false;
            AppServiceConnection conn = new AppServiceConnection();
            //设置必备属性
            conn.PackageFamilyName = runPackageName.Text;
            conn.AppServiceName = (cmServiceName.SelectedItem as ComboBoxItem).Content.ToString();
            //打开连接
            var state = await conn.OpenAsync();
            if (state == AppServiceConnectionStatus.Success)
            {
                //收集数据
                ValueSet p = new ValueSet();
                p.Add("num1",n1);
                p.Add("num2", n2);
                //发送请求
                AppServiceResponse response = await conn.SendMessageAsync(p);
                //获取结果
                if (response.Status == AppServiceResponseStatus.Success)
                {
                    runRes.Text = response.Message["result"].ToString();
                }
            }
            btn.IsEnabled = true;
        }
    }

在构造函数中,获取包名,并赋值给TextBox控件:

        public AppServicesPage()
        {
            this.InitializeComponent();
            runPackageName.Text = Package.Current.Id.FamilyName;
        }

在向承载App Service的服务应用发送消息前,必须确保OpenAsync方法顺利调用,方法成功调用后会返回AppServiceConnectionStatus.Success值。从SendMessageAsync方法返回的AppServiceResponse对象中提取App Service发回的计算结果,并显示到用户界面上。
示例运行效果:应用程序服务
关于应用服务的另一个示例可参考:添加链接描述

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页