Table of Contents
Although this is an article for use in WPF
applications, it includes exactly two snippets of xaml
and those are for the example usage. Please don't expect fancy data binding or clever style triggers. It just works.
There are some development tasks that are repeated for many new applications. Implementing a MRU
, or recent file list, is common to most document-based applications. It's not rocket science and it's been done before. So I've written a control that you can just plug in so you can get on with the interesting new stuff.
I have tried to make the code as easy to use as possible. As a minimum, you can add just two lines of xaml
and a few lines of code. However, it is configurable. For instance, with one more line of code you can persist to an xml
file instead of the registry. You can also fully specify the text displayed in your File
menu by implementing a callback method.
The code is all in one file: RecentFileList.cs. If you're using C#, you can just add this file to your project. In the attached source and binaries, I have wrapped the file in a library, so you could reference that instead. I have also included a demo application that exercises the class. It doesn't actually create files, it is just a simulation.
CPian Joe Woodbury [^] in his article Most Recently Used (MRU) Menu Class for .NET 2.0 in C# [^] provides a fine implementation for Windows Forms. I have taken one of his functions, that shortens long file paths, but the rest of this article is new.
This section describes what you must do to get a working Recent File List. The rest of the article describes what you can do.
Firstly, either include the file RecentFileList.cs, or reference the assembly RecentFileListLib.dll.
Then add the Common
namespace to your Window
:
<Window
...
xmlns:common="clr-namespace:Common; assembly=RecentFileListLib"
...
>
And add the control to your File
menu. It will render as a Separator
. The recommended place is just above your Exit
menu item.
<MenuItem Header="_File">
...
<common:RecentFileList x:Name="RecentFileList" />
<MenuItem Header="E_xit" ... />
</MenuItem>
Then in your code, hook the MenuClick
event.
partial class Window1
{
public Window1()
{
InitializeComponent();
...
RecentFileList.MenuClick += ( s, e ) => FileOpenCore( e.Filepath );
}
}
And then you can call two methods. Call InsertFile
whenever a file is successfully opened or saved. Call RemoveFile
whenever a file fails to open.
partial class RecentFileList
{
public void InsertFile( string filepath )
public void RemoveFile( string filepath )
}
And that's it. You now have a working Recent File List that persists to the Registry under HKCU \ Software \ <CompanyName> \ <ProductName> \ RecentFileList
The main class, RecentFileList
, derives from Separator
. This means that when the list is empty, only the base Separator
is rendered. When some files have been inserted, MenuItem
's are added along with a closing Separator
.
using System.Windows.Controls;
partial class RecentFileList : Separator
{
}
The RecentFileList
class handles all the logic, but relies on an implementation of IPersist
to handle storage using one of my favourite design patterns: Strategy.
partial class RecentFileList
{
public interface IPersist
{
List<string> RecentFiles( int max );
void InsertFile( string filepath, int max );
void RemoveFile( string filepath, int max );
}
public IPersist Persister { get; set; }
}
Two implementations of IPersist
are provided. The default is the RegistryPersister
and the other is the XmlPersister
.
RecentFileList
hooks its own Loaded
event. When this fires, it finds its parent ( which must be a MenuItem
) and hooks its SubmenuOpened
event. This event fires when the menu is opening and this is when the extra MenuItem
's are added.
RecentFileList
exposes one event: MenuClick
. This fires when one of the MenuItems
is clicked and just passes the filepath to any Observers.
partial class RecentFileList
{
public event EventHandler<MenuClickEventArgs> MenuClick;
}
You can set the maximum number of files to list:
partial class RecentFileList
{
public int MaxNumberOfFiles { get; set; }
}
Here are the members that control which implementation of
IPersist
is used:
partial class RecentFileList
{
public IPersist Persister { get; set; }
public void UseRegistryPersister()
public void UseRegistryPersister( string key )
public void UseXmlPersister()
public void UseXmlPersister( string filepath )
public void UseXmlPersister( Stream stream )
}
The RegistryPersister
is used by default. You can provide your own implementation of IPersist
, or use the methods to select and configure one of the existing implementations. The RegistryPersister
uses the key: HKCU \ Software \ <CompanyName> \ <ProductName> \ RecentFileList
by default, but you can override this by providing your own key. The XmlPersister
uses the file: <Environment.SpecialFolder.ApplicationData> \ <CompanyName> \ <ProductName> \ RecentFileList.xml
by default, but again you can provide your own filepath. You can also provide a Stream
and do what you like with the XML
. The Stream
must be readable, writable and seekable, so you would most probably use a MemoryStream
.
There are a number of ways to control the text displayed in the MenuItems
:
partial class RecentFileList
{
public int MaxPathLength { get; set; }
public static string ShortenPathname( string pathname, int maxLength )
public string MenuItemFormatOneToNine { get; set; }
public string MenuItemFormatTenPlus { get; set; }
public delegate string GetMenuItemTextDelegate( int index, string filepath );
public GetMenuItemTextDelegate GetMenuItemTextHandler { get; set; }
}
They are used internally by this method:
partial class RecentFileList
{
private string GetMenuItemText( int index, string filepath, string displaypath )
{
GetMenuItemTextDelegate delegateGetMenuItemText = GetMenuItemTextHandler;
if ( delegateGetMenuItemText != null )
return delegateGetMenuItemText( index, filepath );
string format =
( index < 10 ? MenuItemFormatOneToNine : MenuItemFormatTenPlus );
string shortPath = ShortenPathname( displaypath, MaxPathLength );
return String.Format( format, index, filepath, shortPath );
}
}
The static method ShortenPathname
( which Joe Woodbury [^] wrote ) takes a filepath and shortens it to less than MaxPathLength
characters, by replacing parts of the path with an ellipsis.
By default, String.Format
is called using either MenuItemFormatOneToNine
or MenuItemFormatTenPlus
, depending on the index
. You can set these format strings if this will fulfill your needs. If you require full control over the display text, you can provide a method ( a GetMenuItemTextDelegate
) that takes the index and filepath, and returns the formatted string.
The System.Windows.Forms.Application
had handy static properties like CompanyName
. You could reference this assembly, but that just doesn't seem right for these few properties. Instead, you can access the attributes directly through reflection:
using System.Reflection;
static partial class ApplicationAttributes
{
static readonly Assembly _Assembly = null;
static readonly AssemblyCompanyAttribute _Company = null;
static readonly AssemblyProductAttribute _Product = null;
public static string CompanyName { get; private set; }
public static string ProductName { get; private set; }
static ApplicationAttributes()
{
CompanyName = String.Empty;
ProductName = String.Empty;
_Assembly = Assembly.GetEntryAssembly();
if ( _Assembly != null )
{
object[] attributes = _Assembly.GetCustomAttributes( false );
foreach ( object attribute in attributes )
{
Type type = attribute.GetType();
if ( type == typeof( AssemblyCompanyAttribute ) )
_Company = ( AssemblyCompanyAttribute ) attribute;
if ( type == typeof( AssemblyProductAttribute ) )
_Product = ( AssemblyProductAttribute ) attribute;
}
}
if ( _Company != null ) CompanyName = _Company.Company;
if ( _Product != null ) ProductName = _Product.Product;
}
}
This is just a little project that I hope will save people from reinventing the wheel.
2008 Feb 19: |
First published |
2008 Feb 20: |
Fixed bug when viewed in xaml designer |