使用UWP进行数据存储(译文)
By S.F.
本文链接 https://www.kyfws.com/news/data-storage-with-uwp/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 17 分钟阅读 - 8056 个词 阅读量 0使用UWP进行数据存储(译文)
原文地址:https://www.codeproject.com/Articles/1109667/Data-Storage-with-UWP
原文作者:Joel Ivory Johnson
译文由本站翻译
前言
UWP应用程序可用的存储API与其他.Net环境中的存储API略有不同.本文将向您介绍UWP特有的存储概念,并举例说明如何使用其中的许多概念.
介绍
这是我在通用Windows平台上撰写的系列文章之一.在本文中,我将简要介绍UWP应用程序中可用的一些存储方法,以期在以后的文章中介绍Entity Framework和SQLite.在您编程的几乎每个平台上,您都会遇到保存数据的需求.这可以作为内存中的位标志或数据库中的行. UWP的可用方法在访问文件的方式上与许多其他.Net/C#环境有点不同.最初主要在WPF应用程序上工作的人试图在UWP中首次使用文件,这会使人们一开始感到有些迷茫.但是很容易抓住.我编写此文档的目的是向其他人介绍UWP的持久存储,以防止丢失这种感觉.
目录
- 更新清单
- 本地设置
- 文件选取器
- 档案存取
- StorageFile和存储文件夹
- 已知资料夹
- 下载文件夹和下载文件
- 文件选取器
- 开启档案
- 保存文件
- 文件选择器的可移植性
- 文件夹选择器
- 文件IO
- 访问缓存
- 远端档案
- 外置硬盘
- 实体框架
- 结束语
- 历史
更新清单
下面讨论的某些代码需要更改应用程序清单.清单包含有关您的应用程序的信息的集合,包括可能需要的权限.在您的UWP项目中,将有一个名为" Package.appxmanifest"的文件.双击它会打开一个UI,使清单易于编辑.您也可以使用文本编辑器对其进行编辑,但是我假设您正在使用UI.功能标签是我们感兴趣的一个标签.当我提到添加功能时,您将需要打开此UI并确保已选中所需功能的复选框.
应用程序可能还需要进行声明.在这篇文章中,唯一需要关注的声明是应用程序能够处理特定的文件类型.要添加文件类型声明,请从"可用声明"下拉列表中单击"声明"选项卡,选择"文件类型关联",然后单击"添加".最少需要输入的信息包括名称(必须全部为小写字母,没有空格或特殊字符)以及一种或多种受支持的文件类型,其中文件扩展名(以句点开头)和可选的mime类型.
本地设置
为了存储少量,简单的数据,可以使用ApplicationData.LocalSettings来满足需要. " ApplicationData.LocalSettings"是ApplicationDataContainer最受关注的属性是" Values"属性. “值"是"字典”.您用于设置的名称将是一个长度最多255个字符的字符串.对于Windows运行时基本类型的简单值,该值的数据最大为8K.当存储更复杂的值时,可以将名称和值的集合打包在大小不超过64K的" ApplicationDataCompositeValue"中,并分配给一个值. " ApplicationDataCompositeValue"也可以包含Windows运行时基本类型.
为键值分配名称是保存它所需要做的全部工作.运行时将负责保留这些值,并在应用程序再次运行时将其加载回ApplicationData.LocalSettings
中.在下面的代码中,我获得了第一次运行该应用程序的日期和时间.第一次运行该应用程序时,将不会为关联的密钥保存任何设置.这意味着这是代码第一次运行,并且它将立即保存当前的" DateTimeOffset",以在下次应用程序运行时加载.该代码还加载存储在键UserName下的用户名.此值是一个包含FirstName和LastName值的复合值/如果找不到名称,则使用默认名称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和存储文件夹
IStorageItem接口用于操作和获取文件和文件夹的信息.对于文件项,也将实现" IStorageFile"接口.它允许复制,移动和打开内容.文件夹将实现接口" IStorageFolder".它具有枚举其中的文件以及创建其他文件和文件夹的方法.当然要在这些接口上进行任何调用,首先需要获取对文件和文件夹的引用.
已知资料夹
在某些时候,应用程序需要访问许多文件夹集合.这些文件夹按称为库的组进行组织.库是旨在容纳某些文件类型的文件夹的逻辑集合.同一库中的文件有可能存储在文件系统上的不同位置,甚至存储在不同的计算机上.其中包括用户的文档文件夹,音乐文件夹,图片,视频和可移动存储设备.应用程序必须声明它需要访问这些文件夹.该声明在应用程序的清单中进行.如果在UWP项目中双击应用程序的Package.appxmanifest,然后在出现的窗口中选择"功能"选项卡,您将看到可以声明的功能列表.这里相关的项目是音乐库,视频库,图片库和可移动存储.没有显示文档库的功能,但确实存在.可以通过将清单打开为文本来添加它.仅当应用程序还注册了文件类型时,此方法才有效.如果应用程序具有所需的功能声明,则可以通过关联的文件夹(具有KnownFolders静态类)或通过调用StorageLibrary.GetLibraryAsync(KnownLibraryId)获得引用. KnownFolders中引用的文件夹的名称以及可以传递给GetLibraryAsync的值. 当需要特定类型的文件时,可以使用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)". 应用程序还将有权访问本地文件夹,漫游文件夹和临时文件夹.这些是文件夹,虽然可以被应用程序访问,但用户可能无法直接使用它们.本地文件夹特定于运行应用程序的设备.在漫游文件夹中,您将存储要备份和同步访问计算机的信息.临时文件夹将被视为工作空间,并且在计算机需要释放空间时可以随时将其删除.本地文件夹的内容将保留,直到应用程序将其删除.
下载文件夹和下载文件
所有应用程序都可以访问"下载"文件夹,并且可以在其中创建文件,而无需任何特殊功能.应用程序无法访问彼此的下载.还有一个" BackgroundDownloader"类,可用于下载信息并将信息保存到文件.给定一个URL和一个" IStorageFile",在其中可以说" BackgroundDownloader"将负责创建一个" DownloadOperation"以将数据保存到文件中. " DownloadOperation"没有开始传输
BackgroundDownloader _downloader = new BackgroundDownloader();;
String NewDownloadUri = "<a href="https://c1.staticflickr.com/1/335/18928517216_1f4cfcc0e5_o.jpg">https://c1.staticflickr.com/1/335/18928517216_1f4cfcc0e5_o.jpg</a>";
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.文件选择器类似于" OpenFileDialog"和" CloseFileDialog"类,但有一个明显的例外,那就是尽管文件对话框将返回所选文件的完整路径,但文件选择器却没有.否则,如何使用任一类的总体用法类似.正在写入或读取的文件流不一定要保留在设备的存储中.用户可能已经在OneDrive上选择了文件位置.由于抽象了文件处理方式,因此您的应用程序无需针对这些情况进行任何特殊处理.无论文件是来自本地存储还是通过其他服务进行管理,您的代码都是相同的. 要使用文件选择器,必须标识应用程序可以打开的文件类型,以及是否要打开文件进行读取或写入.您的应用程序可以打开的文件类型通过文件的扩展名标识.有时,一个文件类型可能由多个扩展名标识;静态HTML文档的扩展名可能是" htm"或" htm" l.有关文件类型的信息通过两个对象传递给文件选择器.字符串,它是文件类型的友好名称,是一个或多个包含与该类型相关联的扩展名的字符串的数组. 文件选择器将返回一个" StorageFile",可用于读取和写入.以下代码示例摘自文本编辑器,其标题为"使用UWP进行HoloLens开发简介"(http://www.codeproject.com/Articles/1107169/Introduction-to-HoloLens-Development-with-UWP),内容如下:小修改.在" Init()“中,我将文件类型及其扩展名加载到"字典"中.这不是严格必要的,但是是一种处理文件类型的简便方法.对于打开和关闭代码,代码是相似的.
开启档案
可以通过以下步骤打开要读取的文件.
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;
}
}
保存文件
可以通过以下步骤打开文件进行写入.
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",并且该方法返回所花费的时间非常短,则很可能存在无效的实现.但这也可能意味着对话框打开时,用户按住转义键.因此,计时方法充其量被认为是骇客.
文件夹选择器
FolderPicker
的使用与文件选择器的使用非常相似.实例化一个" FolderPicker",调用该方法以显示选择器(PickSingleFolderAsync()
),如果它返回一个值,则用户已经选择了一个可以使用的文件夹.将文件夹添加到" FutureAccessList",以便以后可以访问. FolderPicker
还允许使用PickerLocationId
枚举来建议起始位置.它定义的值包括" DocumentsLibrary"," Downloads"," MusicLibrary"," PicturesLibrary"," VideosLibrary"和" Objects3d".
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");
IStorageFile fileToRead = await ApplicationData.Current.LocalFolder.GetFileAsync("TestFile.txt");
string contents = await FileIO.ReadTextAsync(fileToRead);
文件类型关联
应用程序可以处理的文件类型在应用程序的清单中声明(通常称为" Package.appxmanifest").如果您在Visual Studio中双击此文件,则可以通过UI更改清单信息.在用户界面中选择"声明"标签.从下拉列表中选择文件类型关联,然后单击添加.
当用户尝试通过文件类型关联使用应用程序打开文件时,应用程序将通过void Applicaiton.OnFileActivated(FileActivatedEventArgs args)
接收打开要求的通知.您将需要在应用程序的应用程序类中覆盖此事件.通过" Files"属性中传递给此类的事件参数传递所请求文件的列表(可以有多个).
访问缓存
用户授予对存储项目的访问权限后,您可以将文件的访问令牌添加到您有权访问的文件列表中.在撰写本文时,该文件最多可以包含25个文件.如果将来有某些文件需要使用,您还可以将对这些文件的引用添加到需要访问的文件列表中.这些可以在静态类" StorageApplicationPermissions"中进行管理.该类具有两个属性. " MostRecentlyUsedList"用于保存您最近访问的存储项目," FutureAccessList"用于保存您尚未访问的存储项目.该应用程序可以终止,重新启动,并且仍然可以访问此列表.一旦列表增加容量并添加更多文件,文件将自动从" MostRecentlyUsedList"中删除.最过期的访问令牌将是从列表中删除的令牌.当从此列表中删除项目时," MostRecentlyUsedList"具有触发的" ItemsRemoved"事件.您可以添加事件处理程序以接收有关已删除项目的通知.
AccessListEntry元素具有两个数据.一个是Token
,可用于再次检索文件的字符串值,另一个是Metadata
,它默认为空,但可以具有您为其分配的一些值.
来自远程文件系统的文件
如果您的应用程序在另一个服务(例如OneDrive)提供给它的文件上工作,则Windows将负责在发生更改时更新该应用程序的文件副本.但是,如果要对文件执行许多操作,则您将不希望Windows尝试同时更新文件.为了防止这种情况,您可以使用" CachedFileManager"来延迟和更新文件,直到完成您想要的操作为止.该静态类有两种有趣的方法. DeferUpdates(IStorageFile)将阻止对远程文件进行更新.完成对文件的修改后,可以通过调用async CompleteUpdatesAsync(IStorageFile)
将其发布以进行更新.释放文件时,将返回FileUpdateStatus
值.
外部/可移动存储
可移动驱动器,例如闪存驱动器,外部硬盘驱动器和存储卡,可以通过" KnownFolders.RemovableDevices"找到.此集合返回的文件夹是附加驱动器的根目录.访问驱动器并不意味着访问驱动器上的所有文件.该应用程序将只能检测其注册类型的文件.如果应用程序尚未注册任何文件类型,则尝试枚举可移动驱动器上的文件将导致" ACCESS DENIED"异常.
List<istoragefolder> DriveList = new List<istoragefolder>();
foreach (var device in await KnownFolders.RemovableDevices.GetFoldersAsync())
{
DriveList.Add(device);
}</istoragefolder></istoragefolder>
使用SQLite的实体框架核心
随着2016 Windows Anniversary更新,SQLite 3.11.2版将发布. SQLite是一个轻量级的单用户数据库系统.它在进程内运行,因此无需为数据库服务器进行设置,也无需进行配置,并且将所有数据包含在一个文件中.可以使用NuGet将对SQLite的支持添加到项目中.要在Visual Studio中为项目添加支持,请打开"工具"菜单,选择" NuGet程序包管理器",然后选择"程序包控制台".要安装支持,请输入Install-Package EntityFramework.SQLite --Pre
.您还将需要可以通过键入Install-Package EntityFramework.Commands
-Pre`来安装的命令包.这里使用" -Pre"参数的原因是,在撰写本文时,这些参数均为预发布形式.这些的发行版本将很快发布.发布之后,我将写另一篇专门介绍使用SQLite的实体框架的文章,并在此处添加一个链接.
在UWP上,您可以将SQLite与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; }
}
}
会话将具有用作主键的" GUID".我已将此属性标记为具有属性的键.请注意,实体框架还将自动假定任何名为" 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 集合.查看作为表的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<location>() };
_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();
}
}
</location>
关于实体框架还有很多要说的.我的相关帖子将在2016 Windows Anniversary更新发布后的几周内发布.
结束语
如前所述,此版本将在Microsoft准备Windows Update时发布.更新后,可能还会有其他信息添加到此帖子中.请在Microsoft发布后的几周内进行检查,并在"历史记录"部分(以下)中查找由于更新而进行的添加的列表.
历史
- 2016年6月28日
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C# Windows Dev XAML Intermediate storage UWP 新闻 翻译