编程 Windows 10 桌面:UWP 重点 (N 中的第 9 部分)
开始 UWP (从 WinForm 迁移) 第 9 章 重构 (MVC 思想),更多 OOP,设计对象,SoC 继续处理 DailyJournal 应用。
- 下载 DailyJournal_v013.zip - 124.4 KB
- 下载 DailyJournal_v014.zip - 124.4 KB
- 下载 DailyJournal_v015.zip - 124.4 KB
- 下载 DailyJournal_v016.zip - 124.6 KB
- 下载 DailyJournal_v017.zip - 124.9 KB
引言
我们花了 8 章才到这里。 回顾之前的文章,以便跟上我们正在构建的 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 部分)[^]
背景
我将继续尝试讲述通过 UWP (通用 Windows 平台) 编程 Windows 10 的故事,我们将通过将代码重构为适当的类来完成工作,我们将讨论重构背后的思路,并讨论 OOD (面向对象设计)。
本章最终长达 32 页,但我希望您能跟上。您可以逐字阅读并仔细查看截图来体验我们所做的更改。 我为此付出了很多努力,希望您喜欢。
此外,作为印刷书籍,您的体验可能会更好,您现在可以获得前 8 章的印刷版 (我将在创建完第二部分后发布)。
编程 Windows 10 Via UWP:学习为桌面编程通用 Windows 应用 (编程 Win10)[^] 我的印刷成本为 5.93 美元,但我将这本书的价格定为仅 6.95 美元。
如果您觉得 Kindle 版本更方便阅读,也可以选择 Kindle 版本。
编程 Windows 10 Via UWP:学习为桌面编程通用 Windows 应用 (编程 Win10) Kindle 版[^]
注意:我知道一些 CP 用户可能会认为这有点多余,但请考虑到我在这里也免费提供了完整的文章,在您因为我尝试以多种格式提供我的书而投反对票之前,请先考虑这一点。
当然,您始终可以在 CP 上阅读所有章节。
继续从上次中断的地方开始
让我们直接开始解决上一章中确定的问题。
如果您没有遵循迄今为止的所有步骤,或者只是想确保您拥有相同的代码,您可以获取 DailyJournal_v013.zip
并从该代码开始。
首先,我们想确保我们的 JournalEntries 对象实际上是一个集合类型的对象。 为此,我们可以简单地确保它是一个 List<>
类型的子项。
这仅仅意味着我们让系统知道它继承了通用 List<>
的行为。 通用 List<>
可以是任何类型,这意味着我们可以将其设置为 List<JournalEntry>
。
<meta charset="utf-8" />当我们定义类并使其继承自某个类时使用的语法如下所示。
public class JournalEntries : List<JournalEntry>
以自然语言阅读代码
如果您用自然语言阅读,您会说,公共类 JournalEntries
_是一个_ JournalEntry
的列表。 只需将 : (冒号) 替换为“是一个”这两个词,并将尖括号 <> 替换为“的”,一切就说得通了。
我们的 JournalEntries 类现在应该看起来像这样。
我们稍后会回来填充这个类,以满足更多需求,但现在让我们回到 MainPage.xaml.cs
并开始编写加载条目的代码。
加载日记条目
在上一章中,我们还决定了加载条目的方式。
-
在
CalendarView
上选择一个日期 (应用程序启动时或用户选择时)。 -
计算相应的
Y-M
目录。 -
如果
Y-M
目录存在,则表示已创建条目。 -
确定是否有任何文件名与所选日期匹配。
-
如果
Y-M
目录不存在或没有文件名匹配,则显示一个带有标题 Entry1 的空条目。 -
如果
Y-M
目录存在并且有一个或多个文件名匹配,则为每个文件生成一个PivotItem
,并将 RTF 数据加载到关联的RichEditBox
中。
当 Page_Loaded
事件触发时,我们将执行所有这些工作,因此让我们添加一个名为 LoadEntriesByDate()
的方法,然后从 Page_Loaded()
事件中调用它。
在 LoadEntriesByDate
() 方法中执行工作时,我们将需要访问 CalendarView,我刚刚注意到我们从未为其命名,所以让我们切换到 XAML 并命名该控件,以便我们更容易引用它,并且我们的代码更容易理解。 我将 CalendarView
命名为 MainCalendar
。
我们需要添加的 XAML 代码如下:
x:Name="MainCalendar"
添加该代码后,请切换回 MainPage.xaml.cs 并添加我们的新方法。
现在是时候删除 Page_Loaded
方法中插入到 ListView
的所有额外行了。 我已突出显示了您应该删除的行。 删除这些行后,请继续在 Page_Loaded
方法中添加一行来调用我们的新方法。
完成此操作后,请继续为新方法创建一个存根。 创建存根只是添加带有函数体但没有实际代码的基本函数。
现在,让我们弄清楚如何获取 CalendarView
设置的值。
好的,我刚刚在 LoadEntriesByDate()
中添加了一行代码,如下所示:
EntriesListView.Items.Add(MainCalendar.SelectedDates[0].ToString());
应用崩溃
然后我构建了应用程序,运行了它,它崩溃了。
这是因为我尝试调用 MainCalendar.SelectedDates[0]
,这是应用程序加载时 MainCalendar
中第一个 SelectedDate
的引用。 然而,这是错误的思维方式。 问题在于 Page_Loaded
方法在 MainCalendar
被选择之前触发 (即使它是自动选择的),当您运行应用程序时,选择发生在 Page_Loaded
方法运行之后。
我们真正需要做的是在 MainCalendar 的 SelectedDatesChanged
事件触发时调用 LoadEntriesByDate()
。
我们可以通过将它添加到 MainCalendar
控件中来确保我们处理该事件。 我们在之前的章节中已经做过,所以您可能还记得如何操作。
-
打开
MainPage.xaml
文件。 -
突出显示 XAML 中的
MainCalendar
(CalendarView
) 控件。 -
在属性窗口中,单击 [闪电] 图标,将显示可用事件列表。
-
向下滚动直到看到
SelectedDatesChanged
事件。 -
双击
SelectedDatesChanged
事件旁边的文本框。 -
Visual Studio 将打开
MainPage.xaml.cs
文件,添加方法并将光标移入该方法,以便您可以编写代码。
接下来,我们需要将 LoadEntriesByDate()
方法的调用从 Page_Load
方法移到我们新的 MainCalendar_SelectedDatesChanged
方法中。
进行此更改后,请继续构建并运行。
注意:请确保将 LoadEntriesByDate 方法从 Page_Load 方法中移出,否则您的应用程序将崩溃。
获取代码
如果您没有自己输入所有代码,可以运行 DailyJournal_v014.zip
中的代码。
更多挑战
当您启动应用程序时,您会看到 CalendarView
被设置为当前日期 (在我这里显示为实心绿色选中)。
但是,正如您所见,我们的 ListView 中没有值,尽管我期望会有值显示出来。
它本应在那里显示,因为我们有 LoadEntriesByDate
() 中的那行代码。
private void LoadEntriesByDate()
{
EntriesListView.Items.Add(MainCalendar.SelectedDates[0].ToString());
}
那行代码将添加 SelectedDates[0]
(零是所有 SelectedDates
的第一个索引)。 在我们的例子中,我们只需要一个选定的日期,并且当应用程序加载时,它应该是当前日期。
似乎我们的 MainCalendar_SelectedDatesChanged
在应用程序启动时没有触发。
但是,如果您单击 CalendarView
中的任何日期,您将看到其关联的日期值已添加到我们的 ListView
中。
问题是:“为什么我们需要单击当前日期才能触发该功能?”
问题的线索
此外,请注意 12 月 2 日的日历项和 12 月 11 日之间的区别。 12 月 2 日是当前日期,实心绿色并非表示它被选中,而是表示它是当前日期。
12 月 11 日日期周围的绿色线条表示它已被选中,因为那是我最后一次单击的日期。
我将重新启动应用程序,并检查当前日期的状态,看看它是什么样子。
这是应用程序启动时 CalendarView
的快照。
请注意,12 月 2 日是实心绿色。 现在单击它。
在之前的快照中,我还包含了我们 ListView
控件的顶部,以向您展示 MainCalendar_SelectedDatesChanged
事件处理程序在我单击当前日期时确实被调用了。
此外,请注意,现在 12 月 2 日的日期有一个小白色轮廓,这表示它现在是选定的日期。 这告诉我们什么?
应用程序启动时没有选择日期
这告诉我们,应用程序启动时实际上没有选择日期。 这就是为什么 MainCalendar_SelectedDatesChanged
事件处理程序从未运行。
作为开发人员,这些是我们必须理解的细微之处。 我们不一定能确切地知道这些控件是如何工作的,因为它们是由其他开发人员 (在本例中是 Microsoft 开发人员) 创建的。
我猜我们可以通过处理 CalendarView
上的 Loaded 事件来解决这个问题。 所以,让我们向 MainCalendar
添加该事件处理程序。 您将遵循我们添加 SelectedDatesChanged
事件时所采取的步骤,因此我不会在此重复所有这些。
一旦您将 Loaded
事件处理程序添加到 CalendarView
,我们只需要添加一行代码即可确保在 CalendarView
加载时选择当前日期。
MainCalendar.SelectedDates.Add(System.DateTime.Now);
System.DateTime.Now 是什么?
System 库包含 Microsoft 提供的用于帮助我们工作的类和方法。 在这种情况下,它们提供了一个静态类 DateTime
,它有一个名为 Now 的静态属性,该属性是全局可用的,因此您可以获取当前的系统日期。 这与 CalendarView
用于设置其当前日期的日期相同。
什么是静态属性或方法?
您可能已经注意到,我们不必实例化一个新的 DateTime
对象即可使用它。
如果我们需要实例化 DateTime
对象,我们将使用 new 关键字,如下所示:
DateTime currentDate = new DateTime();
在这种情况下,Microsoft 开发人员决定 DateTime
功能应该更容易访问,因此他们将其制作成一个 static
类,允许从您的程序轻松全局访问它 (只需添加 System 库引用)。
所以,您不必实例化一个新的变量 (currentDate
),而是可以直接从静态类调用静态方法,并且库会为您实例化一个全局对象以供使用。
在这种情况下,这只是一个便利。 您可以在 Microsoft 文档中阅读有关静态变量的更多信息,并且在本本书中,我们将讨论它们应该如何使用以及不应该如何使用。
继续讨论 Loaded 事件处理程序
现在,让我们继续解释我们代码中的一行代码在 Loaded
事件处理程序中的作用。
从 System.DateTime.Now
静态方法调用获取当前日期后,我们通过调用 Add
方法将其传递到 MainCalendar
控件的 SelectedDates 集合中。
构建、运行并尝试一下
获取 DailyJournal_v015.zip
以获取更新的代码,构建并尝试一下。
现在,当应用程序启动时,它的行为正如我们最初期望的那样,并选择当前日期,这会触发 MainCalendar_SelectedDatesChanged
事件处理程序。 您可以看到当前日期现在具有选中外观,并且当前日期值已添加到 ListView
中。
现在所有这些都正常工作了,我们可以开始填充 LoadEntriesByDate()
方法了。
按照我们在本章开头的工作项列表,我们现在需要检查此年-月目录是否存在。
检查文件夹是否存在
当我们编写 Save() 方法 (现在在我们的 JournalEntry 类中) 时,我们已经确定了我们将为应用程序存储文件的位置。 那行代码允许我们获取将用于包含所有 Y-M 目录的基础文件夹。
Windows.Storage.StorageFolder storageFolder =
Windows.Storage.ApplicationData.Current.LocalFolder;
让我们在 MainPage.xaml.cs
(Page
类) 中添加一个成员变量,我们称之为 appHomeFolder
。
在类的顶部添加一行如下所示的代码:
private Windows.Storage.StorageFolder appHomeFolder;
然后,我们在 Page_Loaded
事件处理程序中添加一行代码来将其初始化为我们应用程序的本地文件夹位置。
appHomeFolder = Windows.Storage.ApplicationData.Current.LocalFolder;
这将确保无论何时启动应用程序,它都在处理相应的家目录。
我们还可以将当前的 Y-M
目录值存储在一个成员变量中,这样我们就无需每次都访问它了。
现在让我们添加该成员变量。
该值可能随时更改 CalendarView
的 selectedDates
更改,因此我们将初始化添加到我们的 MainCalendar_SelectedDatesChange
。
初始化 YMFolder 的代码行如下所示:
YMFolder = System.DateTime.Now.ToString("yyyy-MM");
我们使用 Now
属性的 ToString()
方法来格式化我们将使用的日期字符串。
日期字符串格式说明符
在这种情况下,传递给 ToString() 的字符串是一个格式说明符,其中包含特殊值,告诉它获取 4 位年份,后跟一个破折号 (-),然后是 2 位月份值。 此格式说明符的大小写很重要,您可以看到年份说明符是小写的,但月份 (M) 说明符必须是大写的。 您可以在 Microsoft 文档中查找 Date.ToString() 方法,并找到所有格式说明符以及有关它们工作方式的更多信息。
当这行代码运行时,YMFolder
将设置为“2107-12”,因为它当前是 2017 年 12 月。
请注意,我们在调用 LoadEntriesByDate
() 方法之前添加了该行,以确保在将要使用它的方法之前设置该值。
我们现在可以检查文件夹是否存在 (在 LoadEntriesByDate
() 中),如果存在则执行一项操作,如果不存在则执行另一项操作。
这是我添加到该方法的代码。
if (Directory.Exists(Path.Combine(appHomeFolder.Path, YMFolder)))
{
}
else
{
EntriesListView.Items.Add(Path.Combine(appHomeFolder.Path, YMFolder));
}
让我们先检查第 45 行。 实际上,这一个代码行调用了两个不同的方法。
运行时有一个方法调用顺序。 它非常类似于数学问题的运算顺序。 在这种情况下,括号内的项在其他项之前运行。 最内层的项首先运行,通常这意味着代码行中最右边的项。
在这种情况下,它首先调用 Path.Combine()
方法。
Path.Combine 便利方法
Path.Combine 是 .NET 基础库提供的另一个便利方法,它接受多个字符串作为参数,并将它们连接 (一个接一个地放置) 成一个字符串,该字符串成为路径。
在我们的例子中,我们将我们的 appHomeFolder 路径和 YMFolder 传递给它,并要求它创建一个路径字符串。 此方法在您发送到它的不同参数之间添加了适当的反斜杠分隔符,因此我们不必担心它们是否存在。
当 Path.Combine
完成后,它会返回一个字符串 (代表我们创建的路径)。 我们立即使用该字符串调用我们的下一个方法 Directory.Exists(
)。
Directory.Exists()
这是 .NET 库提供的又一个便利方法,它允许我们传入一个表示路径的字符串,并告诉我们该路径是否存在。 它根据路径是否存在返回一个布尔值 (true 或 false)。
最后添加的行在 else 语句中。
我知道我们还没有在 appHomeFolder
下创建任何 YMFolders
,所以我知道 Directory.Exists()
的结果将始终为 false,并且我希望在用户界面上看到一些东西,所以我添加了一些代码在 else 语句中。
您可以看到我再次调用 Path.Combine
。 这次我调用它来创建表示路径的字符串,以便将其添加到 EntriesListView
中,这样我就可以查看它。
EntriesListView.Items.Add(Path.Combine(appHomeFolder.Path, YMFolder));
构建并运行代码:一个问题
我们现在可以构建并运行代码了。 您可以获取 DailyJournal_v016.zip
并尝试一下。
然而,由于我们创建原始 XAML 布局的方式,应用程序现在表现得有点奇怪。 应用程序启动时,您会立即识别出问题,具体取决于 MainPage
的大小。
我们的 Pivot 被推出了屏幕
突然,因为我们向 EntriesListView
添加了这个长字符串,我们将整个行推到了右侧,而跨越两行的 Pivot 被推出了屏幕。
如果您调整 MainPage
的大小或最大化它,您应该会再次看到 Pivot
。
这并不理想,但我们现在不会处理它。
请注意,您现在可以看到 YMFolder
的完整路径,这很好,因为我们将使用它来保存我们的条目文件。
然而,我们的 Save
现在封装在我们的 JournalEntry
中。 这意味着当我们创建一个新条目时,我们希望将完整的 YMFolder 路径传递进去。 我们需要回去看看我们的 CreateNewEntryButton_Click,看看我们将如何更改它和我们的 JournalEntry
类。
重构开始
由于我们现在将 Page
与 JournalEntries
集合进行交互 (协作),让我们删除我们的 journalEntry
成员变量,并添加新的 JournalEntries
成员变量。
删除高亮显示的行,并添加其前面的行。
完成此操作后,让我们打开 CreateNewEntryButton_Click 并编辑它以创建一个新的 JournalEntry 并将其添加到 currentJournalEntries 集合中。
创建新的 JournalEntry 并将其添加到 JournalEntries 集合
我们可以通过以下代码行将新的 JournalEntry 添加到我们的成员集合中:
currentJournalEntries.Add(new JournalEntry(reb));
在我们的 CreateNewEntryButton_Click
方法中,它看起来像这样:
但这并没有太大帮助,因为我们并没有真正使用添加到集合中的项。 此外,我们需要更改 JournalEntry
的构造函数,以便它可以接受完整的 YMDirectory
路径,这样它就可以将文件保存在正确的位置。
重构就是迭代
这可能感觉我们似乎在来回移动,因为我们不断地更改这个又改那个,但我保证我们正在走向更好、更干净的代码。 我还包含了这段叙述,以便您可以看到真正的开发人员如何解决问题。
让我们切换到 JournalEntry
类,并添加新的成员来存储 YM
全路径,并修改构造函数以接受该值,以便它可以正确初始化成员变量。
修改 JournalEntry
我们将执行以下工作:
-
添加名为 YMFullPath 的新成员变量。
-
在 JournalEntry 构造函数中添加新的传入参数。
-
用传入值初始化我们的成员。
新的 JournalEntry
代码如下所示:
破坏编译
当您添加此新代码后尝试编译代码,编译将失败。
这是因为我们从 NewEntryButton_Click
调用了 JournalEntry
构造函数,当编译器检查已定义构造函数的签名时,它知道它们不匹配。
我已为您突出显示第一个错误,以便我们查看。 它指出,“对于 DailyJournal
‘JournalEntry.JournalEntry
’ (RichEditBox, String) 所需的正式参数 ‘YMFullPath
’,没有参数。”
编译器错误可能令人困惑
编译器会尝试提供帮助,但您需要习惯它用来描述问题的语言。
当然,它也告诉您错误发生在哪个文件和哪一行,所以这非常有帮助。 Visual Studio 通过允许您双击错误并将您转到源代码中的该行,从而轻松移动到错误发生的行。
考虑按列表顺序修复错误
但是,请看看第二个错误。 它说,“当前上下文中不存在 journalEntry…” 编译器提醒我们,我们删除了 journalEntry
成员,因此它不再存在。 我们也将修复该错误,但首先让我们修复列表中列出的第一个错误。
我们需要回到 CreateNewEntryButton_Click
并添加代表 YMFullPath
的参数。
向构造函数调用添加新参数
这是新的修改过的 JournalEntry
构造函数调用:
currentJournalEntries.Add(new JournalEntry(reb, Path.Combine(appHomeFolder.Path, YMFolder)));
我们只是添加了 Path.Combine
来创建到 YM
目录的完整路径。
添加该代码后,我们仍然需要在 RichEditBox_SetFocus
方法中修复我们丢失的成员变量问题,该方法也将无法正常工作。
将屏幕元素绑定到特定的 JournalEntry
我们通过代码所做的主要更改是将 View
元素 (屏幕上的控件 -- Pivot
和关联的 RichEditBox
) 绑定到代表我们日记条目的 Model
* 对象 (JournalEntry
)。
关于模型
*模型对象只是一个类,它代表我们解决方案中的一个事物。 换句话说,该类模拟事物及其相关的行为。 随着您对 OOP (面向对象编程) 的了解越来越多,您会发现将 Views
绑定到 Models
的想法如此普遍,以至于有一个称为 MVC (模型-视图-控制器) 的模式,它提供了设计此类代码的常用方法。
这在 RichEditBox_SetFocus
方法中变得更加明显,因为我们将 RichEditBox 封装到了我们的 JournalEntry 模型中。 现在,我们需要弄清楚如何确定在关联的视图项 (RichEditBox) 获得焦点时选择了哪个 Model
。
当然,我们正在尝试让视图与我们的 JournalEntries
成员进行交互,所以我们可能需要在 JournalEntries
类中添加一个方法,当我们提供当前选定的 JournalEntry
对象时,该方法可以返回该对象。
我们对 JournalEntries 集合了解什么?
为了能够确定哪个 JournalEntry
被选中,我们需要一种方法来唯一地标识 JournalEntries 集合中的 JournalEntry
。 这是我们知道的关于我们的 JournalEntries
集合的一些事情:
-
该集合仅代表一天中的条目。
-
每天可以有多个条目,但每个条目都有一个唯一的 EntryHeader - 显示在 PivotItem 顶部的文本。
这使我们看到,我们可以使用 EntryHeader
作为 JournalEntry
的唯一 ID。 但是,现在我们需要将该成员变量添加到 JournalEntry 中,并且再次需要将其添加到我们的 JournalEntry
构造函数中。 所以,在我们修复这个 RichEditBox_SetFocus
问题之前,让我们修改我们的 JournalEntry
类。
将 EntryHeader 添加到 JournalEntry 类
添加新的成员变量并修复 JournalEntry
构造函数只需要几行代码。 请注意,我实际上将构造函数签名拆分成了两行,这样屏幕截图图像就不会太宽。 C# 允许您这样拆分行,没有任何问题。
另外请注意,这次我们将成员变量的访问修饰符设置为 public。 我们还使用 get 关键字定义了一个 get 方法,该方法使此项可公开访问以供只读。 除了开发人员调用构造函数外,任何时候都不能设置该值。 这可以确保类外部的任何方法都不会突然或意外地更改我们的 EntryHeader
。 由于我们将其用作标识符,因此这很重要。
稍后您将在我们的 JournalEntries 类中看到我们如何使用此成员值。
当我们构建时,我们会收到关于构造函数缺少参数的相同错误,所以再次,让我们构建并修复 CreateNewEntryButton_Click
方法中的构造函数调用。
同样,您会看到我已将长行拆分为多行,以便您更容易查看。
我在下面的图像中高亮显示了进行多个方法调用的那一行。
现在,让我们将 EntryHeader
值添加到 JournalEntry
构造函数调用中。
一旦您在 JournalEntry 构造函数调用末尾键入逗号,Visual Studio 就会弹出一些 Intellisense。
您可以看到它已经知道新的 EntryHeader
参数。
我们希望 EntryHeader
的创建方式与我们在 CreateNewEntryButton_Click
方法的顶部创建 entryText
变量的方式相同。
我们可以使用该 entryText
值作为我们的 EntryHeader
,所以只需将该变量添加到构造函数调用的末尾。
继续构建,但我们仍然需要清理 MainPage
类中的错误。
同样,为了修复这些错误,我们需要能够获取用户当前正在查看的日记条目。 但现在我们可以编写一个方法在我们的 JournalEntries
类中,当我们提供 EntryHeader
字符串值时,该方法应该返回 Model
项目。 当然,该值将来自我们当前正在显示 (由用户当前选中) 的 PivotItem
(View
)。
JournalEntries 辅助方法
JournalEntries
类一直是空的,到目前为止,我们只使用了它提供的继承方法 (因为我们将其设为了 List<>
的子项)。 现在我们准备添加一个新的公共方法,我们可以调用它来获取与当前选定的 View 对象 (PivotItem
) 匹配的 Model
对象 (JournalEntry
)。
实际上,我们只需要:
-
传入将标识我们正在寻找的
JournalEntry
的EntryHeader
值。 -
迭代集合,直到找到匹配的
JournalEntry
。 -
返回
JournalEntry
(如果找到) 或 null (空) 对象 (如果未找到匹配项)。
我将把方法命名为 GetSelectedEntry
。 这是该方法的完整代码列表:
public JournalEntry GetSelectedEntry(String EntryHeader)
{
foreach (JournalEntry je in this)
{
if (je.EntryHeader == EntryHeader)
{
return je;
}
}
return null;
}
foreach
语法 (在第 13 行) 非常有用。 我们只需提供一个临时变量名 (je
),其类型为集合所持有的类型 (JournalEntry
),它就知道如何迭代集合。 注意 _this_ 变量。 它允许我们引用我们当前正在处理的类。 由于我们正在 JournalEntries
类内部工作,所以 this 变量是对当前已实例化的 (程序运行时) JournalEntries 对象的引用。 由于 C# 是一个强类型系统 (程序运行时知道对象的类型),因此程序知道 this 变量是 JournalEntry
对象的集合。
je.EntryHeader :公共成员
je.EntryHeader
是为我们提供 EntryHeader
值的属性,我们只需将其与用户传入的、她试图查找匹配项的值进行比较。
最后,如果找到匹配的 JournalEntry
,我们将其返回,以便用户可以以他们想要的方式与之交互。
如果出于某种原因未找到匹配项,我们将返回 null (空对象),以便调用者可以确定她在这种情况下想要做什么。 您将在我们的 RichEditBox_SetFocus
方法中看到该代码。
再次,我们可以构建,但仍然会出现一些错误。 但是,我们现在应该能够修复这些错误,因为我们可以获得当前显示的日记条目。
回到 MainPage.xaml.cs
,我们的 RichEditBox_SetFocus
方法看起来如下:
现在,我们需要添加对 GetSelectedEntry()
的调用并删除旧的构造函数调用。
我们不需要构造一个 JournalEntry,因为它应该已经添加到我们的 JournalEntries
集合中了。 相反,现在我们只想获取匹配的那个。
再次重构:这确实是开发人员所做的
现在,我正在考虑这个问题,并质疑为什么当控件获得焦点时我想要获取当前控件。 真正的原因只是为了以后当我们保存数据到文件时,我们调用正确的 JournalEntry
的 Save()
方法。
现在,我认为 JournalEntries
集合应该只为我们跟踪当前的 JournalEntry
。 所以,与其在获得焦点时真正需要获取一个 JournalEntry
对象,不如我们实际上想要的是让 JournalEntries 有一个成员来包含当前选定的 JournalEntry
。 这样,以后当我们想要调用当前选定的 JournalEntry 的 Save()
方法时,我们可以只获取当前选定的 JournalEntry 并调用 Save()
。
这种思路意味着我们需要再次修改 JournalEntries,添加一个成员,该成员将是公开可访问的,仅用于 get (不是 set),并且代表当前选定的 JournalEntry。
让我们来更改一下,这样一切都会更有意义。
切换回 JournalEntries
类,并在类顶部添加以下代码行:
public JournalEntry currentJournalEntry { get; private set; }
我们将在 GetSelectedEntry
中设置此成员,因此我们必须允许它也设置成员,所以我们在 set 定义上使用了 private
(访问修饰符)。 这个 private set 意味着 set 只能在类内的 JournalEntry 上调用。
无需返回 JournalEntry
现在我们有了一个代表当前选定条目的成员。
这意味着我们不再需要从 GetSelectedEntry()
返回 JournalEntry。 相反,我们只需要让该方法设置 currentJournalEntry。
让我们现在更改该方法。
这是最终的 GetSelectedEntry
方法代码:
public void GetSelectedEntry(String EntryHeader)
{
foreach (JournalEntry je in this)
{
if (je.EntryHeader == EntryHeader)
{
currentJournalEntry = je;
return;
}
}
currentJournalEntry = null;
}
您可以看到,在第 13 行,我们将返回类型更改为 void,因为它不再需要返回 JournalEntry。
我们还更改了第 19 行和第 23 行,以便根据是否找到匹配项来适当地设置 currentJournalEntry
。
但是,也快速看一下第 20 行。 该行在找到正确的 JournalEntry
后立即退出方法。 这很有帮助,因为它在找到正确的条目后不会浪费时间去查找更多条目,而如果它不退出,它最终会在方法底部始终返回 null 值。
让我们回到我们的 MainPage.xaml.cs
并完成它。
当我们切换回 MainPage.xaml.cs
并检查 RichEditBox_SetFocus
方法时,我们会发现它不需要任何更改。 这是因为当该方法运行时,它只是在集合类上设置当前选定的 JournalEntry
。 这个当前选定的 JournalEntry
仅在稍后由 SaveEntryButton_Click
在尝试 Save()
条目数据时使用。
我们只需要更改 SaveEntryButton_Click
,以确保它只在 currentJournalEntry 不为 null 时才保存。 这是 SaveEntryButton_Click
的代码:
private async void SaveEntryButton_Click(object sender, RoutedEventArgs e)
{
if (currentJournalEntries.currentJournalEntry != null)
{
currentJournalEntries.currentJournalEntry.Save();
}
}
当我构建代码时,我收到一个警告——这表明存在一个会导致程序崩溃的问题。
当我双击该错误时,它会带我到我们创建集合成员变量的位置。
崩溃解释
问题在于我们从未实例化一个新的 JournalEntries
对象。 我们可以通过向 MainPage
构造函数添加一行代码来轻松实现这一点。 让我们这样做并再次构建。
我们需要添加到 MainPage
构造函数中的代码行如下:
currentJournalEntries = new JournalEntries();
为什么 Save() 不起作用
现在应用程序可以构建了,但是 JournalEntry.Save()
方法将无法正常工作,因为我们从未实现我们所做的 YMFullPath
更改。 让我们去完成它。
我在这里遇到了一个挑战
此时,我必须解决许多问题才能确定如何在 Y-M
子文件夹中保存我们的文件夹。 花费了一些时间,但我解决了这个问题,在此期间,我确定我需要在 JournalEntry
类中添加另一个成员变量。
我将其命名为 YMFolder
,它将保存 Y-M
文件夹的名称 (“2017-12”)。 我发现这使得我们的 Save()
方法的代码更加简洁。
当然,我也在 JournalEntry
构造函数中添加了该项。
这是您可以直接复制到 JournalEntry
类顶部的更新代码:
private RichEditBox _richEditBox;
private String YMFullPath;
private String YMFolder;
public String EntryHeader { get; }
public JournalEntry(RichEditBox richEditBox, String YMFullPath,
String YMFolder,
String EntryHeader)
{
_richEditBox = richEditBox;
this.YMFullPath = YMFullPath;
this.YMFolder = YMFolder;
this.EntryHeader = EntryHeader;
}
当然,在进行此更改后,您还需要更改 CreateNewEntryButton_Click
中的 JournalEntry
构造函数调用,使其如下所示 (只需在调用中添加 YMFolder
):
currentJournalEntries.Add(
new JournalEntry(reb,
Path.Combine(appHomeFolder.Path),
YMFolder,entryText));
重构就是设计代码,这需要时间
本章变得非常长 (接近 31 页),这就是开发有时的情况:一旦开始,就无法停止。 像这样的代码重构需要我们修复所有相关内容,并且会产生连锁反应。 然而,它带来了巨大的好处,我们将继续在我们的应用程序创建过程中讨论 (和看到) 这些好处。
仍有工作要做
由于本章变得如此之长,我将在此提供 Save()
方法,并简要讨论它的作用。 然而,仍然有一些地方无法完全正常工作,并且有一些事情我们需要进一步讨论,例如我们在文件系统 (FileStorage
) 中所做的与 WinForm 开发时大不相同的事情。
JournalEntry Save()
这是 Save()
方法的完整源代码:
public async void Save()
{
StorageFolder storageFolder =
Windows.Storage.ApplicationData.Current.LocalFolder;
if (!Directory.Exists(Path.Combine(YMFullPath,YMFolder)))
{
await storageFolder.CreateFolderAsync(YMFolder);
}
StorageFolder subStorage = await storageFolder.GetFolderAsync(YMFolder);
StorageFile sampleFile =
await subStorage.CreateFileAsync(
"FirstRichEdit.rtf",
Windows.Storage.CreationCollisionOption.ReplaceExisting);
IRandomAccessStream documentStream =
await sampleFile.OpenAsync(Windows.Storage.FileAccessMode.ReadWrite);
_richEditBox.Document.SaveToStream(TextGetOptions.FormatRtf, documentStream);
documentStream.Dispose();
}
创建子文件夹存储的挑战
我不得不单独发送 YMFolder,并让 Save() 方法使用它,因为 UWP API 的创建 storageFolder 的方式。 我无法一次创建整个文件夹路径。 相反,我必须创建根目录,然后正如您在第 37 行所看到的,我必须调用 CreateFolderAsync,它只包含子文件夹名称。 它允许我创建一个以 Y-M 目录命名的子文件夹。
目前这可以工作。 所有这些都将构建成功,您可以保存文件,现在文件将保存在 LocalStorage
路径下的子文件夹 (Y-M
) 目录中。
在我的情况下,该路径位于
C:\Users\Roger\AppData\Local\Packages\116d1010-a1e4-452a-a1d2-c84aea07af6d_gw4zt26480tv8\LocalState
如果您最初创建了自己的项目,那么您的 Package ID 将不同,当然您的 <UserName>
也将不同。 请记住,访问此路径的简单方法是 %LocalAppData%\Packages
。
您可以将其粘贴到文件资源管理器中,然后按 <ENTER>,它就会带您到附近。
其他问题:应用程序中的 Bug
-
另外,Entry1 项永远不会保存,因为它目前没有添加到 JournalEntries 列表中。 我们需要做这项工作,稍后我们会做,但本章太长了。 只要知道如果您尝试保存 Entry1,它是不会发生的。
-
要测试 Save() 方法,您需要创建一个新条目,然后键入一些文本并尝试保存它。 根据您创建条目的年份/月份,它将为您创建一个匹配的 Y-M 文件夹并在那里保存一个文件。
-
-
应用程序将所有数据保存在 Y-M 目录下的同一个文件名中。 我们尚未实现为每个条目创建良好文件名的算法。 然而,您会看到,现在我们已经将代码分离到类中,这项工作将非常容易,因为我们可以让 JournalEntry 自己生成一个合适的文件名。
-
尚无条目被重新加载。 这与文件名尚未正确命名有关。 我们很快就会处理这个问题,而且现在我们已经分离了代码,我们几乎完成了。
-
我需要一种方法来重置整个 View (Page) 的状态,使其恢复到应用程序启动时的样子,每次用户更改选定的日期时。 目前,我只是遍历条目并在每次单击新日期时删除它们,这样您将只剩下 Entry1。
-
我还需要修复 MainCalendar_SelectedDatesChanged 中的一个 bug,该 bug 不正确地始终使用 DateTime.Now 值,而不是 MainCalendar 上选定的日期。 我在最后一个源代码下载 (DailyJournal_v017.zip) 中修复了它。 -- 修复如下:
-
YMFolder = MainCalendar.SelectedDates[0].ToString("yyyy-MM");
-
我知道我们仍然需要做很多更改,但这已经是一次巨大的学习章节,并且看到了真实的代码是如何重构的,我相信如果您能顺利完成本章的学习,您将学到很多东西。
下载代码:试用
获取 DailyJournal_v017.zip
并试用代码。 如果您愿意,可以在阅读第 10 章之前尝试修复一些问题,因为这将有助于您学习。
历史
2017-12-03:首次发布