Index
This article documents a MRU (Most Recently Used) WPF control implementation. The architecture pattern applied here follows the MVVM pattern.
A MRU control is a User Interface coponents that helps users keeping track of recently used files. That is, such a control offers mechanisms to list files and implements it ideally in fashion where files that are most recently used are listed before less recently used files.
There are also files that users need to access on a regular base (eg.: weekly) - and these files may even be more important than those that users were using yesterday.
An MRU control is for me a very important control since I cannot be bothered looking up paths and files in the local or remote file system evertime I want to make a note or find a certain directory. Therefore, I have a bit of history implementing different versions of MRU controls (MRU on Codeplex, YalvLib, Edi on Codeplex) and the MRULib version discussed here is strongly influenced by the MRU in the Start Page of Visual Studio 2017.
The MRULib control library is available on Nuget and GitHub and should hopefully help me and others to implement this part of a modern application in an intuitive, consistent, and fluent manner.
Here is a short overview of the required features:
- Provide an async and non-async function to save/load its data via file or string using XML.
- Order of new pinned and new unpinned entries:
- A newly added or updated unpinned entry should be displayed above the unpinned entries displayed so far.
- A new pinned entry should be displayed below the entries pinned so far.
- Items in the list can be grouped by last access (Pinned, Today, Yesterday, Last Week).
- An MRU menu entry sorted by last access (without grouping) supports menu driven UI's.
- A pinned entry can be moved up and down in the list to tune the order of appearance.
- Every entry in the list can be removed based its age (e.g. Remove all entries older than 1 week)
- The MRU control should support Light or Dark theming for use in modern applications.
- The numer of entries in the MRU list can be limited to make sure that the number of kept items is at a useful maximum of items (I do need an MRU List with 1000 entries but I am not sure if 45 entries are enough or rather 256?).
- The path and file name portion of each item is displayed such that the drive portion and the filename portion are visible at all times, unless the space is insufficient, in which case at least the filename is shown.
- Each path and filename should occur only once in the entire list.
- A pin is a graphical element that hints the function of bookmarking an entry.
- Requirements are verified with unit tests to ensure stability now and in the developing future.
An overview of the classes and interfaces shipped in the MRULib library.
The library implements the following main interfaces:
The MRUEntrySerializer class contains methods for saving an loading MRU settings to/from a file/string using XML. There are also:
- Model (pojo objects) to ViewModel and
- ViewModel to Model (pojo objects
conversion methods to support other ways of saving and loading the MRU data.
The IMRUEntryViewModel interface provides access to properties and methodes of an object that represents a MRU entry. An MRU is mainly an object that represents a path to a file. This entry also contains the information:
- when the file was used for the last time (see
LastUpdate
property),
- whether the item is currently pinned or not (see
IsPinned
property) and as a computed value:
- what group this item belongs to (see
GroupItem
property).
It is important to note here that the IsPinned
property represents an int
value and not a boolean
value as usual. The reason for this design decision is detailed in the View section below.
The IMRUListViewModel interface provides access to properties and methods that are used to manage a list of IMRUEntryViewModel entries. Each entry is keyed by its path and filename and can, therefore, occur only once in the collection. It implements mainly:
- an Observable Collection of Entries that can directly be bound to a custom View and
- various methods that can be used to manipulate the MRU list.
A (Nuget) user of the library will mostly interact with the 2 interfaces above. The library design follows the Gang of Four pattern since outside users can create objects that implement the above interfaces only indirectly via static methods of the MRULib.MRU_Service class and the MRUEntrySerializer
class..
We can use the MRULib.MRU_Service class to instantiate the MRU list object (bind a view to it), -manipulate the list with its methods, and create or edit entries with the provided methods in the MRU_Service and mentioned interfaces.
The above MRU list object supports only sort (and key) by path and filenames:
ObservableDictionary<string, imruentryviewmodel=""> Entries { get; } </string,>
So, how can we implement the sort requirements towards grouping and last time of access as stated above? This point can be implemented in the view section (outside of the library) and is clearified in the next section below.
The CollectionView
A rather new feature in WPF is the ability to sort and group entries in a listview by binding to a CollectionView. This feature is quite interesting, because it allows us to view the same data in a completely different way:
A sample view of the MRU List control with debugging values for IsPinned (blue) and GroupType (green) properties.
The screenshot above gives us a hint as to how it is possible to implement the opposing requirements on the order of pinned and unpinned entries as listed above. This was possible because the CollectionView shown below can actually group and sort by multiple agruments. We can see here that we sort by the isPinned
property -- within each group -- first -- and then by the LastUpdate
property. But the isPinned
property is different only in the Pinned group. which results in being a neutral sort element in all other groups where the LastUpdate
property determines the order of entries by itself.
<CollectionViewSource Source="{Binding MRUFileList.Entries}" x:Key="collViewEntries"
IsLiveGroupingRequested="True">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Value.GroupItem.Group" Direction="Ascending" />
<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>
A sample screenshot of MRU items in the application's menu section.
The above screenshot shows the list of MRU entries in a completely different fashion since elements are (not grouped) sorted by their last time of access only:
<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>
<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}" />
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
...
</Menu>
It should be clear by now that grouping and sorting in this fashion, cannot only be applied to a GridView or ListView, but to any view that supports an ItemsCountrol. This again shows the inbuild flexibility of WPF controls and artifacts.
The MRULib.Controls namespace in the library contains a few interesting controls that could also be used without the MRU portion:
The CheckPin Control
The CheckPin control is a custom control derived from a CheckBox control. This control implements the pin displayed in the MRU control. It has a custom UI definition visible in its XAML and implements only one more IsMouseOverListViewItem dependency property. This property is used to tell the control when the mouse is over a given MRU listview item, which in turn triggers the display of the faded horizontal pin, if an entry is not pinned, yet. This is realized with the following XAML portion in the MRU control view:
<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>
I have not tried it, yet, but I guess, a similar XAML could be used to display and use a pin in the menu items section of the application, as well.
FileHyperlink
The FileHyperlink control is explicitely designed for usage with file system references, but it can easily be used for other applications (e.g.: links to web pages), as well, since the command portion can always be used to override the default behaviour which is implemented via the NavigateUri dependency property. A click on a FileHyperlink that has its NavigateUri property bound results in the invocation of the associated Windows application via static method: MRULib.MRU.Models.FileSystemCommands.OpenInWindows.
But it is also possible to bind a Command and optional CommandParameter. This case leads to overriding the previously mentioned NavigateUri binding (NavigateUri is ignored) and invoking the bound command behind the Command binding. This case can implement any custom action any application designer might see appropriate.
The FileHyperlink boosts of course a Text dependency property to control the Text portion being displayed in the link. It has also standard command bindings and a default Context Menu entry to:
- Copy the Text portion into the Windows Clipboard via (CopyUri),
- Open a containing folder in Windows Explorer (OpenContainingFolder), and
- Open a file in the associated Windows application (NavigateToUri).
These 3 standard file system functions are implemented in static methods of a seperate class MRULib.MRU.Models.FileSystemCommands
, which could be used without the MRU controls, as well.
PathTrimmingTextBlock
A PathTrimmingTextBlock [1] is a control that is based on the standard textblock control with the added ability to measure the end of available space and re-fit the text in dependency of the available UI space.
Lets assume that the '|' character denotes the space limit of the available space and we want to display a path like:
'C:\Photos\My Collection\Asia\2003\Japan\Tokio\BulletTrain.jpg'. A PathTrimming control would display this content like so:
- |..BulletTrain.jpg|
- |C:\...\BulletTrain.jpg|
- |C:\Photos\...\BulletTrain.jpg|
- |C:\Photos\My Collection\Asia\2003\...\BulletTrain.jpg|
- |C:\Photos\My Collection\Asia\2003\Japan\Tokio\BulletTrain.jpg|
...assuming that the available space is at its minimum in 1 and big enough in the last version.
This control can be used whenever UI space is likely to be too small for all the text. Showing only a portion of the text (and maybe the full text as tooltip or drop down) might be a better user experience over displaying the full text with scrollbars.
PathTrimmingFileHyperlink
The PathTrimmingFileHyperlink combines the functionality of the previously mentioned FileHyperlink
and PathTrimmingTextBlock
controls. This control provides the functionality of displaying text in a variety of available space, while giving users the ability to interact with the linked items.
The usage of the attached code is rather simple. Just download the archive, unzip and open in Visual Studio 2017 Comunity or better. You may have to Restore Nuget packages for the Unit Tests, but other than that, you should be able to compile and execute the MRUDemo project right away.
The MRULib project lives in a seperate GitHub repository where future developments will take place. The average user should use the corresponding Nuget package since this will be updated when new versions and fixes are available (see Edi project for advanced sample implementation with Nuget).
This article summarizes all important elements for the implementation of modern MRU list controls. I have provided a clear and consistent implementation and guidance towards its usage hoping this will enable others to re-use the listed components and techniques in their own application.
We have learned to link ListViewItems on MouseOver with a CheckPin control via dependency property as descriped above. Sorting and grouping with the CollectionView approach described here is another good example for displaying the same data in a variaty of different ways.