Windows 10 桌面编程:UWP 焦点(N 之 8)
UWP 入门(从 WinForm 转向),第 8 章:应用面向对象编程、设计对象、SoC,并深入设计 DailyJournal 应用。
引言
请查阅之前的文章,以便了解我们正在进行的通过 UWP 创建 Daily Journaling 应用的项目。
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)[^]
为了推进 DailyJournal 应用所需的功能,我们需要为它的使用正确设置文件系统。
上次我们留下了一个能够将任何条目保存为 RTF(富文本文件)的应用,但每个条目都保存到同一个文件中,所以我们需要修复这个问题。
我们还需要确保每个文件都保存到其关联的 Y-M
(年-月)目录中。
一旦我们解决了这两个问题,我们就能够将条目添加到我们的 ListView
中,以便用户可以看到可用条目的列表。我们还可以在 CalendarView
中选择特定日期时加载所有相关条目。
背景
在我撰写和完成本章的过程中,我发现我需要通过讨论来正确设计应用程序。当我这样做时,我记录了“讨论”,以便您可以看到软件设计的真实过程。我之所以提到这一点,是因为我认为(并希望)文章有点冗长,但了解如何将设计融入软件编写中将对读者有所帮助并具有价值。
我们将讨论为什么使用 OOP(面向对象编程),以及什么情况表明应该创建新类等等。
一些代码规划/设计
在我看来,一个 领域
对象正在从我们的问题讨论中产生。我的意思是,当我们讨论我们的问题时,我们可能会发现自己提到了系统中的一些事物(名词),例如 JournalEntry
。我相信 JournalEntry 是我们应用领域(“特定活动或知识领域”,摘自 Google 词典)中的一个候选类。
JournalEntry 类
让我们在我们的项目中添加一个名为 JournalEntry
的类。我相信在设计这个类的过程中,您也会看到 OOP(面向对象编程)将如何帮助我们组织代码,使其更容易维护和扩展。
就目前的代码而言,我正在尝试在代码中管理许多事情,以便在应用执行的任何时候都能知道应该向用户显示哪个 JournalEntry
。我们到目前为止编写的所有代码都只是为了让事情正常工作,但现在随着应用开始增长,我们需要退后一步,确保我们的代码有意义并具有一些基本的组织结构。如果您现在不这样做,那么以后当您想要扩展应用或在另一个应用中使用应用的部分时,您会发现这样做很麻烦或不可能。
讨论我们的应用有助于我们发现它的设计方式
实际上,如果我们思考我们的应用做了什么,并用一个自然语言句子来解释它,我们就会开始看到我们哪里有一些混杂的代码。
以下是我可能解释 DailyJournal 功能的方式。
“它允许用户 CRUD(创建、读取、更新、删除)按日期组织的日记条目。” 创建意味着“第一次保存”。更新意味着“保存对先前创建的日记条目的更改”。
职责分离 (SoC)
面向对象编程 (OOP) 最基本的作用之一是允许开发人员一次专注于一件事。这个概念在软件开发领域被称为 SoC 或职责分离。
我们需要设计我们的代码,以便当你接触代码的一个部分时,它不会破坏或影响代码中不相关的部分。当你大声说出来时,这听起来很明显,但软件开发的历史表明,开发人员“完成工作”后,却发现所有东西都堆在一起。直到后来某个倒霉的家伙(一个维护程序员)不得不修复一些模糊的 bug,然后才发现最初的程序员基本上把所有的代码都扔进一个桶里,然后混淆了所有东西。
面向对象编程 (OOP)
这只是一种方法,允许开发人员通过将代码分成称为类的封装单元来组织代码。系统可以从这些类(模板)创建(实例化)对象。当一个类出现错误时,在一个地方修复它并知道你已经修复它比当代码没有这种组织系统时要容易得多。当然,OOP 还有更多内容,但这是一个反复出现的主题,如果你现在接受它,它将对你作为开发人员的旅程有很大帮助。
代码组织嗅探测试
有了这些基础知识,我们现在可以通过简单地查看 MainPage.xaml.cs
中的 SaveEntryButton_Click
方法来识别一些糟糕的关注点混合。
您分享代码的容易程度如何?
该方法中的代码完成了将日志条目保存到文件的所有工作。现在,想象一下您想与团队中的另一位程序员共享可以保存日志条目的代码。您必须将其复制并粘贴到他的 Page
类中,然后你们每个人都将拥有相同代码的副本。然后,如果你们中的一个人后来注意到该代码中的错误,您就必须告诉另一个人去修复该开发人员使用该代码的地方。这不太好。
代码位置错误
发生这种情况是因为代码位于错误的位置(在我们的 Page
对象内部)。由于我们不是将 Page
对象保存到文件中,而是保存一个日记条目,因此代码表明我们的系统中有一个需要创建的另一个领域对象(JournalEntry
),并且它应该能够自行保存到文件中。
如果我们真的这样做,并且另一个开发人员也想保存一个 JournalEntry
,那么您只需与另一个开发人员共享 JournalEntry
类,他就可以创建一个 JournalEntry 对象,并且它会知道如何保存自己,因为它需要的所有代码都封装在自身内部。此外,他不必理解保存如何工作的具体细节,而是简单地创建一个新的 JournalEntry 并调用它的 Save
方法,一切都会正常工作。
其他表明有问题的地方
代码中还有其他迹象表明有些地方不太对劲。这些地方让我们不得不思考,“等等,屏幕上当前显示的是哪个 JournalEntry?” 诸如此类的事情是微妙的,它们不是硬性规定。它们是向您作为开发人员表明您的系统中还有另一个需要分离到其自身类中的东西(另一个领域对象)。
用户故事可能有所帮助
我们通常可以通过以一种讲故事的方式讨论应用程序的工作方式来更多地揭示这些领域对象。
-
当应用程序启动时,作为用户,我希望它默认设置为当前日期。
-
当应用程序启动并默认设置为当前日期时,作为用户,我希望应用程序显示当前日期的第一个存在的日记条目。
-
作为用户,我希望能够保存我创建的条目。
关于 DailyJournal 如何工作的详细说明
以下是关于它可能如何工作的详细说明
应用已加载
当应用程序加载,或当用户点击 CalendarView
中的一个日期时,我们需要
-
检查日期
-
打开相应的日期文件夹(如果存在)
-
确定当前日期是否有任何日记条目
-
显示当前日期的条目 1(如果存在)-- 如果不存在任何条目,则显示一个空的条目 1。
用户点击保存按钮
当用户点击保存按钮时,应用程序必须
-
获取日期
-
如果
Y-M
目录不存在,则创建它 -
创建 RTF 文件(如果不存在)
-
将 RTF 保存到
Y-M
目录中。
在 Visual Studio 中添加新类
要添加新类
-
转到解决方案资源管理器
-
右键单击您的项目
-
将出现一个菜单——向下移动到[添加]菜单项
-
将出现另一个菜单——向下移动到[类…]菜单项并点击它
当您点击[类…]菜单项时,将弹出另一个窗口。
确保选择该窗口左侧的[代码]。当您这样做时,右侧将出现相应的选项。
选择类选项。
在该窗口底部的[名称:]编辑框中输入我们新类的名称 (JournalEntry
)。
点击[添加]按钮。
当您点击[添加]按钮时,Visual Studio 将会
-
创建新文件
-
将其添加到当前项目
-
打开并显示它,以便您可以编辑它。
您可以看到 Visual Studio 已创建一个名为 JournalEntry
的新类,并将其添加到了我们的默认命名空间 (DailyJournal) 中。此时,您拥有一个空类,它将什么也不做。
我要做的第一件事是剪切 SaveEntryButton_Click
方法(在 MainPage.xaml.cs
中)中的所有代码,并将其粘贴到我们的 JournalEntry
类中的一个新 Save
方法中。
剪切所有蓝色高亮显示的代码
这是您可以粘贴到 JournalEntry 类中以创建新 Save 方法的代码
public async void Save()
{
Windows.Storage.StorageFolder storageFolder =
Windows.Storage.ApplicationData.Current.LocalFolder;
Windows.Storage.StorageFile sampleFile =
await storageFolder.CreateFileAsync("FirstRichEdit.rtf",
Windows.Storage.CreationCollisionOption.ReplaceExisting);
IRandomAccessStream documentStream =
await sampleFile.OpenAsync(Windows.Storage.FileAccessMode.ReadWrite);
currentRichEditBox.Document.SaveToStream(TextGetOptions.FormatRtf, documentStream);
documentStream.Dispose();
}
请注意,该方法被标记为 public
。这样 Page
类就可以使用 JournalEntry
并调用 Save()
方法。如果它被标记为 private
,我们将无法调用该方法。
您可以看到,当我们把代码粘贴到类中时,Visual Studio 正在警告我们一些代码问题。
这仅仅是因为在新的 JournalEntry
类中,我们需要的库没有被引用(没有 using 语句)。
如果你在类文件的顶部添加以下两个 using 语句,那么两个错误就会消失。
using Windows.Storage.Streams;
using Windows.UI.Text;
但是,您仍然会遇到第三个错误,因为 currentRichEditBox
不再可用。它是 Page
类的一个成员,而不是我们新的 JournalEntry
类的一个成员。
我们必须找到一个解决方案。
将代码移动到正确位置(类)
这实际上是 OOP 三大原则之一的例子,称为封装,与作用域相关。OOP 中的对象应该只能被使用它们的实体访问。
换句话说,Page
对象不再需要真正处理 RichEditBox
,而是 JournalEntry
需要知道它。这意味着我们将从 Page 中移除 currentRichEditBox
,并给 JournalEntry
一个作为 RichEditBox
的成员。
要做到这一点,我们只需转到类的顶部并添加一行,如下所示:
private RichEditBox _richEditBox;
我们正在告诉类它将拥有一个私有成员(在该类之外无法访问),它是一个 RichEditBox
。
当我们将 RichEditBox
添加到我们的 JournalEntry 类时,类型未知,因为我们需要另一个 using 语句来引用定义该类型的库。
using Windows.UI.Xaml.Controls;
然而,添加成员变量并不会初始化 RichEditBox
。此外,我们需要它等于我们 MainPage
上显示的 RichEditBox
。
类构造函数
幸运的是,一个类允许我们添加一个特殊的构造函数方法,它将在对象实例化时(当从我们的类模板生成一个对象时)首先运行。
我们可以在 JournalEntry 类中添加一个构造函数,这样当 Page
对象创建一个新的 JournalEntry 时,它可以传入 RichEditBox
引用。
这是定义我们构造函数的代码
public JournalEntry(RichEditBox richEditBox)
{
_richEditBox = richEditBox;
}
这是此时整个 JournalEntry 的样子
代码构建成功,但无法正常工作
有趣的是,代码现在可以构建,但无法正常工作。
问题列表
-
“保存”按钮将不再起作用——它现在根本没有实现代码。
-
JournalEntry 类未被使用
快速修复:向我们展示一个间接级别
让我们对 SaveEntryButton_Click
方法进行一个快速修复,以展示我们如何让应用再次工作,并看看它是如何引导我们进行更改的。
现在我们的 SaveEntryButton_Click
方法是空的。我们把代码移到了 JournalEntry
类中。
所以,让我们现在采取以下步骤
-
在方法中新建一个
JournalEntry
对象。 -
在
JournalEntry
上调用Save
方法。
我们只需要添加两行代码
JournalEntry je = new JournalEntry(currentRichEditBox);
je.Save();
构建、运行、测试
现在,该应用程序将与之前完全相同,您可以像上一章一样将文档保存到 FirstRichEdit.rtf
。
下载代码
如果您还没有跟上,可以在本文顶部下载 DailyJournal_v011.zip
并试用。
大量工作只为回到原点
我理解您可能会觉得这做了很多工作才得到相同的代码。然而,我们不仅在尝试学习如何开发应用程序,还在学习经验丰富的开发人员如何创建健壮、可扩展且更易于使用的代码。
此外,我们还需要修改一些东西,这将有助于更明显地说明为什么将代码移到自己的类中会使事情变得更好。
我们需要改变什么?
我们现在需要更改我们的 MainPage
类,以便它使用 JournalEntry
而不是 RichEditBox
。让我们从类的顶部删除成员变量 currentRichEditBox
并重新构建,因为我们会得到一些错误,它们将指示我们需要将什么移动到我们的 JournalEntry
类中。
进入 MainPage.xaml.cs
文件,在类内部,删除包含成员变量 currentRichTextBox
的那一行。
之后,继续添加一个新的 JournalEntry
成员变量。
private JournalEntry journalEntry;
完成这两项更改后,请继续重建项目。
清理解决方案
注意:如果您在重建项目时遇到困难,请转到 Visual Studio 顶部的[生成…]菜单,然后选择[清理解决方案]菜单项。这将重置解决方案,以便可以正确构建。
#####################################################################
侧边栏:Visual Studio 2017 问题
此时,当我尝试重新构建时,我遇到了一个错误,我的项目无法重新构建。
另一个问题是,这只是一个警告,所以应用程序应该能够正常构建,但它没有。真正的问题是我知道有错误,但正如您所看到的,它报告了 0 个错误。
发生了一些非常奇怪的事情。
我终于右键单击项目并打开属性窗口,然后取消选中这两个框。
之后,我终于看到了我们预期的错误。
#####################################################################
移除 currentRichEditBox 后预期出现的错误
当您在 Visual Studio 中构建时,如果存在错误,它们将列在 ErrorList
窗口中(通常在 Visual Studio 的底部)。
我们有两个错误。我高亮显示了下面的一个,这样你就可以区分一个在哪里结束,另一个在哪里开始。
两个错误都给出了相同的描述,但每一个都来自我们源代码中的不同行(一个在第 82 行,另一个在第 87 行)。
要转到错误发生的位置,请双击第一个错误,它将把您带到 MainPage.xaml.cs
中的那一行。
因为另一个只在下面 5 行,所以我们也能看到它。
您可以看到 Visual Studio 也通过使用红色波浪线来警告您。
这些错误对我们来说是有意义的,因为我们知道我们删除了成员变量。
用一行代码解决问题
我们实际上可以通过以下方式解决这个问题
-
添加一行新代码。
-
删除所有带有波浪线的行
-
将
je.Save()
行修改为使用我们的成员:journalEntry.Save()
这将解决所有问题,代码将再次运行。
这是我们将要添加的代码行
journalEntry = new JournalEntry(reb);
该行代码将当前的 RichEditBox 存储在当前的 JournalEntry
(journalEntry
) 成员变量中。每次 RichEditBox
获得焦点时,它都会创建一个新的 JournalEntry
(使用当前选定的 RichEditBox
),并将其引用存储在我们的成员变量 journalEntry
中(它代表当前的日记条目)。
注意:我们可以做得更好,但目前已经足够好了。我们没有生成(实例化)成千上万的对象,所以没问题,但有更好的方法可以做到这一点,我们稍后会探讨。
这是更新后的代码的样子
构建和运行
您可以从本文顶部下载 DailyJournal_v012.zip
并试用。
一切照常运作
一切照常运作,但我们开始在代码中进行一些分离,这将有助于我们实现即将添加的功能。
我们还需要什么?
我们追求的是什么?我正试图引导我们走向何方?
以下是我们已知的一些事情
-
JournalEntry 已经管理了与该条目关联的 RichEditBox。
-
我们知道我们需要生成一个文件名来保存用户的条目。
-
该文件名与 JournalEntry 相关联,因此我们希望它来完成这项工作。
-
我们知道 JournalEntry 将使用 DateCalendar 值和条目计数来创建文件名。
-
我们知道需要将文件保存到 Y-M(年-月)目录中,并且我们希望 JournalEntry 来管理它,因为它负责保存文件。
UML 最简单的介绍
以下是我们的 JournalEntry 类中需要包含内容的简单模型
那是一个非常粗略的 UML(通用建模语言)类示例。它只是一种快速沟通类包含内容的方式。
- 框的顶部是类名。
- 底部是类包含的成员。
- 破折号表示这些项都是类的私有成员。
- 加号表示该项是公共的,您可以看到 Save() 方法是公共的。
这是一个很好的小结,总结了我们所需要的东西。
保持简单
不要过度解读。图表只是一种更清晰、更快速地传达信息的方式,这也是我们使用它的原因。而且,它肯定会改变。这只是一种快速提醒我们最初目标的方式。
由于我们希望根据 CalendarView
当前所在的日期值创建条目,因此我们将把该值传递给 JournalEntry 的构造函数。
我们还需要知道 JournalEntry
应该为哪个条目编号创建,所以我们也会将其传入构造函数。
我相信有了这两个项目,我们就可以正确生成文件名,这样我们就可以让应用程序将每个条目存储到自己的文件中,而不是那个通用名称的文件 (FirstRichEdit.rtf
)。
CreateNewEntryButton_Click 是错误的
我们实际上一直在作弊,因为我们仍然让 CreateNewEntryButton_Click
做了太多的工作。它创建新条目的工作实际上是错误的,需要移动到我们的 JournalEntry 中,所以我们需要看看如何重构该代码来完成这项工作,然后再继续。
然而,这与我们在 JournalEntry
构造函数中正在做的工作也相关,所以一切都应该顺利解决,但这感觉会是相当多的工作。
我注意到这一点是因为我发现我需要将 EntryNumber
值发送到 JournalEntry
构造函数,而它目前是在 CreateNewEntryButton_Click
方法中计算的。
JournalEntry 集合
当我检查那段代码时,我发现我需要知道有多少 JournalEntry 对象。
我什么时候需要知道有多少 JournalEntry
对象?我需要知道在以下情况下:
-
应用程序加载并设置为当前日期时 - 我需要知道当前日期有多少个
JournEntry
对象,以便我可以加载相应数量的PivotItems
(每个文件一个)。 -
用户在
CalendarView
上选择新日期时,我还需要知道有多少JournalEntry
对象。但这与应用程序启动时自动选择当前日期的情况实际上是一回事。
程序功能的良好总结
我知道一个 JournalEntry
对其他任何 JournalEntry
一无所知。这是我们代码保持分离的一部分。一个 JournalEntry
只是让我能够组织与特定 JournalEntry 相关的代码。然而,我需要知道每个日期的 JournalEntry
对象的数量。我实际上必须根据在 Y-M
中找到的与用户(或应用程序启动时)选择的日期匹配的文件数量来计算 JournalEntry
对象的数量。
以下是我们所需的良好总结:
-
在
CalendarView
上选择一个日期(应用程序启动时或用户选择) -
计算相应的
Y-M
目录。 -
如果
Y-M
目录存在,则已经创建了条目。 -
确定是否有任何文件名与所选日期匹配。
-
如果
Y-M
目录不存在或没有文件名匹配,则显示一个空的条目,标题为 Entry1 -
如果
Y-M
目录确实存在并且一个或多个文件名匹配,则为每个文件生成一个PivotItem
,并将 RTF 数据加载到关联的RichEditBox
中。
决定工作在哪里完成
作为一名软件开发人员,你将要做的主要事情之一是确定工作在哪里完成——你应该在哪个对象内部编写代码来解决问题。
起初,我们让 Page
对象做所有事情,因为我们基本上遵循了“搞定它”的编码方法。现在,我们开始看到不同的对象应该有不同的职责。这甚至可以追溯到一种旧的类设计方法,称为 CRC 卡片。
CRC 卡片
类-职责-协作卡片是一种进行基本设计的方法
-
类 - 你的程序将分为哪些类
-
职责 - 该类将负责的基本功能
-
协作 - 该类将与其他哪些对象交互
这是一种很好的思考程序的方式。
在我们的例子中,我曾认为 Page
类会与 JournalEntry
协作,但现在我看到了不同的情况。
我发现我们确实需要 Page
类与 JournalEntry
对象的集合进行交互。这将使其更容易管理,因为 Page
将简单地对 JournalEntryCollection
说:“你有一个或多个 JournalEntries 吗?”
如果没有一个或多个,那么 Page
类将简单地加载空的 Entry1 Pivot
并完成。如果有一个或多个 JournalEntries,那么 Page 类将简单地为每个 JournalEntry 加载一个 Pivot
。
开始变得更有意义:开发人员的理解很重要
这使得事情变得更有意义,因为现在 Page
的职责仅仅是显示 JournalEntry
对象。由于 Page
实际上是一种视图类型的对象,这似乎很有道理。我们其余的用于确定是否存在文件等代码将从页面中分离出来,放入另外两个类中:JournalEntry
和 JournalEntryCollection
。
集合类型类的命名
我将把我们的 JournalEntry
集合命名为 JournalEntries
。将其写成复数形式是一种约定,它将向未来的开发人员表明这是一个 JournalEntry
集合。这只是一种约定(做事的方式),您可以遵循或不遵循,但它确实效果很好。
本章:设计/OOP 理论讨论较多
由于本章需要大量的代码设计并学习什么是代码设计,因此它变得相当长。我们现在将继续添加新的 JournalEntries
类,然后结束本章,以便您(和我)可以休息一下。此外,我们对代码的最后更改仍然允许应用程序构建和运行,而我们的下一次重构(在 JournalEntries
中编写代码)将大大改变代码,所以现在是休息的好时机。
下一篇文章:JournalEntries 功能
在下一篇文章中,我们将实现我之前提到的所有功能,以便应用程序能够:
-
确定每天存在的条目数量
-
显示每天的相关条目
-
允许用户使用新的
Y-M
目录和正确计算的文件名保存新条目。
添加 JournalEntries 类
-
转到解决方案资源管理器
-
右键点击您的项目
-
将出现一个菜单——向下移动到[添加]菜单项
-
将出现另一个菜单——向下移动到[类…]菜单项并点击它
在[名称:]中输入 JournalEntries.cs
点击[添加]按钮,类将被添加到您的项目中,Visual Studio 将打开并显示该类,这样我们就可以为第 9 章做好准备了。
下次见
第 9 章下次见。
我将在下一篇文章的顶部提供代码下载,其中将包含这个空的新的类(JournalEntries),以便读者能够下载第一个压缩包并开始阅读文章。
历史
2017-12-01:首次发布