这个学期的课程要求开发一个 Todo 类的 UWP 应用程序,一路开发过来发现一堆坑,于是把一些重点记录下来。因为应用的大概框架已经成型,基本的功能就不再涉及,这篇文章主要涉及的是一些高级功能开发的一些重点。
🚀 代码: Github
2018/4/7 更新: 随机绑定磁贴图片的操作、即时搜索以及一些个人理解
2018/4/15 更新: 动态绑定磁贴图片的操作
首先这一阶段的要求是把程序挂起前的状态保存下来,然后再恢复,不如就一步到位先把数据库搞起来,很多东西就没有必要在做。所以第一步是先引入数据库,这里可以直接使用 SQLite 相关库直接用 SQL 语句操作数据库,这个比较简单。我这里想体验一下EF Core
, 所以就出现了以下一顿复杂的操作。
使用 EF Core 操作 SQLite
UWP 在 Windows Fall Creators Update SDK 中增加对 .NET Standard 2.0 的支持。因此可以在 UWP 中使用 Entity Framework Core 操作 SQLite,也可以运用 EF Core 强大的特性。
使用前提:
- Windows 10 Fall Creators Update (10.0.16299.0)
- .NET Core 2.0.0 SDK 或更高版本。
- 具有通用 Windows 平台开发工作负载的 Visual Studio 2017 15.4 版或更高版本。
按照文档中的步骤新建一个模型项目,然后安装两个包
1 | Install-Package Microsoft.EntityFrameworkCore.Sqlite |
注意 1:安装过程中必须要选择模型项目为目标项目,不然会找不到相对应的命名空间
注意 2:需要修改 Model 的 csproj 文件,官方的教程是没有下面那一项的
1 | <PropertyGroup> |
注意 3:新建模型项目的时候必须要选择 Net Standard 类库,不然后续步骤无法完成
由于 .NET Core 工具与 UWP 项目交互的方式受到限制,因此该模型需要放在非 UWP 项目中才能在包管理器控制台中运行迁移命令
注意 4: 还有一个比较坑的地方,官网是一带而过的,选择作为默认项目的模型项目并将其设置为启动项目。这个需要到项目属性里面设置(也可以在工具栏里面设置),不然是无法运行 Add-Migration MyFirstMigration
来搭建基架的。执行 Migration 命令,必须使 Model 项目为启动项,这是因为目前版本的 EF Core Tools 还不支持 UWP 这种类型的启动项目。(参考)
注意 5:需要在主项目添加引用,才能使用这个类库,而添加引用之前必须要修改 Model 的 csproj 为原来的情况。VS 是不允许 UWP 项目应用 netcoreapp2.0 的东西。你也可以选择在修改之前引用,因为后续的 Migration 还是可能需要 netcoreapp2.0 的。
1 | <TargetFrameworks>netstandard2.0</TargetFrameworks> |
注意 6: 而且官方的教程是没有为表设置 Key 的,不设置 Key 这个是无法迁移的。还有许多关于 EFcore 一些表的约束可以参考官方文档。但是我看大多数教程都是没有这个 Key 的,但是我测试是不能成功,并不知道为什么。所以这个还是需要我们自己去设置(参考)
1 | using System.Collections.Generic; |
如果更新了 Model 文件,那就需要再次 Add-Migration, 需要设置启动项目和默认项目为 Model 项目才能运行。
注意 7: Add-Migration 后面接的名字不能为Migration
, 不然会发生异常
注意 8: Add-Migration 的时候另一个项目必须是没有错误的,不然的话会 Build Failed
然后我们就可以在本来的工程里面使用数据库了。
在App.xmal.cs
里面
1 | public App() { |
然后在其他任何地方都可以调用了。
具体的增删改官方文档里面就写得非常详细了。(参考)
在具体使用的时候还是遇到了一些坑:
注意 9: 如果从一个空白的数据库使用 Single 或 First 方法,会导致严重错误导致整个 VS 都会崩溃(我也很绝望。因此,需要使用FirstOrDefault()
和SingleOrDefault()
, 当数据库为空的时候会返回Null
。(参考)
注意 10: 我尝试在Model
的DataContext
里面定义了两个不同名字但是同一类型的DbSet
, 但是很惊奇地发现这两个不同的名字只是同一个数据库的引用。很刺激,也不知道为什么。因此只好定义了两个不同数据类型的DbSet
。
不得不说,在UWP
中使用EF Core
真的是十分多坑,如果只是简单做做还是直接用SQL
语句比较快。🚀
在我的项目里面还有一点不太满意的地方,就是数据Model
与ViewModel
并没有分离开来,导致数据库与视图的耦合度非常地高,我也考虑过分离开来,但是这样一来操作数据的时候就需要做两次操作。有空还是需要重构一下数据模型这一部分的代码,设计一个更好的架构,或者参考一下别人的框架是如何实现的。现在的架构实在是不太优雅,但是最近比较忙,也没有空去搞了。😔
图片数据的保存
保存图片有不少方案,比如可以保存图片所在路径(可能需要获取相关权限并保存),也可以复制所选图片到 UWP 所可以访问的目录,而数据库就存他的路径(也可以使得图片根据数据库记录 ID 来命名)。第一个方法非常不保险,因为图片的路径可能会发生改变,那么重启应用之后就不能访问了,而第二种方法是可行的,但是我这里用了第三种方法,就是把图片数据存在数据库里面。
第二种方法和第三种方法之间有什么优劣,这个就关系到接下来的操作。
如果你使用的是第二种方法,那么对于磁贴动态绑定图标就非常方便,只需要简单修改磁贴 XML 的src
路径,但是在分享的时候,动态绑定图片就需要先把图片读取出来并转化为Stream
(其实还不算麻烦)。
如果你使用第三种方法,那么就方便与分享的动态绑定,而磁贴的动态绑定则需要先把图片数据保存为文件(这个管理起来就有点麻烦),再去修改src
。
当然,如果你有更好的方法欢迎在下面评论区讨论。
由于图片数据是不能直接放在数据库里面的,所以需要把图片转换为byte[]
类型。于是google
了两个函数。他们都是使用Stream
来把BitmapImage
转换为 byte[]
的。
1 | public class UtilTool { |
这两种方法都涉及到async/await
因此需要考虑到一些异步问题。
我在这里就遇到了一个问题。当应用从挂起状态恢复时候,List 还没有渲染完成,但是数据恢复已经开始执行,因此会造成了 List 的SelectedIndex
越界问题。这个就很烦,不能恢复选中的项目。这里先放下来,以后再去研究下解决方案。
挂起后状态的保存
在App.xmal.cs
里面修改OnSuspending
函数, 加入当整个应用挂起后所做的操作。这里用到了ApplicationData.Current.LocalSettings
, 这个是一个键-对的结构类型,可以用于存储一些数据量比较少的状态,因为他是有大小限制的,最多存储一些字符串。
1 | private void OnSuspending(object sender, SuspendingEventArgs e) { |
在App.xmal.cs
里面修改OnLaunched
函数,当程序加载时候做的操作
1 | if (e.PreviousExecutionState == ApplicationExecutionState.Terminated) { |
当然,我一开始就部署好了数据库,我们现在就可以通过数据库保存状态。
对于页面的状态,我们可以通过 override 一些函数,自定义页面加载时和关闭时的操作
1 | protected override void OnNavigatedTo(NavigationEventArgs e) { |
磁贴的操作
这里要求我们使用 XML 的形式开发磁贴,所以首先下载官方文档提供的一个制作器, 可视化制作磁贴。
我们可以参考里面的例子设计出自己的样式。
这个就比较简单了,设计完导出 xml。
我这里设计了三个不同大小的 Tile, 那个最小的我认为没有必要就不搞了
新建一个Tile.xml
, 内容为我们设计的 xml。
然后显示我们所设计的磁贴,下面是一个简单的例子。
1 | public static void AddTile(string title, string des, DateTimeOffset date) { |
这里有一个坑,对于XmlDocument
,它有两个命名空间都是有这个类型的,按Alt + Enter
引入的第一个命名空间并不是接下来所需要的,会得到类型错误的提示,我们需要引用另一个命名空间。
接下来需要循环显示, 这个比较简单,一句话就可以。
1 | TileUpdateManager.CreateTileUpdaterForApplication().EnableNotificationQueue(true); |
这个是一个 FIFO 的 Queue, 据说最多可以有 5 个瓷砖循环。
比较遗憾的是没有研究出如何绑定图片。
今天又研究了一下,根据断点调试输出的变量,研究了一下得到的XMLNode
的结构, 写出了两个设置和改变节点属性的函数。
1 | private static string GetAttriByName(IXmlNode nodes, string name) { |
但是我的图片都是以byte[]
类型存在数据库里面的,因此如果要动态绑定到 Tile 上,还需要重新保存在本地,还是比较麻烦,因此只好使用了一些预先存在Assert
文件夹里面的一些图片。
1 | XmlNodeList imageElements = document.GetElementsByTagName("image"); |
更新: 动态绑定项目的图片的主要难点在于如何有效管理和生成本地图片,因为目前我只发现绑定本地图片路径的方法。首先这个图片必须是应用有权限访问的,因此我把它存在本地目录下。下面这个参数可以将图片的byte[]
转换为本地文件
1 | private static async Task<StorageFile> AsStorageFile(byte[] byteArray, string fileName) { |
然后管理方案我使用了循环数的方法,这是一个非常简陋的方法,一方面考虑到不能占用太多空间,时间戳大概是不行的,因为你很难清理以前的图片,其实还可以使用这个项目的唯一 id 标识,不过清理起来还是有点麻烦。这里使用循环数字可以自动覆盖掉之前的图片,始终占用不超过指定的图片数。因为磁贴队列最多也是 5 个。
1 | private static int count = 0; |
然后就可以动态绑定图片进去里面了。
1 |
|
不过比较遗憾的是,每次操作都要重新生成 5 张或以上的图片,性能的代价比较大。如果使用记录的 ID 管理起来应该就比较方便了,最好还是生成图片的 Hash,可以判断是否存在再生成。
分享功能
首先在 list 右边加一个 button
1 | <AppBarButton x:Name="SettingButton" |
共享功能主要参考这篇文档
然后主要代码如下:
1 | // 当前分享的内容 |
ConvertTo
可以将保存将byte
数据转换成InMemoryRandomAccessStream
, 然后可以动态分享图片了。是不是很简单呢。
搜索功能
在xmal
里面, 使用了AutoSuggestBox
这个强大的控件,如果再添加几个事件还可以实现实时查找。
1 | <AutoSuggestBox PlaceholderText="Search" QueryIcon="Find" Width="300" |
至于搜索的功能简单地运用 SQL 就可以完成。
当然,我这里使用了 EF Core, 因此可以使用LINQ
很方便地进行数据库操作。
1 | private void QueryItem(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) { |
然后添加一下TextChange
事件还能实现即时搜索。
1 | private void TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) { |
然后,这样这周的要求就基本实现了。
最后的效果图