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

MRU (最近最少使用) WPF 控件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2017年8月23日

CPOL

10分钟阅读

viewsIcon

18263

downloadIcon

557

实现一个 WPF/MVVM 控件库(带后端),用于管理最近最常使用的文件列表。

目录

引言

本文档介绍了 MRU (最近最常使用) WPF 控件的实现。这里应用的架构模式遵循 MVVM 模式。

背景

MRU 控件是一种用户界面组件,可帮助用户跟踪最近使用的文件。也就是说,此类控件提供列出文件的机制,并以最近使用的文件列在不那么最近使用的文件之前的样式来实现。

还有一些文件是用户需要定期(例如,每周)访问的 - 这些文件可能比用户昨天使用的文件更重要。

对我来说,MRU 控件非常重要,因为我不想每次想做笔记或查找某个目录时都要在本地或远程文件系统中查找路径和文件。因此,我积累了一些实现不同版本 MRU 控件的经验(Codeplex 上的 MRUYalvLibCodeplex 上的 Edi),而本文讨论的 MRULib 版本受到了 Visual Studio 2017 开始页中 MRU 的强烈影响。

MRULib 控件库可在 Nuget 和 GitHub 上获得,希望能够帮助我以及其他人以直观、一致且流畅的方式实现现代应用程序的这一部分。

要求

以下是所需功能简要概述

  • 提供异步和非异步函数,通过 XML 文件或字符串保存/加载其数据。
     
  • 新固定和新未固定的条目顺序:
    • 新添加或更新的未固定条目应显示在迄今为止显示的未固定条目上方
    • 新固定的条目应显示在迄今为止固定的条目下方
  • 列表中的项目可以按上次访问时间分组(固定、今天、昨天、上周)。
     
  • 按上次访问时间排序(无分组)的 MRU 菜单条目支持菜单驱动的 UI。
     
  • 固定条目可以在列表中上下移动,以调整出现顺序。
     
  • 列表中的每个条目都可以根据其年龄删除(例如,删除所有比 1 周大的条目)
     
  • MRU 控件应支持浅色或深色主题,以供在现代应用程序中使用。
     
  • MRU 列表中的条目数可以限制,以确保保留的项目数量在有用的最大值内(我需要一个包含 1000 个条目的 MRU 列表,但不确定 45 个条目是否足够,还是 256 个?)。
     
  • 每个项目的路径和文件名部分显示,使得驱动器部分和文件名部分始终可见,除非空间不足,在这种情况下,至少显示文件名。
     
  • 每个路径和文件名在整个列表中只应出现一次。
     
  • 固定是一个图形元素,提示了书签条目的功能。
     
  • 通过单元测试验证需求,以确保现在和未来的开发稳定性。

架构


MRULib 库中包含的类和接口概述。

接口

该库实现了以下主要接口

MRUEntrySerializer 类包含使用 XML 将 MRU 设置保存/加载到文件/字符串的方法。还有

  • 模型(pojo 对象)到 ViewModel 的
  • ViewModel 到模型(pojo 对象)的

转换方法,以支持保存和加载 MRU 数据的其他方式。

IMRUEntryViewModel 接口的访问提供了表示 MRU 条目的对象的属性和方法。MRU 主要是一个表示文件路径的对象。此条目还包含

  • 最后一次使用文件的时间(参见 LastUpdate 属性),
  • 该项目当前是否被固定(参见 IsPinned 属性),以及一个计算值
  • 该项目属于哪个组(参见 GroupItem 属性)。

在此需要注意的是,IsPinned 属性表示一个 int 值而不是通常的 boolean 值。此设计决策的原因将在下面的视图部分详细介绍。

IMRUListViewModel 接口的访问提供了用于管理 IMRUEntryViewModel 条目列表的属性和方法。每个条目都由其路径和文件名作为键,因此在集合中只出现一次。它主要实现了

  • 一个可观察集合(Observable Collection),可以将其直接绑定到自定义 视图,以及
  • 用于操作 MRU 列表的各种方法。

该库的 (Nuget)用户主要与上述两个接口进行交互。该库的设计遵循 四人帮模式,因为外部用户只能通过 MRULib.MRU_Service 类的静态方法和 MRUEntrySerializer 类间接创建实现上述接口的对象。

我们可以使用 MRULib.MRU_Service 类来实例化 MRU 列表对象(绑定视图到它)、使用其方法操作列表,并通过 MRU_Service 和上述接口中提供的方法创建或编辑条目。

上述 MRU 列表对象仅支持按路径和文件名排序(和键)

ObservableDictionary<string, imruentryviewmodel=""> Entries { get; } </string,>

那么,我们如何实现前面提到的关于分组和上次访问时间的排序要求呢?这一点可以在视图部分(库外部)实现,并在下面的部分中详细说明。

View

CollectionView

WPF 中一个相对较新的功能是能够通过绑定到 CollectionView 来对列表视图中的条目进行排序和分组。这个功能非常有趣,因为它允许我们以完全不同的方式查看相同的数据。


MRU 列表控件的示例视图,带有用于IsPinned(蓝色)GroupType(绿色)属性的调试值。

上面的截图提示了如何实现关于固定和未固定条目顺序的相反要求。这是可能的,因为下面的 CollectionView 实际上可以按多个参数进行分组和排序。我们可以看到,我们首先按 isPinned 属性排序——在每个组内——然后按 LastUpdate 属性排序。但是,isPinned 属性在固定组中有所不同,这导致它在所有其他组中成为一个中性排序元素,在这些组中 LastUpdate 属性本身决定了条目的顺序。isPinned 属性在固定组中有所不同,这导致它在所有其他组中成为一个中性排序元素,在这些组中 LastUpdate 属性本身决定了条目的顺序。

<CollectionViewSource Source="{Binding MRUFileList.Entries}" x:Key="collViewEntries"
                      IsLiveGroupingRequested="True">
    <CollectionViewSource.SortDescriptions>
        <!--This will sort groups-->
        <scm:SortDescription PropertyName="Value.GroupItem.Group" Direction="Ascending" />

        <!--This will sort items-->
        <scm:SortDescription PropertyName="Value.IsPinned" Direction="Ascending"/>
        <scm:SortDescription PropertyName="Value.LastUpdate" Direction="Descending"/>
    </CollectionViewSource.SortDescriptions>
    <CollectionViewSource.GroupDescriptions>
        <dat:PropertyGroupDescription PropertyName="Value.GroupItem.Group" />
    </CollectionViewSource.GroupDescriptions>
</CollectionViewSource>


应用程序菜单部分中 MRU 项目的示例屏幕截图。

上面的屏幕截图以完全不同的方式显示了 MRU 条目列表,因为元素仅按上次访问时间排序(不分组)。

<Menu xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
      xmlns:dat="clr-namespace:System.Windows.Data;assembly=PresentationFramework"
      Grid.Row="0">
    <Menu.Resources>
        <CollectionViewSource Source="{Binding  MRUFileList.Entries}" x:Key="LastUpdateViewEntries"
                              IsLiveGroupingRequested="True">
            <CollectionViewSource.SortDescriptions>
                <!--This will sort items-->
                <scm:SortDescription PropertyName="Value.LastUpdate" Direction="Descending"/>
            </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>
    </Menu.Resources>
    <MenuItem Header="File">
    <MenuItem ItemsSource="{Binding Source={StaticResource LastUpdateViewEntries}}"
          Header="Recent Files"
          Visibility="{Binding Path=MRUFileList.Entries.Count, Mode=OneWay, Converter={StaticResource zeroToVisibilityConverter}}">
    <MenuItem.ItemContainerStyle>
        <Style TargetType="MenuItem" BasedOn="{StaticResource {x:Type MenuItem}}">
            <Setter Property="Header" Value="{Binding Value.DisplayPathFileName, Mode=OneWay}" />
            <Setter Property="Command" Value="{Binding Path=Data.NavigateUriCommand, Source={StaticResource AppDataContextProxy}}" />
            <Setter Property="CommandParameter" Value="{Binding Value.PathFileName, Mode=OneWay}" />
            
            <Setter Property="ToolTipService.ShowOnDisabled" Value="True" />
            <Setter Property="ToolTip" Value="{Binding Value.PathFileName}" />
            <!-- Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}" / -->
        </Style>
    </MenuItem.ItemContainerStyle>
</MenuItem>
...
</Menu>

到现在应该很清楚,这种分组和排序不仅可以应用于 GridView 或 ListView,还可以应用于任何支持 ItemsControl 的视图。这再次显示了 WPF 控件和构件固有的灵活性

MRULib 附带的控件

库中的 MRULib.Controls 命名空间包含一些有趣的控件,它们也可以在没有 MRU 部分的情况下使用

CheckPin 控件

CheckPin 控件是一个派生自 CheckBox 控件的自定义控件。此控件实现了 MRU 控件中显示的固定(pin)。它在 XAML 中有一个自定义 UI 定义,并且只实现了一个额外的 IsMouseOverListViewItem 依赖属性。此属性用于告知控件鼠标何时位于给定的 MRU 列表项上,这又会触发显示褪色的水平固定(pin),如果条目尚未被固定。这是通过 MRU 控件视图中的以下 XAML 部分实现的。

<ControlTemplate>
...
<ctrl:CheckPin Grid.Column="1" Margin="0,0,12,0"
                 HorizontalAlignment="Right"
                 Name="checkPin"
                 IsChecked="{Binding Value.IsPinned, Mode=OneWay,UpdateSourceTrigger=PropertyChanged, Converter={StaticResource IntToBoolConverter}}"
                 Command="{Binding Path=Data.ItemIsPinnedChanged, Source={StaticResource DataContextProxy}}"
                 CommandParameter="{Binding Key}"
              />
    </Grid>
    <ControlTemplate.Triggers>
        <Trigger Property="IsMouseOver" Value="True" >
            <Setter TargetName="checkPin"  Property="IsMouseOverListViewItem" Value="True" />
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

我还没有尝试过,但我想,类似的 XAML 也可以用来在应用程序的菜单项部分显示和使用固定(pin)。

FileHyperlink

FileHyperlink 控件明确设计用于文件系统引用,但也可以轻松用于其他应用程序(例如,网页链接),因为命令部分始终可用于覆盖通过 NavigateUri 依赖属性实现的默认行为。点击具有 NavigateUri 属性绑定的 FileHyperlink 会通过静态方法:MRULib.MRU.Models.FileSystemCommands.OpenInWindows 调用关联的 Windows 应用程序。

但也可以绑定 Command 和可选的 CommandParameter。这种情况会导致覆盖前面提到的 NavigateUri 绑定(NavigateUri 被忽略),并调用 Command 绑定后面的已绑定命令。这种情况可以实现任何应用程序设计者认为合适的所有自定义操作。

FileHyperlink 当然还有一个 Text 依赖属性来控制链接中显示的文本部分。它还具有标准的命令绑定和一个默认的上下文菜单项,用于

  • 将文本部分复制到 Windows 剪贴板(通过 CopyUri),
  • 在 Windows 资源管理器中打开包含的文件夹(OpenContainingFolder),以及
  • 在关联的 Windows 应用程序中打开文件(NavigateToUri)。

这 3 个标准文件系统功能是在一个单独的类 MRULib.MRU.Models.FileSystemCommands 的静态方法中实现的,该类也可以在没有 MRU 控件的情况下使用。

PathTrimmingTextBlock

PathTrimmingTextBlock [1] 是一个基于标准文本块控件的控件,它增加了测量可用空间末端并根据可用 UI 空间重新适应文本的能力。

假设“|”字符表示可用空间的限制,并且我们要显示一个路径,例如
'C:\Photos\My Collection\Asia\2003\Japan\Tokio\BulletTrain.jpg'。PathTrimming 控件会这样显示此内容

  1. |..BulletTrain.jpg|
  2. |C:\...\BulletTrain.jpg|
  3. |C:\Photos\...\BulletTrain.jpg|
  4. |C:\Photos\My Collection\Asia\2003\...\BulletTrain.jpg|
  5. |C:\Photos\My Collection\Asia\2003\Japan\Tokio\BulletTrain.jpg|

…假设在 1 中可用空间最小,而在最后一个版本中足够大。

当 UI 空间可能太小而无法容纳所有文本时,可以使用此控件。显示部分文本(也许完整文本作为工具提示或下拉列表)可能比显示带有滚动条的完整文本提供更好的用户体验。

PathTrimmingFileHyperlink

PathTrimmingFileHyperlink 结合了前面提到的 FileHyperlinkPathTrimmingTextBlock 控件的功能。此控件提供了在各种可用空间中显示文本的功能,同时使用户能够与链接项进行交互。

Using the Code

附加代码的使用相当简单。只需下载压缩包,解压缩并在 Visual Studio 2017 Comunity 或更高版本中打开。您可能需要为单元测试恢复 Nuget 包,但除此之外,您应该能够立即编译和执行 MRUDemo 项目。

MRULib 项目位于一个单独的 GitHub 存储库中,未来的开发将在那里进行。普通用户应该使用相应的Nuget 包,因为当新版本和修复可用时,它将被更新(有关 Nuget 的高级示例实现,请参阅 Edi 项目)。

结论

本文总结了实现现代 MRU 列表控件的所有重要元素。我提供了清晰一致的实现和使用指南,希望这能使其他人能够在自己的应用程序中重用所列组件和技术。

我们已经学会了通过依赖属性将 ListViewItems 在 MouseOver 时与 CheckPin 控件链接,如上所述。使用此处描述的 CollectionView 方法进行排序和分组是另一种以各种不同方式显示相同数据的绝佳示例。

参考文献

© . All rights reserved.