编程 Windows 10 桌面:UWP 聚焦 (N 中的第 12 部分)
开始使用 UWP (从 WinForm 迁移) 第 12 章 使用 ListView - 通过 XAML 设置 ListView 样式,在 C# 中使用数据绑定和匿名类型。
- 下载 DailyJournal_v024.zip - 126.1 KB
- 下载 DailyJournal_v025.zip - 126.1 KB
- 下载 DailyJournal_v026.zip - 126.1 KB
- 下载 DailyJournal_v027.zip - 126.4 KB
- 下载 DailyJournal_v028.zip - 126.4 KB
引言
这是 UWP (通用 Windows 平台) 桌面开发持续进行的系列文章。
第 12 章是重量级的,篇幅为 31 页,包含 35 张截图。
一切都始于以下章节:
编程 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 部分)[^]
编程 Windows 10 桌面:UWP 聚焦 (N 中的第 11 部分)[^]
亚马逊提供打印版或 Kindle 版
您也可以在亚马逊上购买本书的前 8 章打印版或 Kindle 版。
通过 UWP 编程 Windows 10:学习为桌面编程通用 Windows 应用 (编程 Win10) [^]
背景
正如承诺的,在本章中我们将开始设置 ListView
,以便它能够
- 查找所有现有条目 (基于选定的月份)
-
计算每天找到的现有条目数
-
为有 1 个或更多条目的每一天,将一个日期项和一个计数添加到
ListView
中。
ListView XAML 设计研究
当我开始使用 ListView
来写本章时,我在网上到处搜索。 我找不到任何好的简单示例来演示如何用简单的标题设置 ListView
,以及如何格式化 ListView
项目使其看起来美观。
我找到了 Microsoft 的 Github 示例,但由于 Visual Studio 2017 和 ListView 示例与其他众多示例混在一起,我无法编译它们。 学习技术性的东西有时就是这样:你找不到一个简单的示例来帮助你入门。
我喜欢循序渐进地积累知识,但很多时候你只能看到别人如何解决某个特定问题的非常具体的例子 (在本例中是 ListView
)。
图形元素的快照未显示
令我沮丧的是,我经常会找到一些示例,其中根本没有显示最终外观的快照。 我无法仅凭查看 XAML 就知道该示例是否是我想要的。 这让人有点沮丧。
考虑到所有这些,我们的 ListView
示例将从我们放在 Page
上的最基本控件开始,一直构建到最终结果。 我还将提供控件在每个步骤中变化的快照,以便您知道它应该是什么样子。
这将使您能够理解它的工作原理,然后随着您学习更多知识来扩展您的知识。 这比仅仅将一个示例放在 Page
上并希望它能起作用要好得多。
获取当前代码
如果您需要上一篇文章中的代码,请获取 DailyJournal_v023
并进行编译,您就能跟上进度了。
EntriesListView 的当前状态
由于我们在 DailyJournal 应用中没有对 ListView
做太多处理,因此控件 XAML 非常基础。
<ListView Name="EntriesListView" Grid.Row="2" Grid.Column="0"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
我们将向 ListView 添加一些子元素 (节点),所以我们要做的第一件事是确保 ListView 元素是块级元素 -- 具有开始节点 <ListView>
和结束节点 </ListView>
,我们将用它们来包含子元素。
我们要向 ListView
添加两项内容:
-
一个标题
-
Data
如果我们想添加静态数据,我们可以只添加一些字符串元素,条目就会出现在列表中。
<x:String>Item 1</x:String>
<x:String>Item 2</x:String>
<x:String>Item 3</x:String>
<x:String>Item 4</x:String>
<x:String>Item 5</x:String>
当然,我们希望我们的条目能够通过编程方式添加,因为应用程序会找到当月有效的条目。
我们还希望 ListView
显示两列数据。 一列是条目日期,另一列是该日找到的条目数。
它的外观将与我以前在传统 WinForms 应用中创建的有些类似。
注意:在我们继续下面的部分时,我会在控件中保留两个静态条目,以便我们能看到它们相对于其他条目的外观。 稍后我们会删除这些条目。
我们首先添加的是标题部分,它将显示“日期”和“条目数”。
ListView 提供了一种添加 HeaderTemplate 的方法,因此我们可以定义标题应该是什么样子。 HeaderTemplate 是 ListView 类的一个属性,因此我们可以轻松添加新的 XAML,如下所示:
<ListView.HeaderTemplate>
</ListView.HeaderTemplate>
在 Visual Studio 中输入开始标签并按 <ENTER> 时,结束标签会自动为您创建。 但是,您也会看到 Visual Studio 警告它认为新的 XAML 有问题。
HeaderTemplate
是一种容器类型的元素,它要求您向其中添加一个子节点 (元素)。
它特别需要 DataTemplate
元素,所以我们现在就添加该元素。
<DataTemplate>
</DataTemplate>
添加新元素后,Visual Studio 将停止抱怨缺少项。
DataTemplate 元素的名称有点奇怪
DataTemplate
这个名字对我来说有点奇怪,因为它让我想象我们要添加一个模板来样式化 ListView
包含的数据 (如 Item1 和 Item2)。 然而,模板要样式化的数据是在 HeaderTemplate
中,也就是显示在 ListView
顶部标题的文本。 当您看到我们添加 ListView.ItemTemplate
元素来样式化条目,并且该模板也包含 DataTemplate
元素时,这会更容易理解。
进一步定义我们的 ListView 设计
我们的标题将是一行两列。 第一列需要更多空间,因为它显示更多数据 (日期字符串),而第二列只需要大约 3 个字符宽就能显示到 999。
Grid 或 StackPanel
此时,我们将添加创建标题布局的元素。
两个最明显的选择是 Grid
或 StackPanel
。
我们可以使用 StackPanel
,并将其方向设置为水平,然后我们可以添加两个 TextBox
元素,每个元素都会将其关联的标题文本添加到 ListView
。
XAML 将如下所示:
<StackPanel Orientation="Horizontal">
<TextBlock FontWeight="Bold">1st Header</TextBlock>
<TextBlock FontWeight="Bold">2nd Header</TextBlock>
</StackPanel>
然而,问题在于使用 StackPanel
更难让元素利用可用空间。 您可以在图片中看到两个元素没有正确间隔。
StackPanel
适用于多种用途,但在本例中,我认为 Grid
将更容易让我们获得正确的布局。 当然,我们已经为主要布局使用了 Grid
,因此我们将执行的工作对您来说将很熟悉。
但是,我想向您展示这一点,以便您能记住 StackPanel
以备将来使用,并且因为在使用 StackPanel
与 Grid
之间存在相当大的争论。
注意:如果您将 StackPanel
粘贴到布局中,请将其删除,因为我们不会使用它。
让我们将初始 Grid
XAML 添加到我们的 DataTemplate
中。
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Foreground="#ff0000" Text="Date" />
<TextBlock Grid.Row="1" Grid.Column="1" Foreground="#ff0000" Text="Entry Count" />
</Grid>
您可以看到,我们现在有一个带有两列的标题,每列都有自己的红色标题文本。
这是因为我们创建了一个 Grid
,它有一个 Grid.RowDefinition
和两个 Grid.ColumnDefinition
。
行定义很简单,其 Height
属性设置为占据可用空间。
接下来,第一个列定义将 Width
属性设置为占据可用空间。 这将是剩余空间的大部分,因为第二列的 Width
属性设置为 Auto
-- 这将使列的宽度与文本 (条目计数) 所占用的宽度一致。
TextBlock 样式
当然,是在 TextBlock 本身之上,我们定义了 Grid.Row
和 Grid.Column
,TextBlock
应该显示在哪个位置。 我们还通过设置 Text
属性来定义将显示在 TextBlock
中的文本。 设置文本颜色很简单,只需将 Foreground
颜色设置为有效的 RGB
颜色即可。 在我们的例子中,我们将其设置为红色。
基本布局有效,但存在错误
这是一个基本布局,目前可以正常工作,但我们稍后运行程序时会遇到一个问题。
在运行应用程序之前,让我们也定义一个 ListView.ItemTemplate
,这样我们在构建和运行后就能获得更多价值。
ListView.ItemTemplate
添加 ItemTemplate
就像添加 HeaderTemplate
一样。
只需一次性添加 ListView.ItemTemplate 和 DataTemplate 元素。
<ListView.ItemTemplate>
<DataTemplate>
</DataTemplate>
</ListView.ItemTemplate>
添加这两个元素后,您会发现预览布局中的两个硬编码的 ListView
条目将消失。
现在我们可以定义 Items
的 Grid
布局,这些 Items
将添加到 EntriesListView
中。
这是我们的初始 Grid
布局:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="5*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Foreground="DarkBlue" Text="2017-12-11" />
<TextBlock Grid.Row="0" Grid.Column="1" Foreground="DarkBlue" Text="3"/>
</Grid>
Grid
的定义与我们定义 HeaderTemplate
的 Grid
完全相同。
当然,我们确实更改了单个 TextBlock
条目,这次我们将 Foreground
颜色更改为 DarkBlue
( .NET 库中预定义的 RGB
颜色)。 如果您在 Foreground 值内的引号之间输入一个字母,Visual Studio 将弹出一个智能提示对话框,允许您通过名称从预定义的颜色中选择一种颜色。
布局预览
由于设计时布局预览中没有显示任何数据,我添加了 Text
属性,并将其设置为当前日期 (2017-12-11) 和第二个列的随机值 3。
但还有另一个问题,因为正如您所见,条目计数 (3) 显示在当前日期之后,并且看起来像是日期值的一部分。
现在,让我们添加一个 Margin
值来分隔它们。
Margin="25 0 2 0"
请记住,有四个 Margin
值将设置到 TextBlock
的每一侧,从左侧开始,顺时针方向 (Left, Top, Right, Bottom
)。
至少现在您可以区分它们是两个单独的列和值了。
设置生成值的数据
当然,我们不希望将数据设置为硬编码值,所以让我们看看如何解决这个问题。
还记得我们是如何向 ListView 添加内容的吗?
首先,作为提醒,请查看 MainPage.xaml.cs
文件中 LoadEntriesByDate
() 方法的第一行。
现在,每当用户单击 MainCalendar
上的日期时,它都会向 EntriesListView
添加一个新项。
但是,我们需要告诉 ListView
绑定到该值。 我们构建在其上的框架非常出色,XAML 提供了一种方法,可以将我们的 Text
属性轻松绑定到以编程方式添加的值。
绑定语法
我们所要做的就是使用一些特殊的语法,其中包含花括号 {},编译器能够理解它。
要简单地让 ListView
绑定到存在的值,我们只需要通过设置 Text
属性来告诉它使用 Binding
,如下所示:
Text="{Binding}"
目前,我们将继续使用硬编码的条目计数 3。
添加此项的另一个好处是,一旦完成,设计布局将绑定到我们在 XAML 中设置的硬编码值,并且它们将显示出来。
构建、运行、检查
现在让我们构建应用程序并看看它的行为。
如果您没有跟上进度,请获取 DailyJournal_v024
源代码并进行编译和运行。
应用程序启动时
当您首次启动应用程序时,将添加两个硬编码的条目,然后当当前日期在 MainCalendar
中自动选中时,将生成并插入第三个条目。
之后,如果您单击 MainCalendar
上的任何其他日期,将触发 SelectedDateChanged
事件,该事件最终会调用 LoadEntriesByDate
() 方法,该方法将运行添加新项到 EntriesListView 的代码行,因此您将看到更多条目 (您可能需要滚动) 出现在 ListView
中。
如果您添加一些条目并滚动,您会发现至少有两件事有点奇怪。
-
当滚动条出现时,标题的一部分会被遮挡 (参见下图)。 我们将在 XAML 设计中修复此问题。 可能通过简单地添加一个边距。
-
当您滚动条目时,标题也会滚动并移出视图。 这是 Microsoft 默认设计的,有一些方法可以修复它,但它们需要进行大量的布局更改。 目前,我们不会对此进行更改。 它可能在手机或屏幕空间有限的设备上很有意义,但在桌面应用程序上感觉很奇怪。
需要更改的事项
在继续之前,我们应该修复几件事。
-
删除测试用的硬编码条目,使其不再出现。
-
将日期格式化为更接近我们的条目文件名 (
yyyy-MM-dd
),因为我们不需要时间,而它只是占用了用户界面 (UI) 上的空间。 -
向
HeaderTemplate
添加Margin
,以便第二个列标题不会被遮挡。
删除硬编码的 ListView 条目
首先,让我们从 XAML 中删除那两行。
您可以突出显示它们并按 [Delete] 键。
这确实意味着 ListView
中不再显示任何条目,但由于我们正朝着最终解决方案发展,所以没关系。
接下来,我们将日期格式化为与我们文件条目使用的格式匹配。 当然,由于每天都有所有文件的计数,所以我们不需要包含文件编号。
更改日期格式
打开 MainPage.xaml.cs
并在 LoadEntriesByDate
() 方法中的第一行代码处。
我们所要做的就是将格式说明符 (“yyyy-MM-DD”)
添加到 ToString
() 方法中。
EntriesListView.Items.Add(MainCalendar.SelectedDates[0].ToString("yyyy-MM-dd"));
向 HeaderTemplate 添加 Margin
我修改了两个 HeaderTemplate TextBlock
条目上的 Margins
,它们现在看起来如下:
<TextBlock Margin="7 0 0 0" Grid.Row="0" Grid.Column="0" Foreground="#ff0000" Text="Date" />
<TextBlock Margin="0 0 15 0" Grid.Row="0" Grid.Column="1" Foreground="#ff0000" Text="Entry Count" />
############################################################################
侧边栏小错误修复
注意:在下一个示例中显示的 TextBlock
代码中有一个小的错误修复。 这是因为我注意到第二个 TextBlock
元素的 Grid.Row
被设置为 1。 然而,这里没有 Row 1 (第二行)。 幸运的是,XAML 解析器会简单地忽略该值,而是根据 Grid.RowDefinition
来判断只有一行。
############################################################################
我遇到的问题:Microsoft Bug?
此时,我认为我可以运行应用程序并且一切看起来都会正确,但我发现了一个关于 ListView
中条目布局的问题。
条目数据重叠
即使我们为 ItemTemplate 使用了与 HeaderTemplate
匹配的 Grid
定义,条目布局也不会以相同的方式格式化。 这会导致问题,使得条目数据与标题不对齐。 看起来一点也不好看!
我不得不处理布局,甚至还在 StackOverflow 上发布了一个关于 HeaderTemplate
和 ItemTemplate
不匹配的问题,尽管 XAML 完全相同 (https://stackoverflow.com/questions/47775116/why-do-listview-headertemplate-and-listview-itemtemplate-uwp-xaml-display-diff)*。
*在撰写本章时,该问题的解决方案已经到来,所以我回来添加此注释。 所述解决方案要求我们添加一个新的 ListView.ItemContainerStyle
。 这会打开一个全新的潘多拉魔盒,所以目前,我将忽略该解决方案,使用我更简单 (但远不如好的) 的解决方案。
我最终确定,如果我对 ItemTemplate
列定义进行如下更改,它将接近我们期望的外观。
这是 ListView.ItemTemplate
的完整 XAML 列表,以便您确保一切都正确。 主要的变化是 ColumnDefinitions
的 Width
值。
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Margin="7 0 0 0" Grid.Row="0" Grid.Column="0" Foreground="DarkBlue" Text="{Binding}"/>
<TextBlock Margin="25 0 2 0" Grid.Row="0" Grid.Column="1" Foreground="DarkBlue" Text="3" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
构建、运行、查看
现在,如果您运行它,ListView
的外观会好得多。 如果您需要当前的代码,请获取 DailyJournal_v025
并进行编译和运行。
每次单击日期时,应用程序都会将一个条目添加到 ListView
中。 尽管应用程序最终不会这样工作,但它允许我们进行测试。
滚动条和对齐修复
滚动条不再遮挡标题,并且条目与标题的对齐方式也更加美观。
当前,我们使用 XAML 中值为 3 的值来表示条目计数。
日期值当前通过设置在 TextBlock 上的通用 {Binding}
语法绑定到条目项。
然而,我们真正想要的是一个简单的对象,它具有这两个属性 (日期、条目计数),以便我们可以轻松地将值绑定到 ListView
条目。
这将使我们在计算每天的条目数并将其插入 ListView
时更容易设置和跟踪这些值。
ViewModel 对象:基础知识
我们正在谈论的这个对象实际上只是一个 ViewModel
。 它是 View
将用于显示某些信息的某事物 (两个一起使用的值) 的 Model
。
C# 中的匿名类型
由于这个对象实际上不会在其他任何地方使用,我将创建一个匿名类型。
这将使您获得一些匿名类型的经验。
匿名类型只是一个即时生成的类型,因此它没有名称。
通常,当我们创建一个新类型时,我们会将其包装在 C# 类定义中,这会创建一个有名称的类型 (其类型名称来自类名)。
例如,如果我们查看 Page
类的顶部,我们可以看到我们将其命名为 MainPage
。
在这种情况下,MainPage
成为我们 UDT (用户定义类型) 的类型名称。
相比之下,匿名类型没有在任何地方定义为类,但编译器知道如何构建它并在内部跟踪它。
这是我们用来创建匿名类的语法:
var myAnonymous = new {name=value, name=value};
名称由开发人员提供。
值类型由放入其中的数据类型决定。 这在 C# 通常使用的静态类型 (int
、String
、bool
等) 方面被称为 dynamic
类型。
如果值是数字,则无需加引号。如果值为 String
,则需要加引号。
这里有一些额外的示例:
var thing = new {count = 5, name=”flintstone”};
创建匿名类型后,可以通过名称访问其属性。
Console.WriteLine(thing.count); // prints 5
Console.WriteLine(thing.name); // prints flintstone
由于我们希望一个对象同时保存日期和条目计数,这将非常有用。
如果您想要更多信息,有一个关于匿名类型的好读的参考资料,其中包含一些很好的示例:https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/anonymous-types
让我们在 LoadEntriesByDate
() 方法中添加我们新的匿名类型,我们在其中向 ListView
添加新项。
以前,我们只是将格式化的 String
添加到 ListView
中。
现在,我们将添加一个包含格式化 String
和条目计数 Object 的对象到 ListView。 我将把新行显示在多行上,以便它更清晰。
请注意,我们仍然在调用 Items 列表上的 Add
() 方法。
然而,现在我们添加的是一个匿名对象,它被定义为:
{date = MainCalendar.SelectedDates[0].ToString("yyyy-MM-dd"), entryCount = 12}
这个匿名对象在一个代码行中被实例化并添加到 ListView
中,但它有两个名为 date 和 entryCount
的属性。 现在我将 entryCount
设置为 12,这样您就知道我们将不再使用之前使用的值 3。
现在构建和运行:不完全正确
如果您构建并运行,您会发现代码可以构建而没有错误,并且可以运行。 但是,ListView
条目看起来不正确。
默认绑定尝试但失败
我们设置的默认绑定会尝试绑定到它能找到的任何值,因此它会抓住 ListView
中包含的匿名对象,并且默认的 ToString
() 会添加到每个对象中,以便您可以看到我们用来创建匿名对象的原始对象定义的一部分。
当然,我们仍然在条目计数 TextBlock
中有硬编码的值 3,我们也需要修复它。
修复绑定
修复绑定非常简单。 让我们切换到 MainPage.xaml
并修改 ItemTemplate TextBlocks
上的 Binding
值。
我们所要做的就是将属性名称添加到 Binding
语法中,一切都将得到修复。
现在我们的更新后的 TextBlocks
应该看起来像这样:
<TextBlock Margin="7 0 0 0" Grid.Row="0" Grid.Column="0" Foreground="DarkBlue" Text="{Binding date}" />
<TextBlock Margin="25 0 2 0" Grid.Row="0" Grid.Column="1" Foreground="DarkBlue" Text="{Binding entryCount}" />
通过这个更改,我们告诉绑定机制在任何附加对象中查找 date
属性和 entryCount
属性,并使用它来获取应该显示的文本。 当然,这些值是区分大小写的,所以它们必须与我们匿名对象中的属性名称完全匹配。
构建和运行
让我们构建此代码并运行它,看看我们的更改看起来如何。 如果您还没有,请获取 DailyJournal_v026
源代码。
现在,情况看起来正确了。
越来越接近最终功能
我们越来越接近想要的功能,但还有一些事情要做。
我发现列出我将要处理的事情的简短列表很有帮助。
-
从
LoadEntriesByDate
方法中删除将条目添加到 ListView 的代码行。-
我们将删除该行,因为它只是为了测试我们的
ListView
。
-
-
编写代码以计算当前月份中每天存在的条目数。
-
添加
ListView
的SelectedChanged
事件,以便当用户选择一个ListView
条目时,MainCalendar
会移动到该日期,并且会自动加载存在的第一个条目。
条目计数代码
计算条目的代码应该放在哪里? 首先,我们将执行实际计数和排序条目的代码放在自己的私有方法中。 我猜我们会称之为 CalculateEntryCount
()。
之后,每当用户加载新月份或保存新项时,我们都会调用 CalculateEntryCount
()。
但是,我们希望该方法由另一个方法调用,以分离所完成的工作。 CalculateEntryCount
() 将计算条目,它不会更新 ListView
。
相反,这项工作将在一个单独的方法中完成,该方法将调用 CalculateEntryCount
()。
从长远来看,这使得一切都更清晰,而且当你几周或几个月不看代码时,它仍然有意义。
我们将调用更新 ListView
UI 的方法 InitializeEntriesListView
()。
开始工作 (Get ‘R Dun / Get ‘R Workin’)
每次用户单击新日期时,都会调用 InitializeEntriesListView 方法。 目前,我不会担心月份是否已更改。 我只会每次都触发功能。 我们可以稍后使其更高效,但现在我只想让它起作用。
我首先要做的是将存根方法 (没有实现的方法 -- 没有代码) 添加到 MainPage.xaml.cs 文件中,这样我们就可以填充它们了。
首先,我们将调用 InitializeListView
() 添加到 MainCalendar_SelectedDatesChanged
方法中。
InitializeEntriesListView();
现在我们可以确保每次用户选择新日期时都会调用该方法。 该方法在应用程序加载时也会被触发,这意味着应用程序启动时它将使用可用的条目列表初始化 ListView
。
现在,我在 MainPage.xaml.cs
文件中腾出了一些空间,并添加了我们两个新的空方法。
private void InitializeEntriesListView()
{
}
private Dictionary<string,int> CalculateEntryCount()
{
}
由于我同时处理这两个方法,因此将它们放在文件中是很方便的。
当然,Visual Studio 知道 CalculateEntryCount
() 方法将返回一个 Dictionary
(稍后详细介绍),并且它会警告说该方法中缺少 return 语句,显示为红色的波浪线。
CalculateEntryCount 实现
我花了一些时间来弄清楚代码,并且已经写好了这个方法。 首先,这是 CalculateEntryCount
() 方法所做的事情的摘要。
高层概览
-
确定当前年月文件夹
Y-M
是否存在。如果不存在,则该月没有文件,无需计算。 -
如果
Y-M
文件夹存在,则该月至少有一份文件,因此它会找到当前月份文件夹中的所有文件。 -
找到所有文件后,需要对它们进行组织,以便于分组 (计数) 和使用 (创建新的匿名对象添加到
ListView
)。 -
它存储文件日期,并将它们及其关联的计数存储在
Dictionary
集合中,并将集合返回给调用方法 (在本例中为InitializeEntriesListView
())。
代码详细说明
这是完整的 CalculateEntryCount
() 方法。
private Dictionary<string,int> CalculateEntryCount()
{
String ymfolder = YMDDate.Substring(0, 7);
Dictionary<string, int> allEntries = new Dictionary<string, int>();
if (Directory.Exists(Path.Combine(appHomeFolder.Path, ymfolder)))
{
String[] allCurrentFiles = Directory.GetFiles(
Path.Combine(appHomeFolder.Path, ymfolder),
"*.rtf",
SearchOption.TopDirectoryOnly);
foreach (string f in allCurrentFiles)
{
int x = 0;
// if the date key is already in the collection x will be > 0
string strippedFileName = Path.GetFileName(f).Substring(0, 10);
allEntries.TryGetValue(strippedFileName, out x);
if (x == 0)
{
allEntries.Add(strippedFileName, 1);
}
else
{
allEntries[strippedFileName] = ++x;
}
}
}
return allEntries;
}
此方法中迭代文件的代码与我们在 LoadEntriesByDate()
中之前看到的代码完全相同。 但是,从第 121 行开始的、在 foreach 循环内部的新代码是新颖且有趣的,所以让我们来谈谈它。
在第 121 行,我创建一个局部临时变量 (x
),并在每次循环时将其设置为 0,因为我需要它每次都初始化。
在第 123 行,我只提取文件名 (没有路径信息),然后对文件名进行 Substring 操作,这样我们就不会有任何条目号信息。 我只获取前 10 个字符 (yyyy-mm-dd
)。
然后,我在第 124 行使用我们的 Dictionary 集合,该集合是我在方法顶部准备好的。
Dictionary
是一个由 Key
和 Value
组成的通用集合。 由于它是一个 Dictionary
,Key
必须是唯一的。 它不允许您多次添加具有相同 Key
的项。
这是一个通用的 Dictionary
,因为您可以设置 Dictionary
将包含的 Key
和 Value
的类型。 在我们的例子中,我将 Key 设置为 String,将 Value 设置为 Integer。
我依赖于 Dictionary
Key
需要一个唯一的键,以便我可以创建一个键控 (唯一) 的文件名/日期集合。 然后,每次找到相同的键时,我都会递增值。
稍后,我会更清晰地解释这一点。
Dictionary.TryGetValue() 方法:工作原理
在第 124 行,我调用 Dictionary 的一个名为 TryGetValue 的方法,该方法接受两个参数:
-
您要在
Dictionary
中搜索的Key
-- 这是我们的String
日期,如 2017-12-05。 -
您通过
out
变量传入的值 -- 我传入的是整数x
变量。
如果 TryGetValue
在 Dictionary
中找不到 Key
,它不会返回任何内容,x
仍将是其初始值 0。
这就是为什么在第 125 行我检查 x
是否等于 0。 如果是,则向 Dictionary
添加新项,值为 1 (在第 127 行)。
所以,第一次找到文件时,对于日期 2017-12-05,它将插入 Key
"2017-12-05" 并将 value
设置为 1。
这表明我们找到了一份于 2017-12-05 创建的文件。
但是,如果存在多个具有该相同前缀 (2017-12-05) 的文件,则 TryGetValue 将整数值返回到 out 参数 x。
所以,下一次循环时,当它找到一个文件,其前缀为 2017-12-05 时,TryGetValue 将返回 x 的值 1。
当它返回 x 的值 1 (或任何不等于 0 的值) 时,我们将进入 else 块和第 131 行的代码行。
第 131 行将获取具有 String Key 值 2017-12-05 的当前对象,并将其 Value 设置为 ++x。 这就是 x 增加 1。
这将构建一个 allEntries Dictionary 中的完整条目集,看起来可能像这样:
Key = 2017-12-05, Value=3 Key = 2017-12-14, Value=1 Key = 2017-12-18, Value=1
既然我们已经完成了困难的部分,我们所要做的就是迭代它们并将它们添加到 ListView
中。
InitializeEntriesListView:显示
此方法将执行以下操作:
-
清空
EntriesListView
-- 因为它每次都会运行,所以需要清除旧条目。 -
迭代
CalculateEntryCount
() 返回的条目列表。 -
从每个返回的条目创建一个匿名对象。
-
将每个匿名对象添加到
EntriesListView
。
这是代码:
private void InitializeEntriesListView()
{
EntriesListView.Items.Clear();
foreach (var item in CalculateEntryCount().OrderBy(f => f.Key))
{
EntriesListView.Items.Add(
new
{
date = item.Key,
entryCount = item.Value
}
);
}
}
我高亮显示了创建并传递到 Items.Add()
方法中的匿名对象,以使其更加突出。
您可以看到,在第 104 行,我们清空了当前的 EntriesListView,所以所有条目都会被删除,因为我们要重新加载它。
接下来在第 105 行,我们通过 CalculateEntryCount
() 返回的项进行 foreach 循环。 我们将这个 Dictionary 中的每个对象称为 item,并对 Dictionary
调用一个 OrderBy
扩展方法,该方法将 Dictionary
排序,以便较低的日期值 (如 12 月 4 日在 12 月 5 日之前) 会先在 EntriesListView
中列出。
在 foreach 循环内部,我们使用 item 变量来创建具有两个命名属性 (date
和 entryCount
) 的新匿名对象。 我们从 item.Key
获取日期,从 item.Value
获取 entryCount
。
现在,当我们运行代码时,为一个月找到的所有条目都将列在 EntriesListView
中。
构建并启动!
构建代码并尝试一下。 如果您需要所有更新,请获取 DailyJournal_v027
。
您可以看到,每个日期的条目还包含一个条目计数。 例如,2017-12-07 的条目计数为 3。 如果我们在 MainCalendar 中单击该日期 (我已经这样做了),您会发现实际上有三个条目在右侧。
哦,还有一件事
一切都很顺利。 除了,还有一件事。
您应该能够单击 ListView
条目,使其移动到您选择的日期,并在右侧加载条目。 这很容易编写代码,所以让我们来做,结束本章。
我们希望代码在用户更改 EntriesListView
上的选择时运行。 那就是 ListView
的 SelectionChanged
事件。
您可以
-
转到
MainPage.xaml
并选择 XAML 中的EntriesListView
。 -
在
Properties
对话框中单击[闪电]图标。 -
向下滚动到
SelectionChanged
事件。 -
双击
SelectionChanged
事件旁边的TextBox
。
Visual Studio 将打开 MainPage.xaml.cs
并添加新方法 (EntriesListView_SelectionChanged
),并将光标放在括号内,以便您可以开始输入代码。
这是我们需要的简单代码:
if (e.AddedItems.Count > 0) {
dynamic currentAnon = e.AddedItems[0];
MainCalendar.SelectedDates.Clear();
DateTime dt = DateTime.Parse(currentAnon.date);
MainCalendar.SelectedDates.Add(dt);
}
当此方法被操作系统调用,因为用户在 ListView
中进行了新的选择时,子系统会提供一个名为 e 的参数。 该参数包含一个名为 AddedItems
的项数组。 有时,即使没有添加项,该方法也可能被调用,所以我们必须检查 Count
是否大于 0 -- 即集合中是否有项。 一旦确认,我们只关心第一项 (索引 0),所以我们获取它,并将其保存在一个 dynamic
变量中。
dynamic
是一个关键字,它告诉编译器我们有一个类型,并且我们希望它弄清楚其中包含什么。 由于我们之前将匿名类型添加到 ListView
中,我们知道它们被定义为 <string, int>
,因此我们知道如何获取它们的值。
在第 242 行,我们清除 MainCalendar
上可能当前选中的所有日期。 这会为日历做准备,因为我们将以编程方式选择一个日期。
接下来在第 242 行,我们使用一个名为 Parse
的静态方法,它是 DateTime
对象的一部分,可以轻松解析匿名类型的 date
属性 (一个字符串) 回到日期。 我们将该值保存在局部临时变量 dt
中。
最后,在第 243 行,我们使用该日期将其添加到 MainCalendar
的名为 SelectedDates 的集合属性中。 当我们添加该值时,它就成为新选定的日期 -- 与 EntriesListView 中的值匹配。 当 ListView
中选中该新日期时,MainCalendar_SelectedDatesChanged
事件将被触发,并运行我们的代码以加载所有可用条目。 现在用户可以选择条目列表视图中的一项,以便她可以轻松地浏览现有条目。
你一定要试试
构建并运行它,它现在可以像一个真正的应用程序一样工作了。 导航和管理您的条目非常容易。 当然,您仍然无法从应用程序中删除它们。 还有工作要做。 :)
获取 DailyJournal_v028
源代码并尝试一下。
现在,当您单击任何 EntriesListView
项时,关联的日期将在 MainCalendar
上被选中,并且该日期的条目将被加载。
此外,如果您循环浏览月份并选择该月中的任何一天,EntriesListView
将被重新初始化,您将能够看到该月份中有哪些天有条目。
它提供了一种很好的方式来浏览可用的条目。
历史
2017-12-12:第一个发布的第 12 章在 12 号 (12)。 xD