Click here to Skip to main content
15,885,537 members
Articles / Desktop Programming / WPF
Article

RecentFileList: a WPF MRU

Rate me:
Please Sign up or sign in to vote.
4.17/5 (23 votes)
20 Feb 2008CPOL5 min read 85.2K   3.4K   33   18
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:

XML
<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.

XML
<MenuItem Header="_File">
   ...
   <common:RecentFileList x:Name="RecentFileList" />
   <MenuItem Header="E_xit" ... />
</MenuItem>

Then in your code, hook the MenuClick event.

C#
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.

C#
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.

C#
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.

C#
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.

C#
partial class RecentFileList
{
   public event EventHandler<MenuClickEventArgs> MenuClick;
}

Using the Code

MaxNumberOfFiles

You can set the maximum number of files to list:

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

Persister

Here are the members that control which implementation of IPersist is used:
C#
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:

C#
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:

C#
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:

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United Kingdom United Kingdom
I discovered C# and .NET 1.0 Beta 1 in late 2000 and loved them immediately.
I have been writing software professionally in C# ever since

In real life, I have spent 3 years travelling abroad,
I have held a UK Private Pilots Licence for 20 years,
and I am a PADI Divemaster.

I now live near idyllic Bournemouth in England.

I can work 'virtually' anywhere!

Comments and Discussions

 
QuestionIncluding RecentFileList.cs file in the project Pin
QSabaa6-May-20 14:53
QSabaa6-May-20 14:53 
QuestionHow can I get my click event back into the ViewModel Pin
FAUCHILLE29-Mar-20 2:47
FAUCHILLE29-Mar-20 2:47 
QuestionMy vote of 5 Pin
Gary Wheeler21-Mar-16 4:38
Gary Wheeler21-Mar-16 4:38 
QuestionMRU list using MVVM LIGHT Pin
MTaunton21-Sep-15 19:31
MTaunton21-Sep-15 19:31 
QuestionLate to the party Pin
Ken Billing12-Dec-14 11:27
Ken Billing12-Dec-14 11:27 
GeneralMy vote of 5 Pin
rakkeshbisht9-Jun-14 6:00
rakkeshbisht9-Jun-14 6:00 
QuestionStyle for MRU Pin
ricardosobrado20-Aug-13 0:27
ricardosobrado20-Aug-13 0:27 
GeneralMy vote of 5 Pin
radsd28-Aug-12 12:53
radsd28-Aug-12 12:53 
QuestionProposal to make Persister chosable in xaml Pin
EvAlexHimself16-Feb-12 22:09
EvAlexHimself16-Feb-12 22:09 
GeneralMy vote of 5 Pin
DanM224-May-11 6:26
DanM224-May-11 6:26 
GeneralMy vote of 5 Pin
User 57410707-Apr-11 10:11
User 57410707-Apr-11 10:11 
GeneralCaching of entries to improve performance Pin
Alexey Mednonogov5-Sep-10 8:06
Alexey Mednonogov5-Sep-10 8:06 
GeneralRecently Used Files Pin
Brian C Thompson16-Dec-09 2:47
Brian C Thompson16-Dec-09 2:47 
GeneralRe: Recently Used Files Pin
Nicholas Butler18-Dec-09 1:23
sitebuilderNicholas Butler18-Dec-09 1:23 
GeneralMy vote of 2 Pin
#realJSOP18-Nov-09 1:11
mve#realJSOP18-Nov-09 1:11 
GeneralRe: My vote of 2 PinPopular
Nicholas Butler19-Nov-09 1:59
sitebuilderNicholas Butler19-Nov-09 1:59 
GeneralNamespace Pin
MTaunton27-Jul-08 21:57
MTaunton27-Jul-08 21:57 
GeneralRe: Namespace Pin
RichQ29-Dec-08 8:48
RichQ29-Dec-08 8:48 
Nick, good stuff. I had just started to implement my own MRU code when I found this. Thanks for putting this together!

Same thing on the namespace.. It didn't take long to figure it out, but it tripped me up for a few.

Thanks!
-Rich

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.