Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

RecentFileList: a WPF MRU

0.00/5 (No votes)
20 Feb 2008 2  
A Most Recently Used menu item control for WPF applications

Table of Contents

Preface

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.

Introduction

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.

Background

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.

Quickstart

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

Architecture

Base class

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
{
}

Persistance

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.

Hooks

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.

Event

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;
}

Using the Code

MaxNumberOfFiles

You can set the maximum number of files to list:

partial class RecentFileList
{
   public int MaxNumberOfFiles { get; set; } // default = 9
}

Persister

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.

Display format

There are a number of ways to control the text displayed in the MenuItems:

partial class RecentFileList
{
   public int MaxPathLength { get; set; } // default = 50
   public static string ShortenPathname( string pathname, int maxLength )
   
   public string MenuItemFormatOneToNine { get; set; } // default = "_{0} {2}"
   public string MenuItemFormatTenPlus { get; set; } // default = "{0} {2}"
   
   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.

Points of Interest

Application attributes

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;
   }
}

Conculsion

This is just a little project that I hope will save people from reinventing the wheel.

History

2008 Feb 19: First published
2008 Feb 20: Fixed bug when viewed in xaml designer

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here