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

编程 Windows 10 桌面:UWP 焦点(10/N)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2017 年 12 月 5 日

CPOL

22分钟阅读

viewsIcon

14498

downloadIcon

496

UWP 入门(摆脱 WinForm)第 10 章 重构有时意味着修复错误——让 DailyJournal 文件名格式正常工作。

引言

在深入探讨通过 UWP 开发应用程序的可行性之前,有九章内容。请查看之前的文章,以了解我们正在构建的 DailyJournal 应用程序的最新进展,我将为您提供具体的细节和大量的屏幕截图。


编程 Windows 10:UWP 焦点(1/N)[^]
编程 Windows 10:UWP 焦点(2/N)[^]
编程 Windows 10:UWP 焦点(3/N)[^]
编程 Windows 10 桌面:UWP 焦点(4/N)[^]
编程 Windows 10 桌面:UWP 焦点(5/N)[^]
编程 Windows 10 桌面:UWP 焦点(6/N)[^]
编程 Windows 10 桌面:UWP 焦点(7/N)[^]
编程 Windows 10 桌面:UWP 焦点(8/N)[^]
编程 Windows 10 桌面:UWP 焦点(9/N)[^]

您也可以在亚马逊上阅读这本书的前 8 章的印刷版或 Kindle 版。

通过 UWP 编程 Windows 10:学习为桌面编程通用 Windows 应用 (Program Win10) [^]

 

背景

在上一个章节结束时,我们的应用程序中存在一些问题(错误)(请参阅上一章的“其他问题:应用程序中的错误”部分,了解列表)。

那里列出的第一个项目描述了Entry1条目永远不会保存的问题。让我们先尝试解决这个问题。

如果您需要获取代码以便从本章开始,请获取DailyJournal_v017.zip

为什么 Entry1 不保存?

了解为什么Entry1不保存有助于我们解决问题。它从未被保存,因为它从未添加到 JournalEntries 集合中。由于它从未添加,当用户单击日历日期时,它永远无法在该列表中找到。

空白 Entry1 何时出现?

一旦我们添加了加载以前保存的条目的功能,空白的Entry1将只在用户选择的日期没有条目时出现。如果有任何条目(文件),它们将按顺序加载,并且 Entry 标题将从 1 开始递增。

这意味着我们将来只有在当天没有创建其他条目时才会有空白条目。这段代码将在LoadEntriesByDate() 的else语句中运行——这是我们在上一章中编写但未完全填充的方法。

添加默认条目

我将把空的Entry1称为默认条目,所以我将把执行该工作的方法命名为AddDefaultEntry()。

现在我们来编写这个方法。

AddDefaultEntry 方法

打开MainPage.xaml.cs文件,向下移动到LoadEntriesByDate() 方法。

我们将在代码的 else 部分工作,因为我们只希望我们的代码在某个日期没有条目时运行。

我将把对该方法的调用添加到 else 块中,尽管我们还没有编写它。

AddDefaultEntry();

 

现在,让我们在文件中的LoadEntriesByDate() 方法之后打开一些空间,并编写新方法。

方法中的代码只有一行

private void AddDefaultEntry()

{

   currentJournalEntries.Add(new JournalEntry(MainRichEdit, appHomeFolder.Path,

              YMFolder, "Entry1"));

}

 

我们所要做的就是向currentJournalEntries集合添加一个新的JournalEntry

硬编码值:另一次嗅探测试

最后一个参数总是“Entry1”,所以我们已将其硬编码到调用中。这种硬编码实际上没有通过嗅探测试。它似乎有点异味,因为这里有些奇怪。

这并不是一个好的做法,因为这可能在您的应用程序中创建魔术字符串:否则,其他开发人员在查看您的代码时可能无法轻易知道其所依赖的内容。我们暂时保留这种方式,但稍后我们将讨论如何改进它。

请记住,LoadEntriesByDate() 方法在选定日期更改时都会被调用,因为我们已经为该事件实现了处理程序(MainCalendar_SelectedDatesChanged),该处理程序会调用LoadEntriesByDate()。

 

 

构建、运行,但不太正常

现在我们已经进行了这些更改,代码将构建并运行,但它不能完全按预期工作。在重构时,情况似乎总是如此。这是因为我发现了一个错误——这与对LoadEntriesByDate() 应该如何工作的错误理解有关。

if语句实际上是错误的。

现在,if 语句正在检查YMFolder是否存在。如果存在,它将加载所有文件。但是,if语句应该真正检查是否有任何文件与所选日期的模式匹配。然后,如果匹配,它应该加载任何文件。我们需要更改它,但我们还需要稍微讨论一下我们将用于文件名的模式。既然我们需要处理文件的命名方式,那么现在就来解决这段代码吧。

DailyJournal 文件命名如何工作?

DailyJournal 文件名的格式将使用以下模式

Y-M-D-N.rtf,其中

  • Y = 4 位年份

  • M = 2 位月份

  • D = 2 位日期

  • N = 3 位生成的条目号

  • 扩展名将始终是 rtf(富文本格式)

 

它看起来像:YYYY-MM-DD-NNN.rtf2017-12-03-001.rtf

 

每次创建新文件(首次保存)时,JournalEntry 类将生成一个匹配该模式的适当文件名。

 

我们来看看 JournalEntry 类及其构造函数,以确定它是否包含足够的信息在构造时生成文件名。

更多清理工作

当我查看 JournalEntry 类时,我立即发现了一些命名不当的成员变量,并想起上一章我们更改了一些东西的工作方式,但我们从未更改成员变量名。现在让我们来做这件事,我们还将看到 Visual Studio 如何使用其重构功能帮助我们重命名变量。

YMFullPath 应为 AppHomeFolderPath

YMFullPath 不再指向完整路径,因为我们实际上是单独传入 Y-M 文件夹。

让我们重命名它以表示它实际持有的值。

Visual Studio 重命名帮助

右键单击类顶部的成员变量,将出现一个菜单,其中包含[重命名...]选项。

 

 

选择[重命名...]选项,Visual Studio 将高亮显示该成员在文件中所有出现的位置,并显示一个辅助对话框,允许您重命名文本出现的位置,甚至在注释等地方。

 

 

一旦出现,只需输入新名称(AppHomeFolderPath),Visual Studio 就会开始更改所有这些名称。完成后,按<ENTER>或单击[应用]按钮,所有名称都将正确重命名。

这样做的好处是,当该变量在其他文件中也被引用时,您无需担心自己搜索每个文件。

 

 

如您所见,我们仍然有构造函数参数命名不正确,因此您可以自行重命名。我现在将我的重命名为与成员变量相同。

重命名后,最好重建一下,以确保所有内容都正确,并且没有引入不正确的变量或类似的东西。

现在变量名更合理了,我们可以回到添加代码以正确命名文件的工作了。

由于文件名末尾的计数器值,这将是一个有趣的问题。

 

是的,这又是另一次重构

然而,当我检查 JournalEntry 构造函数时,我注意到我们没有日期的日期部分。JournalEntry需要这个来正确创建文件名,所以我现在正在考虑构造函数参数YMFolder。它传入的是Y-M值。我现在可以添加另一个构造函数参数来传入日期值。这会更容易,重构也更少,但也会更随意。

 

我们真正需要做的是将格式化的Y-M-D字符串作为一个参数传入,然后让我们的JournalEntry用它来创建Y-M目录和文件上的日期值。事实上,如果我们传入完整的Y-M-D值,我们将拥有我们想要的文件名,并且我们只需要为文件编号添加 N 值。让我们再次重构JournalEntry构造函数,然后我们也将更改对构造函数的调用。我们还需要在MainPage类中更改另一件事,因为它存储了YMFolder,我们将把它更改为存储YMD值。

 

在我们开始这次重构之前,让我给您我刚刚更改的代码(本章前面),以便我们确保我们有相同的代码库可以开始。

下载DailyJournal_v018.zip并从那里开始。

 

添加新成员变量:YMDDate

  1. 我要做的第一件事是向 JournalEntry 添加一个新的私有成员变量,它是一个字符串,名为YMDDate

  2. 接下来,我将把 JournalEntry 构造函数中的参数 YMFolder 重命名为 YMDDate。

  3. 我将确保构造函数代码使用 YMDDate 参数初始化 YMDDate 成员。

  4. 最后,我将通过从构造函数中的 YMDDate 字符串参数获取值来设置我们旧成员变量 YMFolder 的值。我这样做是因为 YMFolder 仍然单独使用,我们需要将该值与 YMDDate 分开——用于存储文件的文件夹名称。

您可以在下面的代码列表中看到所有这些更改

 

Substring 方法

第 30 行是最有趣的一行。由于我们知道 YMDDate 将以何种格式传入,我们可以简单地通过调用内置的 String 方法 Substring() 来从中获取 Y-M。

 

Substring()在这种情况下需要两个参数。第一个参数是我们要开始的字符串中的索引。在我们的例子中,我们想从字符串的第一个字符开始,C# 中的数组索引从零开始,所以我们提供一个零。

 

第二个参数是您想从字符串中获取的长度(或字符数),从索引开始。我们想要 4 位年份的 4,1 位破折号的 1,2 位月份的 2,总共是 7 位。

我们将其设置为YMFolder成员变量,现在它将像以前一样设置。

 

现在如果构建会怎样?

代码会构建,但不对。代码仍然会构建,因为我们没有更改构造函数接受的参数数量或类型。以前,构造函数的第三个参数是字符串,现在它也是字符串,所以对编译器来说,没有任何改变。

然而,我们知道我们只传递了字符串的Y-M部分,它将来无法正常工作。

 

让我们去修改MainPage.xaml.cs中的代码来解决这个问题。

现在我将MainPage成员变量YMFolder重命名为YMDDate

此值主要用于我们构造新JournalEntry的每个地方。

但是,它目前也在MainCalendar_SelectedDatesChanged事件处理程序中初始化。

 

我们只需要稍微改变一下格式字符串,添加最后一个破折号和两位数的日期值。

这是我们将替换当前行的代码行

YMDDate = MainCalendar.SelectedDates[0].ToString("yyyy-MM-dd");

 

所有这些都能无错误地构建,并且它会像以前一样运行和工作,但是,有了这段代码,我们离正确生成新文件名更近了。

我没有忘记 LoadEntriesByDate()

请记住,我们仍然需要修复 MainPage 的 LoadEntriesByDate() 方法,因为它目前错误地检查 Directory.Exists(),但我没有忘记,我们会修复它。但再次强调,一旦我们重构 JournalEntry 以正确生成文件名,这项工作就会更容易,所以让我们继续沿着这条路径前进。

解决文件名生成问题

当 JournalEntry 构造时,我们还需要为该类生成一个文件名。这样,如果用户决定保存文件,Save() 方法将在将数据写入存储时使用该文件名。

文件名与文件不同

请注意,当我们实例化 JournalEntry 时,我们生成的是文件名,而不是文件。我们只有在用户执行保存操作时才在存储中创建实际文件。这里的重点是,当对象构造时,它已经生成了完成工作所需的一切。如果用户不保存文件,文件名可能永远不会被使用,这没关系。

文件名将仅在JournalEntry类中使用,因此我们将它添加为新的private成员变量。

这是将其添加为成员的一行代码

private String FileName;

 

另请注意,我们无需将此项添加到构造函数中,因为它将由JournalEntry类本身生成。

由于生成文件名的代码不止一两行,我将把它分离到一个名为GenerateFileName()的私有方法中,并将其调用作为JournalEntry构造函数中的最后一行。

存根方法

我通常像这样存根我的方法,只是为了让代码运行起来,但仍然可以正常构建,因为没有语法错误或其他错误。“存根”只是指添加基本方法声明,即使它不包含任何代码。

 

 

我们的GenerateFileName方法需要:

  1. 确定特定日期是否有任何现有文件。

  2. 如果当前日期没有文件,则会将 FileName 成员变量末尾的文件号设置为 001。

  3. 如果当前日期有文件,则需要获取最大 N 值,然后将其加一,并将值设置为 FileName 末尾的 00X 格式。(这不是完美的算法,但对我们来说会起作用)

 

这是我为当天尚未创建文件的情况编写的代码。

 

          string targetDirectory = Path.Combine(AppHomeFolderPath, YMFolder);

           if (Directory.Exists(targetDirectory)){

               String[] allCurrentFiles = Directory.GetFiles(targetDirectory,

                   String.Format("{0}*.rtf", YMDDate), SearchOption.TopDirectoryOnly);

               if (allCurrentFiles.Length <= 0)

               {

                   FileName = String.Format("{0}-{1}.rtf", YMDDate, 1.ToString("000"));

               }

               else

               {


               }

           }

           else

           {

               FileName = String.Format("{0}-{1}.rtf", YMDDate, 1.ToString("000"));

           }

 

让我们更改 Save() 方法,使其引用 FileName 成员而不是硬编码的值 (FirstRichEdit.rtf)。

 

当前的 Save() 方法看起来像这样

 

我们只需更改该参数以引用我们的新成员变量。

 

发现 Bug 是重构的一部分

还有一个 bug,这次是在 Save() 方法中。当我们重命名 YMFolder 项时,我们不小心重命名了两个应该保留为 YMFolder 的 YMFolder 变量。

第 62 行的 YMDDate 实际上应该是 YMFolder,第 64 行和第 66 行的 YMDDate 也应该改为 YMFolder。这是因为我们必须在 localstorage 文件夹下创建 Y-M 文件夹的方式。这是更新后的代码,您可以看到我已重命名了这两个项。

 

应用程序崩溃的原因并非总是显而易见

此时,我构建并运行了应用程序,测试它是否能正确保存文件。在某些情况下可以,但由于我们的 GenerateFileName() 方法中有一个空的else语句,因此有时应用程序会崩溃。

当 Y-M 目录存在且目录中有文件时,它会崩溃。如果发生这种情况,JournalEntry FileName 将不会被设置(它将为 null)。然后,当您去保存关联文件时,由于它实际上尚未设置,应用程序将崩溃,因为它无法创建 null 文件。

 

仔细想想,这说得通,但当你运行代码并它崩溃时,这并不明显。

 

让我们通过在else语句中填充我们需要的代码来解决这个问题,以便在特定选定日期已经创建条目时生成有效的 FileName。

算法假设

该算法将假设条目编号从 1 到 N,序列中没有缺失值。问题在于当用户从一组现有条目中删除特定条目时。这将在我们的序列中留下一个空白。但是,我现在决定,当用户删除条目时,删除操作也将重新编号文件号(重命名它们),以便文件号序列中不会有缺失值。

这个假设对我们现在意味着什么?

这意味着我们只需获取文件列表(我们已经有了),获取最大 N 值,然后将其加一作为我们将在文件名中使用的新文件号。

 

考虑到所有这些,这是我们将用于递增文件号的代码。我们需要将此代码放在GenerateFileName()方法的else语句中。

增加文件号

List<short> fileNumbers = new List<short>();

foreach (string f in allCurrentFiles)

{

    String fileName = System.IO.Path.GetFileNameWithoutExtension(f);

    fileNumbers.Add(Convert.ToInt16(fileName.Substring(fileName.Length - 3, 3)));

}

int maxValue = fileNumbers.Max<short>();

maxValue++;

FileName = String.Format("{0}-{1}.rtf", YMDDate, maxValue.ToString("000"));

 

让我们逐行检查。


 

第 49 行创建了一个新的本地List<short>short类型的List),我们将用它来存储文件号。

什么是 Short?

short类型是 C# 中的一个别名关键字,它是定义Int16类型的另一种方式。这意味着它是一个两字节整数(16 位)。Short 是一种长期以来用于表示整数类型小于基本整数类型的约定。还有一个 Long 类型,它是一个 64 位(8 字节)整数大小。当然,short 的最大值更小,因为它只有 16 位来存储值。由于 short 是有符号整数(可以包含正值和负值),因此它可以存储的最大值为 32767。

这意味着我们将特定Y-M目录中的文件数量限制为 32767。这应该没问题,尤其是考虑到 Windows 和文件资源管理器可能也难以处理目录中如此多的文件。

第 50 行,您会看到我们使用 C# foreach 构造来遍历列表。我们所要做的就是提供一个类型(在这种情况下是字符串)和一个临时变量 (f),列表中的每个值都会在每次循环中存储到其中供我们使用。

System.IO.Path

第 52 行,我们将该值作为参数传递给 GetFileNameWithoutExtension() 方法。

该方法是 System.IO.Path 库中的另一个库方法,由 .NET 为我们提供。其中包含许多与路径、文件、目录等操作相关的有用方法。

这段代码的摘要

在我们的例子中,我们只想要文件名而不要扩展名,因为我们真正想要做的是获取每个文件的当前文件号值并将其添加到List<>中。我们将其添加到List<>中,因为List<> 类型提供了一个名为Max<>()的方法,它将确定其中包含的最大值并将其返回给我们。这将使我们的代码非常简单。

我有点超前于我们的代码,但这是一个很好的总结,说明了我们正在做什么。

 

第 52 行运行后,fileName 将填充已创建的文件名之一(不含扩展名)。

所以文件名可能看起来像:2017-12-06-001

第 53 行实际上包含 3 个代码语句(或操作),并且再次涉及操作顺序。当然,最右侧的方法调用首先运行,因为它在最内层的括号中。在这种情况下,该行按顺序执行以下三件事

  1. 调用 String 库的Substring方法,对fileName变量获取字符串的特定部分

  2. 调用库方法Convert.ToInt16()将我们传递给它的字符串值转换为有效的Int16short值)

  3. 最后,我们将转换后的值添加到我们的 short 值列表中。

子字符串

当我们对文件名调用 Substring 时,它从我们在第一个参数中提供的索引值开始。在这种情况下,我们提供值 fileName.Length - 3。fileName.Length 返回字符串的长度,作为字符计数。在我们的例子中,返回的值将是 14,因为我们的文件名始终是 (2017-12-06-001) 格式。

以下是 fileName 作为字符索引数组的示例。

然而,请注意,我只能使用一个字符来表示 10、11、12 和 13。

这里的重点是最大索引总是比字符串的实际长度小一。

这是因为 C# 在数组中使用零基索引(即数组从零开始计数)。

现在,如果我们仔细观察,我们可以更清楚地看到fileName.Length - 3的含义。

长度为 14,当我们从中减去 3 时,得到 11,所以我们看到Substring方法将从索引 11 开始(第一个参数的值)。现在,查看索引,您会看到第二个 1 是第 11 个索引,它位于倒数第二个零之上。这就是我们想要开始获取子字符串值的地方。

子字符串:第二个参数

提供给 Substring 方法的第二个参数是我们想要检索的字符数,我们给它的值为 3。所以从索引 11 开始,数过三个字符(从你所在的字符开始计数),我们得到了字符串的最后三个字符。

在我们的例子中,Substring 返回的值将是“001”。此时它是一个字符串值,我们需要将其转换为整数。

这就是第二个方法调用的作用。

Convert.ToInt16

我们调用 .NET 库提供的静态 Convert.ToInt16(),并简单地传入一个有效的字符串。有效的字符串意味着它包含可以合理转换为数字值的字符。

忽略前导零

Convert 的优点在于它会简单地忽略前导零并转换值。

在此示例中返回的值是整数 1。

最后,我们调用 List 方法 Add() 将该值添加到我们的列表中。所有这些都封装在我们的 foreach 循环中,以便我们将所有当前存在于 allCurrentFiles 数组中的文件号添加到列表中。

所有这些都是为了获取最大文件号

我们做了所有这些工作只是为了轻松获取当前最大文件号值。

第 55 行,我们创建一个新的局部整数变量来存储该值,并调用List提供的方法Max()。该方法简单地返回列表包含的Max值。当它这样做时,我们将其存储在一个局部变量中供我们使用。

第 56 行,我们简单地增加我们拥有的值,因为我们正在创建一个新的文件名以及一个新的文件号。

最后,在第 57 行,我们将FileName成员设置为String.Format调用返回的值,该调用用于确保我们的 FileName 格式始终正确。此String.Format()调用与 if 语句中的调用之间的区别在于,我们现在引用maxValue而不是 1,以确保它是应该用于文件号的新有效当前值。

它奏效了!大部分

这段代码现在将构建并运行,并将文件保存到正确的目录中。

但是,在我们的 MainPage.xaml.cs 中还有一件事需要修复。这是一件很小的事情,但如果你要尝试新功能,我认为我们应该修复它,这样应用程序才能更符合我们的预期。

修复 MainRichEdit 以清除

这就是现在出现的问题。

如果您运行代码并在Entry1中输入文本,然后切换到任何其他日期,您会发现 Entry1 仍然在 RichEditBox 中显示您输入的文本,尽管当您切换到新日期的条目时它应该清除它。

我们需要修复的代码在MainPage.xaml.csMainCalendar_SelectedDatesChanged()方法中。

在 while 循环中,我清除了多余的 PivotItems (Entries),但我忘记清除了 RichEditBox Document。

要解决此问题,我们只需向该方法添加以下一行代码

MainRichEdit.Document.SetText(TextSetOptions.ApplyRtfDocumentDefaults, "");

 

这只是将MainRichEditBox Document属性(对象)的文本设置为空。

 

获取最终章节代码

获取DailyJournal_v019.zip并构建运行。它将保存多个条目。

摘要

现在,代码会将您的所有文件保存到正确的Y-M目录中,并且所有与保存文档相关的功能都运行良好。

加载现有条目

然而,当您回到已经创建了条目的日期时,它们不会再次加载,因此看起来您从未保存任何条目。目前唯一检查条目的方法是使用文件资源管理器导航到文件夹,并使用 WordPad 等其他应用程序打开文档。

下次

这是我们本章最初计划解决的问题,但我们还有很多其他工作要做。下次我们将修复 LoadEntriesByDate() 方法并解决此问题,我们将非常接近一个完全正常工作的基本应用程序。

历史

2017-12-05:首次发布

© . All rights reserved.