65.9K
CodeProject 正在变化。 阅读更多。
Home

UWP 的数据存储

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (10投票s)

2016 年 6 月 29 日

CPOL

18分钟阅读

viewsIcon

65672

UWP 应用程序可用的存储 API 与其他 .Net 环境中的 API 略有不同。本文将通过示例介绍 UWP 特有的存储概念,并演示如何使用其中的许多功能。

引言

这是我正在撰写的关于通用 Windows 平台的系列文章之一。在这篇文章中,我将简要介绍 UWP 应用程序中一些可用的存储方法,目的是在后续文章中重点介绍 Entity Framework 和 SQLite。在几乎所有您进行编程的平台上,您都会遇到保存数据的需求。这可能是在内存中的位标志,也可能是数据库中的行。UWP 的可用方法在文件访问方面与许多其他 .Net/C# 环境有所不同。对于主要从事 WPF 应用程序工作并在 UWP 中首次尝试处理文件的人来说,起初可能会感到有些迷茫。但这很容易掌握。我写本文档的目的是向他人介绍 UWP 中的持久化存储,希望能够消除那种迷茫的感觉。

 

目录

更新清单

下面讨论的一些代码需要对应用程序的清单进行更改。清单包含有关您应用程序的集合信息,包括它可能需要的权限。在您的 UWP 项目中,会有一个名为 Package.appxmanifest 的文件。双击它会打开一个 UI,允许轻松编辑清单。您也可以使用文本编辑器进行编辑,但我将假设您使用的是 UI。一个有趣的选项卡是功能选项卡。当我说添加功能时,您需要打开此 UI 并确保所需功能的复选框已勾选。

清单中的功能选项卡

应用程序可能还需要进行声明。在这篇文章中,唯一需要关注的声明是应用程序有能力处理特定的文件类型。要添加文件类型声明,请在可用声明下拉菜单中单击声明选项卡,选择文件类型关联,然后单击添加。最少需要输入的信息包括一个名称(必须全部小写,无空格或特殊字符)和一个或多个支持的文件类型,即文件扩展名(前面加上一个点)以及可选的 MIME 类型。

清单中的文件类型声明

本地设置

对于少量、简单数据的存储,ApplicationData.LocalSettings 就足够了,而且易于使用。ApplicationData.LocalSettings 是一个 ApplicationDataContainer。最受关注的属性是 Values 属性。Values 是一个 Dictionary。您用于设置的名称字符串长度最多为 255 个字符。简单值的数据大小可达 8KB,这些值是 Windows 运行时基类型。在存储更复杂的值时,可以将一组名称和值打包在 ApplicationDataCompositeValue 中,大小可达 64KB,并分配给一个值。ApplicationDataCompositeValue 也可以包含 Windows 运行时基类型。

只需为键值分配一个名称即可保存它。运行时将负责持久化这些值,并在应用程序再次运行时将它们加载回 ApplicationData.LocalSettings。在以下代码中,我获取了应用程序首次运行的日期和时间。应用程序首次运行时,不会为关联的键保存任何设置。这意味着这是代码第一次运行,它会立即保存当前的 DateTimeOffset,以便下次应用程序运行时加载。代码还加载了用户的姓名,该姓名存储在键 UserName 下。该值是一个复合值,包含 FirstNameLastName 的值。如果未找到姓名,则使用默认姓名 John Doe

const string FirstRunKey = "FirstRun";
const string UserNameKey = "UserName";
const string FirstNameKey = "FirstName";
const string LastNameKey = "LastName";

var settingValues = ApplicationData.Current.LocalSettings.Values;
DateTimeOffset firstRunDate;
String firstName = "John", lastName = "Doe";
Object temp;
if(settingValues.TryGetValue(FirstRunKey, out temp))
    firstRunDate = (DateTimeOffset)temp;
else
    settingValues[FirstRunKey] = firstRunDate =DateTimeOffset.Now;

if(settingValues.ContainsKey(UserNameKey))
{
    ApplicationDataCompositeValue nameValues = (ApplicationDataCompositeValue)settingValues[UserNameKey];
    firstName =(String) nameValues[FirstNameKey];
    lastName = (String)nameValues[LastNameKey];
}
else
{
    ApplicationDataCompositeValue nameValues = new ApplicationDataCompositeValue();
    nameValues[FirstNameKey] = firstName= "John";
    nameValues[LastNameKey] = lastName = "Doe";
}

文件访问

UWP 应用程序在沙箱环境中运行。它们无法完全访问运行它们的文件系统。应用程序可以访问许多位置。与其他 .Net 环境不同,您无法直接通过路径访问外部资源。您的应用程序需要向用户请求访问文件的权限,或者查询其已声明需要访问的特定类型的文件或特定库集合中的文件。硬编码的外部资源路径在此环境中通常不起作用。这是一个需要适应的限制和考虑因素,因为它有所不同。

应用程序已经有权访问一些文件夹。这些文件夹特定于应用程序,因此其他应用程序无法查看其内容。两个应用程序可以通过注册相同的文件类型并将其数据写入库之一,或者通过用户授予对两个文件的文件资源的访问权限来共享文件。

StorageFile 和 StorageFolder

IStorageItem 接口用于操作文件和文件夹以及获取有关它们的信息。对于文件项,还将实现 IStorageFile 接口。它允许复制、移动和打开文件内容。文件夹将实现 IStorageFolder 接口。它具有枚举文件夹内文件以及创建其他文件和文件夹的方法。当然,要对这些接口进行任何调用,首先需要获取文件和文件夹的引用。

已知文件夹

应用程序预计在某些时候会需要访问许多文件夹集合。这些文件夹被组织成称为的组。库是逻辑文件夹集合,旨在保存特定类型的文件。同一库中的文件可能存储在文件系统上的不同位置,甚至可能存储在不同的计算机上。这些包括用户的文档文件夹、音乐文件夹、图片、视频和可移动存储设备。应用程序必须声明它需要访问这些文件夹。声明在应用程序的清单中进行。如果您在 UWP 项目中双击应用程序的 Package.appxmanifest 文件,然后在出现的窗口中选择功能选项卡,您将看到可以声明的功能列表。此处相关项目是音乐库视频库图片库可移动存储。文档库的功能不显示,但它确实存在。可以通过将清单打开为文本来添加它。只有当应用程序还注册了文件类型时,此方法才有效。如果应用程序具有所需的功能声明,则可以通过关联文件夹通过 KnownFolders 静态类获取引用,或者通过调用 StorageLibrary.GetLibraryAsync(KnownLibraryId) 来获取引用。KnownFolders 中引用的文件夹名称以及可以传递给 GetLibraryAsync 的值。

 

名称 API 访问 KnownLibraryId
Documents KnownFolders.DocumentsLibrary KnownLibraryId.Documents
音乐 KnownFolders.MusicLibrary KnownLibraryId.Music
图片 KnownFolders.PicturesLibrary KnownLibraryId.Pictures
视频 KnownFolders.VideosLibrary KnownLibraryId.Music
可移动存储* KnownFolders.RemovableDevices
家庭组库 KnownFolders.HomeGroup
媒体服务器设备 (DLNA) KnownFolders.MediaServerDevices

当需要特定类型的文件时,可以使用 QueryOptions 对象和要返回的文件扩展名来构建查询。

async void PopulateSongList()
{
    QueryOptions queryOption = new QueryOptions(CommonFileQuery.OrderByName, new string[] { ".mp3", ".mp4", ".wma" });
    Queue<IStorageFolder> workFolders = new Queue<IStorageFolder>();
    var fileList =await  KnownFolders.MusicLibrary.CreateFileQueryWithOptions(queryOption).GetFilesAsync();

    foreach (var file in fileList)
    {
        {
       		//the file variable now holds a reference to one of the song file
            svm.SourceFile = file;
            SongList.Add(svm);
        }
    }
}
查询用户音乐库中音乐文件的集合

应用程序文件夹

可以使用属性 Windows.ApplicationMode.Package.Current.InstalledLocation 获取表示应用程序包内文件的存储文件夹。也可以通过 URI 直接访问包内的文件。包内文件的 URI 可以通过在资源名称前加上 ms-appx:/// 来形成。该 URI 将传递给静态方法 StorageFile.GetFileFromApplicationAsync(String URI)

应用程序还将访问本地文件夹、漫游文件夹和临时文件夹。这些文件夹虽然应用程序可以访问,但用户可能无法直接访问。本地文件夹特定于应用程序运行的设备。漫游文件夹用于存储要备份并在不同计算机之间同步的信息。而临时文件夹应被视为工作空间,可能会在机器需要释放空间时随时删除。本地文件夹的内容将一直保留,直到应用程序将其删除。

文件夹类型 URI 前缀 静态对象
应用程序包 ms-appdata:/// Windows.ApplicationModel.Package.Current.InstalledLocation
临时文件夹 ms-appdata:///temp/ ApplicationData.Current.TemporaryFolder
本地文件夹 ms-appdata:///local/ Windows.Storage.ApplicationData.Current.LocalFolder
漫游 ms-appdata:///roaming/ Windows.Storage.ApplicationData.Current.RoamingFolder

下载文件夹和下载文件

所有应用程序都可以访问 Downloads 文件夹,并且可以在其中创建文件而无需任何特殊权限。应用程序无法访问彼此的下载内容。还有一个 BackgroundDownloader 类,可用于下载信息并将其保存到文件中。给定一个 URL 和一个用于保存文件的 IStorageFileBackgroundDownloader 将负责创建 DownloadOperation 将数据保存到文件。DownloadOperation 不会开始传输

BackgroundDownloader _downloader =  new BackgroundDownloader();;
String NewDownloadUri = "https://c1.staticflickr.com/1/335/18928517216_1f4cfcc0e5_o.jpg";
String fileName = NewDownloadUri.Substring(NewDownloadUri.LastIndexOf("/") + 1);
IStorageFile newFile = await DownloadsFolder.CreateFileAsync(fileName, CreationCollisionOption.GenerateUniqueName);
var newDownload = _downloader.CreateDownload(new Uri(NewDownloadUri),newFile );
newDownload.StartAsync();

文件选择器

当您的应用程序需要用户选择文件进行打开或保存时,可以使用 FilePickers。文件选择器类似于 OpenFileDialogCloseFileDialog 类,但有一个重要的区别是,文件对话框会返回选定文件的完整路径,而文件选择器则不会。否则,如何使用任一类的总体用法是相似的。正在写入或读取的文件流不一定存储在设备存储中。用户可能已将文件位置选择为 OneDrive。由于文件处理方式被抽象化,您的应用程序无需为这些情况进行任何特殊处理。无论文件来自本地存储还是通过其他服务进行管理,您的代码都将是相同的。

要使用文件选择器,您必须确定您的应用程序可以打开的文件类型以及您是希望以读写模式打开文件。您的应用程序可以打开的文件类型通过文件的扩展名来标识。有时,文件类型可能由多个扩展名标识;例如,静态 HTML 文档可能有一个 htmhtml 扩展名。文件类型信息通过两个对象传递给文件选择器:一个字符串作为文件类型的友好名称,以及一个或多个字符串数组,其中包含与该类型关联的扩展名。

文件选择器将返回一个 StorageFile,可用于读取和写入。以下代码示例摘自一篇题为Introduction to HoloLens Development with UWP 的文章中的文本编辑器,并进行了少量修改。在 Init() 方法中,我将文件类型及其扩展名加载到 Dictionary 中。这并非严格必需,但处理文件类型的一种便捷方式。对于打开和关闭代码,代码是相似的。

打开文件

可以使用以下步骤以读模式打开文件。

  1. 创建 FileOpenPicker
  2. 将扩展名添加到选择器的 FileTypeFilter 集合中
  3. 通过调用选择器的 async PickSingleFileAsync() 请求 StorageFile
    (如果请求多个文件,请改用 async PickMultipleFilesAsync()
  4. 如果返回值是 null,则表示用户取消/关闭了对话框。相应处理
  5. 从流中读取
Dictionary<string, IList<string>> FileTypeList ;
public void Init()
{
	FileTypeList = new Dictionary<string, IList<string>>();
	FileTypeList.Add("Text Document", new List<string>() { ".txt", ".text" });
	FileTypeList.Add("HTML Document", new List<string>() { ".htm", ".html" });
}

async void OpenFile()
{
	//Create a FilePicker
    FileOpenPicker fileOpenPicker = new FileOpenPicker();
    //Populate the file types 
    foreach (string key in FileTypeList.Keys)
    {
        foreach (string extension in FileTypeList[key])
        {
            fileOpenPicker.FileTypeFilter.Add(extension);
        }
    }
    //Get the Files
    StorageFile file = await fileOpenPicker.PickSingleFileAsync();
    if (file != null)
    {
        Text = await FileIO.ReadTextAsync(file);
        FileName = file.Name;
    }
}

保存文件

可以使用以下步骤以写模式打开文件。

  1. 创建 FileSavePicker
  2. 将文件类型添加到 FileSavePickerFileTypeChoices 集合中
  3. 通过调用选择器的 async PickSaveFileAsync() 请求 StorageFile
  4. 如果返回值是 null,则表示用户取消/关闭了对话框。相应处理
  5. 写入流
async void  SaveFile()
{
    FileSavePicker fileSavePicker = new FileSavePicker();
    foreach(string key in FileTypeList.Keys)
    {
        fileSavePicker.FileTypeChoices.Add(key, FileTypeList[key]);
    }
    StorageFile file = await fileSavePicker.PickSaveFileAsync();
    if(file != null)
    {
        var sf = await file.GetParentAsync();
        var x = sf.Provider;
        CachedFileManager.DeferUpdates(file);
        await FileIO.WriteTextAsync(file, Text);
        FileUpdateStatus status = await CachedFileManager.CompleteUpdatesAsync(file);
        FileName = file.Name;
    }
}

文件选择器的可移植性

在测试不同 UWP 实现的 API 时,我注意到在 Xbox One 和 Windows IoT 实现上,虽然存在文件选择器接口,但似乎没有与之相关的 UI。调用它们不会显示 UI,而是返回 null 来请求存储文件。似乎没有探测设备上是否存在工作版本的此接口的方法。在没有官方方法检测其可用性时,我求助于计时文件选择器响应所需的时间。如果返回值是 null 且方法返回的时间极短,则很可能存在一个不起作用的实现。但这也可能意味着用户在打开对话框时按下了 Escape 键。因此,计时方法最多被视为一种 hack。

文件夹选择器

使用 FolderPicker 与使用文件选择器非常相似。实例化一个 FolderPicker,调用显示选择器的方法(PickSingleFolderAsync()),如果它返回一个值,则表示用户已选择一个您可以使用的文件夹。将文件夹添加到 FutureAccessList,以便以后可以访问它。FolderPicker 还允许使用 PickerLocationId 枚举来建议起始位置。它定义的值包括 DocumentsLibraryDownloadsMusicLibraryPicturesLibraryVideosLibraryObjects3d

var folderPicker = new Windows.Storage.Pickers.FolderPicker();
folderPicker.SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.Desktop;
var folder = await 	folderPicker.PickSingleFolderAsync();
if(folder!=null)
{
	StorageApplicationPermissions.FutureAccessList.AddOrReplace("OutputFolder", folder);

}

FileIO 类

FileIO 类是一个静态类,充当某些文件操作的辅助程序。它作用于传递给它的第一个参数的 IStorageFile 实例。此类用于执行诸如在文件末尾追加字符串、将文件读取或写入为字符串或字符串列表(列表中每个元素是文件中的不同行)或从文件中读取和写入字节等操作。

var targetFile =await ApplicationData.Current.LocalFolder.CreateFileAsync("TestFile.txt", CreationCollisionOption.GenerateUniqueName);
await FileIO.WriteTextAsync(targetFile, "This content will be written to the file");
创建一个名为 TestFile.txt 的新文件(如果存在同名文件,则分配一个新名称),并将一行文本写入该文件。
IStorageFile fileToRead = await ApplicationData.Current.LocalFolder.GetFileAsync("TestFile.txt");
string contents = await FileIO.ReadTextAsync(fileToRead);
打开现有文件并将所有内容读取为字符串。

文件类型关联

应用程序可以处理的文件类型在应用程序的清单中声明(通常名为 Package.appxmanifest)。如果您在 Visual Studio 中双击此文件,您可以通过 UI 更改清单信息。在 UI 中选择声明选项卡。从下拉列表中选择文件类型关联,然后单击添加

当用户尝试通过文件类型关联打开应用程序文件时,应用程序将收到有关打开请求的通知,通过 void Applicaiton.OnFileActivated(FileActivatedEventArgs args)。您需要在应用程序的 app 类中重写此事件。通过传递给此类中的事件参数的 Files 属性,可以传递请求的文件列表(可以有一个以上)。

访问缓存

一旦用户授予对存储项目的访问权限,您就可以将您的文件访问令牌添加到您有权访问的文件列表中。在撰写本文时,最多可以有 25 个文件。如果您以后需要访问某些文件,也可以将这些文件的引用添加到您需要访问的文件列表中。这些可以通过静态类 StorageApplicationPermissions 进行管理。该类有两个属性。MostRecentlyUsedList 用于保存您最近访问过的存储项,而 FutureAccessList 用于您尚未访问过的存储项。应用程序可以终止、重启,仍然可以访问此列表。当列表达到容量且添加更多文件时,文件将自动从 MostRecentlyUsedList 中移除。最旧的访问令牌将被从列表中移除。当项目从该列表中移除时,MostRecentlyUsedList 会触发 ItemsRemoved 事件。您可以添加一个事件处理程序来接收项目被移除的通知。

AccessListEntry 元素包含两个数据。一个是 Token,您可以用来再次检索文件的字符串值;另一个是 Metadata,默认为空,但可以包含您分配给它的值。

来自远程文件系统的文件

如果您的应用程序处理的是由其他服务(如 OneDrive)提供的文件,当发生更改时,Windows 将负责更新应用程序的文件副本。但是,如果您需要对文件执行多个操作,您将不希望 Windows 在同一时间尝试更新文件。为防止这种情况,您可以使用 CachedFileManager 来推迟对文件的更新,直到您完成预期的操作。这个静态类有两个值得关注的方法。DeferUpdates(IStorageFile) 将阻止对远程文件的更新。当文件修改完成后,可以通过调用 async CompleteUpdatesAsync(IStorageFile) 来释放它进行更新。释放文件时会返回一个 FileUpdateStatus 值。

含义
Incomplete 更新不成功。可以重试
Complete 文件已成功更新
UserInputNeeded 需要用户采取行动,例如输入凭据
CurrentlyUnavailable 远程文件不可用
失败 文件当前及以后无法更新。这可能发生在远程文件被删除时
CompleteAndRenamed 文件已保存为其他名称

 

外部/可移动存储

可以通过 KnownFolders.RemovableDevices 发现可移动驱动器,如闪存驱动器、外部硬盘驱动器和存储卡。此集合返回的文件夹是已连接驱动器的根目录。访问驱动器并不意味着访问驱动器上的所有文件。应用程序只能检测到它已注册的文件类型。如果应用程序未注册任何文件类型,尝试枚举可移动驱动器上的文件将导致 ACCESS DENIED 异常。

List DriveList = new List();
foreach (var device in await KnownFolders.RemovableDevices.GetFoldersAsync())
{
    DriveList.Add(device);
}
创建外部存储设备列表

Entity Framework Core with SQLite

随着 2016 年 Windows Anniversary Update 的发布,SQLite 版本 3.11.2 将发布。SQLite 是一个轻量级的单用户数据库系统。它在进程内运行,因此无需设置数据库服务器,无需配置,并且所有数据都包含在一个文件中。可以通过 NuGet 为项目添加对 SQLite 的支持。要在 Visual Studio 中为项目添加支持,请打开工具菜单,选择NuGet 包管理器,然后选择程序包管理器控制台。要安装支持,请键入 Install-Package EntityFramework.SQLite -Pre。您还需要命令包,可以通过键入 Install-Package EntityFramework.Commands -Pre 来安装。此处使用 -Pre 参数的原因是,在撰写本文时,这些仍处于预发布状态。这些的正式版本很快就会发布。发布后,我将撰写另一篇专门介绍 Entity Framework with SQLite 的文章,并在此处添加链接。

在 UWP 上,您可以将 SQLite 与 EntityFramework 一起使用。通过 EntityFramework,要在表中保存的数据类型是在代码中定义的。接下来的代码示例来自一个位置记录器。要保存的数据分为两种类型。有个体位置,以及用于将带时间戳的位置分组的会话。面向代码的 EntityFramework 要求您首先在代码中定义数据结构,然后从代码派生数据库。这是 Location 类。

using System;

namespace SQLiteSample.Data
{
    public class Location
    {
        public DateTimeOffset Timestamp { get; set;  }
        public double Latitude { get; set;  }
        public double Longitude { get; set;  }

        public double Altitude { get; set;  }

        public double HorizontalAccuracy { get; set;  }
    }
}
用于存储的 Location 类

会话将有一个 GUID 作为主键。我已使用属性将此属性标记为主键。请注意,EntityFramework 还会自动假定任何名为 Id(类型名称)Id 的属性为主键属性。会话和位置之间的关联是通过 ICollection 进行建模的。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;


namespace SQLiteSample.Data
{
    public partial  class LogSession
    {
        [Key]
        public Guid SessionID { get; set;  }
        public DateTimeOffset  SessionStart { get; set;  }
        public DateTimeOffset? SessionEnd { get; set;  }

        public string Name { get; set;  }

        public virtual ICollection<Location> Locations { get; set;  }
    }
}

目前这些只是松散的类。要将它们保存在数据库中,我们需要定义一个派生自 DbContext 的类,其中包含这些类的 DbSet<T> 集合。将 DbSet<T> 属性视为表。在此类中,我们还定义了将保存数据表的文件的名称,并可以指定有关表的其他信息。在此示例中,我将一个值标记为必需。

using Microsoft.Data.Entity;


namespace SQLiteSample.Data
{
    public class LocationLogContext: DbContext
    {
        public DbSet<LogSession> Sessions { get; set;  }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite("Filename=Locations.db");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<LogSession>().Property(b => b.SessionID).IsRequired();
        }
    }
}

要使用数据库,我们只需实例化派生的 DbContect 类并调用 EnsureCreated() 方法。EnsureCreated() 方法将检查数据库是否存在。如果数据库存在,则此方法不再执行任何操作。如果不存在,则此方法将创建它。创建后,向数据库添加新数据只需实例化新类的实例,将它们添加到 DbContext(或添加为已收集到 DbContext 中的对象的子对象),然后调用 SaveChanges()SaveChangesAsync()

//Create a session and save it
_currentSession = new LogSession() { LogSessionId = Guid.NewGuid(), SessionStart = DateTimeOffset.Now, Locations = new List()  };
_locationLogContext.Sessions.Add(_currentSession);
_locationLogContext.SaveChanges();


private void _locationWatcher_PositionChanged(Geolocator sender, PositionChangedEventArgs args)
{
    var session = _currentSession;
    var coords = args.Position.Coordinate;
    if(session != null)
    {
    	//Create a new location object
        Location loc = new Location()
        {
            Longitude = coords.Longitude,
            Latitude = coords.Latitude,
            HorizontalAccuracy = coords.Accuracy,
            Altitude = coords.Altitude ?? 0,
            Timestamp = DateTimeOffset.Now, 
           // ID = session.Locations.Count,
            //SessionID = session.SessionID
        };
        //Add it to the current session object
        session.Locations.Add(loc);
        //Save the changes
        _locationLogContext.SaveChangesAsync();
    }
}

		

关于 Entity Framework 还有很多内容可以讨论。关于它的文章将在 2016 年 Windows Anniversary Update 发布后的几周内提供。

闭幕词

如前所述,本文发布的时间接近 Microsoft 准备 Windows 更新的时间。更新后,可能需要向本文添加其他信息。在 Microsoft 发布后的几周内回访,并在历史记录部分(下方)查找因更新而进行的添加列表。

历史

  • 2016 年 6 月 28 日 - 首次发布
© . All rights reserved.