编程 Windows 10 桌面应用:UWP 焦点(N 篇之第 11 篇)
UWP 入门(从 WinForm 迁移)第 11 章 LoadFileFromStorage - 完成本章更改后,您将拥有一个能够保存所有条目并从文件中加载它们的应用程序。
- 下载 DailyJournal_v019.zip - 125.4 KB
- 下载 DailyJournal_v020.zip - 125.6 KB
- 下载 DailyJournal_v021.zip - 125.7 KB
- 下载 DailyJournal_v022.zip - 125.8 KB
- 下载 DailyJournal_v023.zip - 125.8 KB
引言
这是继续探索通过 UWP(通用 Windows 平台)开发桌面应用程序可行性的旅程。您可以阅读前面的章节,以便了解我们开发 DailyJournal 应用程序的进度。
编程 Windows 10:UWP 焦点(N 篇之第 1 篇)[^]
编程 Windows 10:UWP 焦点(N 篇之第 2 篇)[^]
编程 Windows 10:UWP 焦点(N 篇之第 3 篇)[^]
编程 Windows 10 桌面应用:UWP 焦点(N 篇之第 4 篇)[^]
编程 Windows 10 桌面应用:UWP 焦点(N 篇之第 5 篇)[^]
编程 Windows 10 桌面应用:UWP 焦点(N 篇之第 6 篇)[^]
编程 Windows 10 桌面应用:UWP 焦点(N 篇之第 7 篇)[^]
编程 Windows 10 桌面应用:UWP 焦点(N 篇之第 8 篇)[^]
编程 Windows 10 桌面应用:UWP 焦点(N 篇之第 9 篇)[^]
编程 Windows 10 桌面应用:UWP 焦点(N 篇之第 10 篇)[^]
您还可以从亚马逊购买纸质书或 Kindle 版,阅读该书的前 8 章
通过 UWP 编程 Windows 10:学习为桌面编写通用 Windows 应用程序(编程 Win10) [^]
背景
如果您从本章开始,请获取 DailyJournal_v019
源代码(在前一章创建),以便您可以继续进行本章中的更改。
文件名格式已解决
现在我们已经处理好 FileName
格式,我们可以编写代码,在每次选择日期时加载条目。
这项工作将在我们的 LoadEntriesByDate()
方法中完成,该方法目前看起来像
当我们重命名并重新格式化 YMDDate
值时,我们实际上放弃了 YMFolder
值,因为我认为我们在 MainPage.xaml.cs
上不需要它。然而,我发现在这里的 LoadEntriesByDate()
中,我们实际上错误地引用了 YMDDate
(2017-12-02),而我们真正只需要 Y-M
文件夹 (2017-12)。
两种修复方法
我们有两种方法可以解决这个问题,思考我们可能会选择哪种方法是有益的。
-
我们可以只在
LoadEntriesByDate
方法中创建一个局部变量,并对YMDDate
调用Substring()
,然后在此处设置局部变量供我们使用,然后就完成了。 -
我们可以将
YMFolder
成员变量添加到类的顶部,并在每次初始化YMDDate
时(在CalendarView_SelectedDatesChanged
中)设置该值。
选择几乎是任意的。然而,有几个问题需要您问自己,这可能会导致您做出一个或另一个决定。
1. 该值是否会在类的其他地方使用?
目前,我们没有看到该值在其他地方使用,但它可能会被使用。
2. 如果其他开发人员将来找不到该变量及其初始化位置,是否会感到困惑?
我认为在这种情况下,由于我们没有在类的其他地方使用该值,我们只需将其作为局部变量添加即可。
我们现在就这样做。
这是新代码行和使用新局部变量作为最后一个参数的修改后的 Directory.Exists
调用。
String ymfolder = YMDDate.Substring(0,7);
if (Directory.Exists(Path.Combine(appHomeFolder.Path, ymfolder)))
如果目录存在,我们将保证至少有一个条目。我们将通过决定以下几点来保证这一点:
-
仅当为特定日期创建初始
JournalEntry
时才创建新的YM
目录。除非将文件保存到目录中,否则不应创建YM
目录。 -
当
YM
目录存在并且从中删除了最后一个现有JournalEntry
时,YM
目录也必须被删除。即使我们还没有编写JournalEntry
删除代码,我们已经开始设计它的工作方式,因为它间接与加载条目代码的工作方式有关。
考虑到这两点,我们知道如果目录存在,其中至少会有一个 JournalEntry
。
预先计划就是设计
预先计划您希望代码执行的操作总是一个好主意。
以下是总结。
我们现在将编写的代码将
-
获取所选日期的所有日记条目文件列表
-
从每个文件创建一个新的
JournalEntry
-
将每个
JournalEntry
添加到currentJounalEntries
集合中。 -
将用户界面元素(XAML 元素、PivotItem 和关联的
RichEditBox
)添加到页面中,以便它们将显示。 -
将适当的
RichEditBox.Document
设置为每个日记条目文件中找到的数据。
这与用户点击“新建条目”按钮创建新 JournalEntry
(CreateNewEntryButton_Click
)时的操作非常相似。
Directory.GetFiles()
我们需要获取 Y-M
文件夹中的所有文件,我们知道该文件夹的路径是 appHomeFolder.Path
和我们的 ymfolder 局部变量的组合。
我们在 JournalEntry
类中也使用了 Directory.GetFiles()
来获取文件,但我认为我在详细解释方面有所疏忽。
Directory.GetFiles
是 .NET 库(在 System.IO
中找到)为我们提供的另一个方法。
我们使用此方法时,它接受三个参数,并返回一个字符串数组,表示在目录中找到的文件列表。参数如下:
-
您要查找文件的目录的完整路径
-
您要搜索的文件模式(可以是 *.* 等)
-
一个
SearchOption
枚举值,它是两个值之一(TopDirectoryOnly
或AllDirectories
)
为了正确设置参数,我们使用 Path.Combine 为第一个参数创建搜索路径。
对于第二个参数,我们使用 String.Format() 方法提供基于当前 YMDDate
的模式。这是因为我们只想要用户选择的特定日期的文件。我们的 String.Format
调用如下:
String.Format("{0}*.rtf", YMDDate)
这将确保 Directory.GetFiles()
只获取与我们当前的 YMDDate
匹配但与任何文件编号值匹配的文件。这样我们就可以获得所选日期的所有文件列表。
最后,对于最后一个参数,我们将其设置为只搜索 TopDirectory
,因为我们没有要搜索的子目录。
我们提供一个局部字符串数组,该方法将使用它返回所有文件名。
String[] allCurrentFiles = Directory.GetFiles(
Path.Combine(appHomeFolder.Path, ymfolder),
String.Format("{0}*.rtf", YMDDate),
SearchOption.TopDirectoryOnly);
一旦 Directory.GetFiles()
返回,allCurrentFiles
的 Length
将为 0(如果未找到文件)或大于零,以匹配找到的文件数量。
文件命名方案奏效
我们的 JournalEntry
文件命名方案的优点现在更加明显。
因为我们将文件命名为 YMDDATE-NUMBER
,并且由于文件系统自动排序文件名的方式,我们将按数字顺序获取它们,这意味着 001 将在 002 之前添加到我们的 allCurrentFiles
中,等等。它们自然地为我们排序,因为我们的文件命名方案很好。这是一个很好的好处。
这使得通过简单检查 allCurrentFiles
字符串数组的 Length
属性来确定目录中是否有任何文件变得容易。
当然,在我们的代码中,这个值必须始终大于 0,因为我们保证如果 ymfolder
存在,那么它将至少包含一个文件。
我们仍然会检查 Length
值,以确保目录列表的正确执行。
if (allCurrentFiles.Length > 0)
{
}
显示条目的代码将放在该 if
语句中。
现在,我们只想逐个遍历文件列表。正如我们在前面章节中看到的,C# 的 foreach
结构使得这非常容易。
foreach (String f in allCurrentFiles)
{
}
非常有趣的是,在那个 foreach 循环中,我们需要执行的功能与我们在 CreateNewEntryButton_Click
方法中执行的功能非常相似。
然而,它略有不同,因为我们需要将现有文件数据加载到 RichEditBox
中。为了继续,我将
-
复制
CreateNewEntryButton_Click
方法中的代码并运行它,然后检查它做了什么。 -
重构出不同的部分
-
尝试创建一个方法,让两个方法(
CreateNewEntryButton_Click
和LoadEntriesByDate
)都可以调用。
我们把代码复制进去。我只是把 CreateNewEntryButton_Click
方法中的所有行都复制粘贴到 foreach 循环中。
PivotItem pi = new PivotItem();
var entryText = String.Format("Entry{0}", rootPivot.Items.Count + 1);
pi.Header = entryText;
RichEditBox reb = new RichEditBox();
reb.HorizontalAlignment = HorizontalAlignment.Stretch;
reb.VerticalAlignment = VerticalAlignment.Stretch;
currentJournalEntries.Add(
new JournalEntry(reb,
Path.Combine(appHomeFolder.Path),
YMDDate, entryText));
pi.Content = reb;
pi.Loaded += PivotItem_Loaded;
rootPivot.Items.Add(pi);
rootPivot.SelectedIndex = rootPivot.Items.Count - 1;
这会运行,并且几乎正常工作。
获取代码,构建并运行
获取 DailyJournal_v020
并构建并运行它。
您将看到代码运行良好,如果您已为特定日期创建了多个条目文件,您将看到为每个条目文件创建了一个新的 PivotItem
和关联的 RichEditBox
。
然而,问题在于它创建的 PivotItem
比您实际拥有的条目数量多了一个。这是因为在 CreateNewEntryButton_Click
中,我们假设您正在添加一个到任何当前创建的条目中,并且您肯定至少有一个已创建的条目(默认)。
这是一个有点不正确的假设,因为用户实际上可以点击[添加]按钮,即使她还没有在默认(Entry1
)条目中保存任何内容。
那是我们还需要解决的另一个问题。然而,让我们继续沿着这条路走下去,解决当前的问题:UI 中多添加了一个 PivotItem
的问题。
问题发生的原因
问题实际上发生是因为我们需要将找到的第一个条目文件加载到 Entry1 PivotItem
中,该 PivotItem
最初将通过 MainPage.xaml
的原始创建的 XAML 加载。
找到的第一个条目文件处理方式不同
这意味着对于我们找到的第一个条目文件,我们不需要添加 PivotItem
,而是将其加载到现有的条目中。
迭代和数组
这使我们了解到,对于 allCurrentFiles
数组中的第一个项(第一个条目文件),我们需要做一些不同的事情。然而,我们正在使用 foreach 迭代器遍历文件,并且实际上不知道我们当前在哪个文件索引上。
第一个可能的解决方案
我们可以添加一个 counter
,每次循环时递增,然后我们可以有一个特殊的 if
语句,只在 counter
等于 0 时(第一次通过)执行特殊代码。
但这听起来不太好,因为我们每次循环时都在递增和检查一个值,但我们只关心一次该值:当它为零时。
第二个可能的解决方案
我们还可以将 foreach
循环更改为 for()
循环。for
循环需要计数器,然后我们按计数器索引,还有许多其他工作要做,而 foreach
结构可以减轻这些工作。
我想保留 foreach,但这意味着我需要以某种方式
-
使用数组中的第一个值
-
删除数组中的第一个值,然后允许 foreach 循环迭代
网络搜索与研究
我搜索并找到了一种从数组中删除第一个元素的简单方法,因此我们可以使用数组对象提供的这个方法来完成这项工作。然而,一旦我们从数组中删除了该项,我们就无法将其取回,所以我们首先需要使用该项。
Skip() 方法
我们将实现的从数组中删除该项的代码行将使用 Skip()
方法。
最终的代码行将如下所示:
allCurrentFiles = allCurrentFiles.Skip(1).ToArray();
Skip()
方法接受一个参数,该参数指示要从数组开头跳过的元素数量。我们只想跳过第一个,所以我们传入值 1。
最后,在 Skip
方法返回后,我们对其返回的对象调用 ToArray()
,将其强制转换回字符串数组。
瞧!第一个项目不见了
然后我们取那个不再包含第一个项目的新数组,并将其重新分配回我们的原始数组变量。就像魔法一样,第一个项目消失了。
但是,我们首先需要使用该项目。所以现在我们编写代码,它将使用第一个文件来获取数据,然后我们将数据加载到 MainRichEdit Document
中。
从文件加载
一旦我们开始处理存储,我们知道程序中可能会有延迟,因此 .NET 库方法总是异步运行,这意味着我们的 LoadByEntriesDate()
方法现在也必须标记为异步,因为它将调用异步方法。
您现在可以更改方法签名,这样 Visual Studio 就不会抱怨我们了。
private async void LoadEntriesByDate()
我们需要做几件事来读取现有条目文件。
-
获取
appHomeFolder
的子文件夹(出于安全原因,Microsoft 不允许您直接访问这些特殊文件夹。我们必须获取应用程序文件夹,然后使用文件夹名称调用GetFolderAsync
。) -
异步获取文件的句柄,我们将使用它来打开流进行读取。
-
异步打开文件流进行读取
-
将
RichEditBox Document
加载到文件流中的数据。 -
确保文件再次关闭(
Dispose()
文件流对象)
最后,打开文件后,我们将调用 Skip()
方法,以确保第一个文件从 allCurrentFiles 中删除。
以下是与这些行完全匹配的代码
StorageFolder subStorage = await appHomeFolder.GetFolderAsync(ymfolder); Windows.Storage.StorageFile currentFile = await subStorage.GetFileAsync(Path.GetFileName(allCurrentFiles[0])); Stream s = await currentFile.OpenStreamForReadAsync(); MainRichEdit.Document.LoadFromStream(Windows.UI.Text.TextSetOptions.FormatRtf, s.AsRandomAccessStream()); s.Dispose(); allCurrentFiles = allCurrentFiles.Skip(1).ToArray();
Path.GetFileName()
请注意静态库方法 (来自 System.IO) Path.GetFileName()
。它只返回路径中的文件名。这在我们的 GetFilAsync()
调用中对我们很重要,因为它不允许我们提供的文件名中包含任何路径信息。所有路径信息都必须像我们之前讨论的那样设置在 FileStorage
项上。
每当您运行 Directory.GetFiles()
时,它返回的每个字符串都将包含完整路径和文件名。在这种情况下,我们只想要文件名,因此我们实现了 GetFileName()
方法,以便轻松获取该值。
构建、运行、测试
如果您正在跟着做,运行应用程序并查看。或者,获取 DailyJournal_v021
源代码并构建和运行。
第一个条目已加载
现在,只要找到与选定日期匹配的有效条目文件,第一个条目就会被加载。
当然,您必须为您选择的日期创建一个有效的条目。而且,如果您有多个条目,则只会加载第一个。这是因为我们还没有为 allCurrentFiles
中的所有其他文件实现代码。
bug,bug,更多的 bug
我还注意到,当我尝试保存 Entry1
项时,文件未创建。看起来您必须创建一个新条目,然后它才会保存该条目。 当然,这也需要修复。
GenerateFileName() 问题
我们还有一个问题,就是当前条目无法保存回正确的文件。我逐步调试代码后发现,这是因为每次我们加载条目并调用 JournalEntry
构造函数时,代码实际上都会调用 GenerateFileName()
。
然而,当我们从存储加载文件时,我们已经有了文件名,不需要生成一个。
我们需要更改 JournalEntry
构造函数,以便它能够正确处理此问题。
我希望它在创建新条目时生成文件名,并在提供了文件名时使用传入的文件名。我们可以通过添加另一个具有默认值 null
的参数来实现这一点。
为了进行此更改,我们将
-
向构造函数签名添加另一个名为 FileName 的参数,并将其设置为 null。
-
添加代码,该代码确定 FileName 参数的值是否为 null,并采取适当的行动。
这是新 JournalEntry
构造函数的完整列表
public JournalEntry(RichEditBox richEditBox,
String AppHomeFolderPath,
String YMDDate,
String EntryHeader,
String FileName = null)
{
_richEditBox = richEditBox;
this.AppHomeFolderPath = AppHomeFolderPath;
this.YMDDate = YMDDate;
this.YMFolder = YMDDate.Substring(0, 7);
this.EntryHeader = EntryHeader;
if (FileName == null)
{
GenerateFileName();
}
else
{
this.FileName = FileName;
}
}
现在,所有其他代码将正常工作,因为参数默认设置为 null,并且 GenerateFileName()
方法仍将运行。然而,我们现在可以简单地更改 LoadEntriesByDate()
方法中的 JournalEntry
构造函数,这个问题就会得到解决。
添加到 currentJournalEntries
但这暴露出导致问题的另一个 Bug。我们目前没有将加载的条目添加到 currentJournalEntries
集合中。由于 MainPage.xaml.cs
与集合类协作,它不知道哪个条目是当前选定的条目,因为我们没有正确更新协作类(currentJournalEntries
)。
让我们现在就解决这个问题,我们将确保新的 JournalEntry 的构造函数传入文件名。
当我们创建新的 JournalEntry 时,我们添加新的文件名参数值 (Path.GetFileName(allCurrentFiles[0])
),这将是添加到我们的 currentJournalEntries
集合中的 JournalEntry
。
currentJournalEntries.Add(
new JournalEntry(MainRichEdit, appHomeFolder.Path,
YMDDate, "Entry1",
Path.GetFileName(allCurrentFiles[0])));
这解决了我们目前讨论的所有问题,除了一个。
如果您点击某个月中没有条目但该月确实包含条目的一天,那么 AddDefaultEntry()
方法将不会像预期那样被调用。这是因为对于 Y-M
目录存在(因为当前月份有文件但其他日期没有)但用户点击的日期没有当前条目文件的情况,我们需要一个 else 情况。
我们只需在看起来像这样的 if 语句中添加一个带有对 AddDefaultEntry()
的调用的 else 语句
if (allCurrentFiles.Length > 0)
这是应该添加的整个 else 语句
else
{
AddDefaultEntry();
}
构建、运行、测试
同样,您可以构建代码并尝试一下。现在您可以打开或创建 Entry1,并在您最初保存或更新它时正确保存。
它现在开始像一个真正的应用程序了。
您可以获取 DailyJournal_v022
源代码并构建运行以尝试。
仅从文件加载第一个条目
当然,只加载第一个条目。我们需要为每个其他条目做同样的工作。但是,如果我们只是简单地将代码复制/粘贴到 foreach 块中,那么我们就会有重复的代码,可能会导致错误。如果它确实出现错误,那将意味着有人必须理解他们将不得不在两个不同的地方修复代码。
这就是为什么我们希望将这段代码移到它自己的私有方法中,这样我们就可以从多个地方调用它,但又知道代码只存在于一个地方,以便扩展维护。
由于这段代码基本上是从存储中加载条目文件,我将把新方法命名为 LoadFileFromStorage()
。
新方法:LoadFileFromStorage
我们基本上会
-
将完成该工作的行从它们当前的位置剪切出来
-
将它们粘贴到新方法中。
-
之后,我们将确保添加方法所需的必要参数
-
然后我们将在需要代码运行的两个位置添加对该方法的调用。
以下是将要移到新方法中的行:
现在我已经创建了基本的方法块并粘贴了这些行。但是您可以看到存在一些问题,因为 Visual Studio 正在显示一些红色波浪线。
代码中确实存在三个问题,但 Visual Studio 只将其中两个视为问题
-
ymfolder
在此方法的范围内未定义(它是LoadEntriesByDate
的局部变量) -
allCurrentFiles
字符串数组也未在此方法的范围内定义(它也是 LoadEntriesByDate 的局部变量)。 -
MainRichEdit
是可访问的,但只有在 Entry1 显示时才正确。在其他生成的条目上,我们需要生成的RichEditBox
。
所有这些问题都可以通过传入参数轻松解决。
让我们修改方法签名以接受我们需要的参数,这样我们就可以传入值*。
注意:值在这里不太正确,因为我们还传入了一个对象引用(RichEditBox
)。
这是新的方法签名,它将解决所有三个问题。
但是,我们还需要更改方法代码中的两件事
-
参数从
GetFileAsync(Path.GetFileName(allCurrentFiles[0])
到entryFileName
参数 -
对
MainRichEdit.Document
... 的引用变为reb.Document
...
private async void LoadFileFromStorage(String ymfolder,
String entryFileName,
RichEditBox reb)
现在修改后的代码看起来像这样:
我将第一个参数 ymfolder
命名与调用方法中的名称完全相同。这样做只是为了简化,这样我就不必更改访问该值的代码行。
第二个参数名为 entryFileName,试图表明我们只希望传入文件名。
现在,我们需要在 LoadEntriesByDate()
中的两个位置添加对此方法的调用。
第一次调用 LoadFileFromStorage
第一次调用将直接放在先前代码块所在的位置。
LoadFileFromStorage(ymfolder,
System.IO.Path.GetFileName(allCurrentFiles[0]), MainRichEdit);
您可以看到,我们在调用中剥离了所有路径信息,以便接收方法只获取文件名本身。
此外,在这种情况下,我们传入 MainRichEdit RichEditBox
,因为这是第一个条目。
第二次调用 LoadFileFromStorage
我们将在 foreach 循环中,在创建名为 reb
的 RichEditBox
之后(因为我们需要将其作为引用发送到 LoadFileFromStorage
方法)放置对 LoadFileFromStorage
的第二次调用。
LoadFileFromStorage(ymfolder, System.IO.Path.GetFileName(f), reb);
在这种情况下,我们再次引用 ymfolder
,并从我们的临时 foreach 变量 (f) 中剥离路径信息,该变量表示条目文件名。
当然,我们传入新创建的 reb,以便将适当的条目文档填充来自条目文件的数据。
就这样。
构建并运行:加载所有现有条目
构建并运行应用程序并试用。
获取 DailyJournal_v023
并构建并运行它。
Github 仓库中的源代码
您还可以在 Github 仓库中获取所有源代码:https://github.com/raddevus/Win10UWP
你现在可以
-
保存任何日期的一个或多个条目
-
无论何时选择日期,应用程序都会重新加载所有条目和相关数据。
两个简单的测试场景
-
如果您在 2017-12-22 创建了 8 个条目并点击该日期,所有条目都将加载,并且每个条目都将加载其关联数据。
-
然后您可以点击每个条目并查看相关文档。
-
-
如果您点击一个不包含条目的日期,您将看到空的默认条目。
-
如果您添加数据并保存,新文件将被保存,以便如果您切换到另一个日期再切换回来,关联的条目文件将加载到
RichEditBox
中。
-
应用程序崩溃
我在使用应用程序时注意到应用程序崩溃了几次,但很难确定原因。我在调试模式下运行它,并点击周围,更改日期。我最终注意到应用程序会在 MainCalendar_SelectedDatesChanged 事件处理程序中的以下行崩溃:
YMDDate = MainCalendar.SelectedDates[0].ToString("yyyy-MM-dd");
非常奇怪的行为
由于某种原因,控件的 SelectedDatesChanged
事件触发了,但它认为根本没有选择日期,所以 SelectedDates[0]
是 null
。就好像控件认为您点击了控件,但介于日期之间或其他什么。不确定。然而,尝试对空对象调用 ToString()
显然是未定义的,应用程序会抛出异常或崩溃。
我在代码中修复了这个问题 通过检查该值是否为 通过检查 null
SelectedDates
对象的 Count
属性。
<s>if (MainCalendar.SelectedDates[0] != null)</s>
文章编辑 - 上一行代码应为
发布此文章后,我了解到之前的修复不太正确。我更新了附件压缩包 (v023) 和 Github 仓库中的源代码。它实际上应该检查 Count > 0 -- 检查 null 仍然会抛出错误并导致应用程序崩溃。
if (MainCalendar.SelectedDates.Count > 0)
如果您发现点击日期时,应用程序似乎忽略了您在 MainCalendar
上的点击,那可能是因为它认为 SelectedDates[0]
为 null
。这很奇怪,我会进一步研究。
下次
我们几乎完成了基本应用程序,但是,我们从未更新 ListView
控件的代码。ListView
应该提供包含条目的日期列表以及每个日期存在的条目数量。
这是我旧版 WinForms 版本的快照
这允许用户点击月份,查看哪些日期关联有条目,然后通过点击 ListView 项目来选择它们(移动到该条目)。
历史
2017-12-07: 首次发布