Introduction

A few years ago, I wrote code and an article to implement a Most Recently Used files class for .NET 1.1. Visual Studio 2005 and .NET 2.0 introduced the MenuStrip class. Unfortunately, they still forgot to add an MRU list, so I modified my original code. I debated just adding the new project to that one, but decided it was best to keep this as a separate article since the previous class is now obsolete.

The MruStripMenu Class

The MruStripMenu class displays a list of the most recently used files in a popup menu. A derived class, MruStripMenuInline, displays them as additional entries in a menu, or inline. In both cases, a number is displayed to the left of the file. Entries 1-10 allow you to use a number on the keyboard to select an entry.

Namespace

This class is in the creatively named, JWC (Joe Woodbury Classes) namespace.

This is the menu item that will serve as the anchor point for the MRU list. It must be created and inserted into the desired spot in a menu, before creating an MruMenu instance.

clickedHandler

The delegate that will be called when one of the MRU entries is selected. The entry number (zero based) and the filename will be passed to the handler.

registryKeyName

The name of a registry key MruMenu that will be used to load and/or store the contents of the MRU list. If a key name is passed to the constructor, and loadFromRegistry is true, MruMenu will attempt to load the files from the registry. However, to save the MRU list, you must call SaveToRegistry().

loadFromRegistry

If true, MruMenu will attempt to load the MRU list stored in the passed registry key. If the registry key does not exist, an exception will not be thrown.

maxEntries

The maximum number of entries to allow in the MRU list. Currently, for practical reasons, the number of entries is limited to be from 4 through 16. If _maxEntries is not within this range, it will be adjusted up, or down, whichever is appropriate (an ArgumentOutOfRangeException will not be thrown).

Delegates

public delegate void ClickedHandler(int number, string filename)

number - The MRU relative number of the entry that was clicked

filename - The filename of the entry that was clicked

Properties

ToolStripItemCollection MenuItems (ReadOnly)

The ToolStripItemCollection where the MRU list is located.

int StartIndex (ReadOnly)

The menu index of the first MRU entry.

int EndIndex (ReadOnly)

The menu index immediately "after" the last MRU entry.

int NumEntries (ReadOnly)

The current number of MRU entries.

int MaxEntries (Read/Write)

The maximum number of MRU entries allowed. You can set the MaxEntries to the range of 4 through 16. If the new maximum entries is less than the current number of entries, the oldest entries will be discarded.

int MaxShortenPathLength (Read/Write)

The maximum length of the path to allow in the menu, in characters. When setting, any value less than 16 will be changed to 16. The new length will have no effect on the current contents of the menu.

string RegistryKeyName (Read/Write)

Set/Get the registry key name where the MRU list will be loaded/saved. No error checking is done to verify if the key is valid.

Static Methods

staticstring FixupEntryname(int number, string entryname)

Adds the entry number prefix and mnemonic (for entries 0-9) to the filename. Note that the number is zero based, while the actually displayed numbering is one based.

number

The MRU relative number to use for the prefix.

entryname

The string to which to apply the prefix.

staticString ShortenPathname(string pathname, int maxLength)

If the pathname is longer than maxLength, it replaces consecutive path components by ellipsis. It will always preserve the root of the path and at least three characters of the filename, which may cause the result to be longer than maxLength.

pathname

The pathname to check and, possibly, shorten. The pathname should be fully resolved and should start with a drive letter or be a UNC path.

maxLength

The maximum length, in characters, of the resulting path.

Methods

int FindFilenameNumber(string filename)

Find the MRU relative number of the entry which matches filename. This match must be exact except for case sensitivity.

Returns -1 if a matching entry cannot be found.

filename

The filename to find.

int FindFilenameMenuIndex(string filename)

Find the menu index of the entry which matches filename. This match must be exact except for case sensitivity.

Returns -1 if a matching entry cannot be found.

filename

The filename to find.

int GetMenuIndex(int number)

Returns the menu index of the entry at the MRU relative number.

number

The MRU relative number of the menu index.

string GetFileAt(int number)

Returns the filename stored at the MRU relative number.

number

The MRU relative number of the filename to remove. Note that this number is zero based, while the display is one based.

string[] GetFiles()

Returns the list of files in the MRU list. The most recent file will be at index zero.

void SetFiles(String[] filenames)

Replaces the MRU entries with filenames. (Functionally equivalent to: RemoveAll(); AddFiles(filenames);.) The filenames will be added such that the first string in the array will be the topmost on the MRU list. (In other words, the entries will be added from the end to the beginning.)

filenames

The list of filenames to set.

void SetFirstFile(int number)

Makes the specified entry the first on the MRU list.

number

The MRU relative number of the entry to make the first. Note that this number is zero based, while the display is one based.

void AddFiles(String[] filenames)

Adds filenames to the list of files. The filenames will be added such that the first string in the array will be the topmost on the MRU list. (In other words, the entries will be added from the end to the beginning.)

filenames

The list of filenames to add.

void AddFile(String filename)

Adds a filename to the MRU list. If the entry exists, it will be moved to the first position. Otherwise it will be added in the first position. If the number of entries exceeds maxEntries, the least recent file (the last one) will be removed.

Before being added, the filename will first be resolved to an absolute path. The result will then be passed to the ShortenPathname function. Despite the absolute path resolution, it is possible for multiple entries to refer to the same physical file.

filename

The filename to add.

void AddFile(String filename, String entryname)

Adds entryname to the MRU list and stores filename with that entry. If the entry exists, it will be moved to the first position. Otherwise it will be added in the first position. If the number of entries exceeds maxEntries, the least recent file (the last one) will be removed. Note that filename will be used to determine whether an entry exists. Thus it is possible to have two identical entries in the MRU list, even though the associated filenames are different.

filename

The filename to add.

entryname

The text to use in the MRU menu.

void RemoveFile(int number)

Removes the entry at the MRU relative number.

number

The MRU relative number of the entry to remove. Note that this number is zero based, while the display is one based.

void RemoveFile(String filename)

Removes the entry associated with filename.

filename

The filename to remove.

void RemoveAll()

Removes all MRU entries.

void LoadFromRegistry(String keyName)

Sets the registry key and loads the entries located at that key.

keyName

The registry key from which to restore the MRU entries.

void LoadFromRegistry()

Loads the entries located at the stored key.

void SaveToRegistry(String keyName)

Sets the registry key and saves the entries to that key.

keyName

The registry key where the MRU entries will be stored.

void SaveToRegistry()

Saves the entries located at the stored key.

Changes

24 September 2005

Posted on CodeProject

16 October 2007

Fixed a bug in FixupPrefixes where the first character was being stripped (thanks to shostakovich55 for that catch and his elegant solution)

Constructor now calls property to set MaxEntries which does range checking

5 November 2009

Fixed the RemoveAll bug first observed by Mark Rice, ignored by me, and then verified by hgy. My apologies to all. hgy's fix was pasted in and tested. Good work and thank you!

Share

About the Author

Joe is one of those software engineers with a film degree. His first paid programming job (what, you think film is a good way to make a living?) was writing games for Apple II's using 6502 assembly. He soon moved to 80x86 assembly, C, C++ (for a long time), C# and then back to C++ with some work in C#.

He first wrote software for Windows 3.0 in 1990. Save for some continued work in DOS and a horrid, and mercifully brief, foray into OS/2, he has concentrated on designing and writing software for all versions and types of Windows except RT.

Comments and Discussions

Hi Joe,
I understand that you create a delegate sub (sorry for my terminology, I come from a VB background) which remains for the lifetime of the control. My point is that each MRU menu item has a reference to this delegate (which of course is required since it wouldn't be able to register any clicks if it didn't) but nowhere do I see this reference released. So my point is, are the menu items being kept alive by this reference even though they have been removed from the menu or does the Remove release this delegate reference?

Hi Joe,
That's my point. The MRUStripMenu publishes the ClickedHandler. Each of the MRUMenuItems subscribe to this via a delegate that is called when the user clicks that menu item. The MRUStripMenu then receives this notification and in your demo application a dialogbox is displayed as a result. So the delegate ties the MRUMenuItem to the parent MRUStripMenu and keeps the MRUMenuItem alive.

Each MRUMenuItem is a publisher, not a subscriber. The subscriber is MruStripMenu.

When deleting a publisher, it will clean up the delegates subscribed to it. It is the one sending information so the target of the call needs to be around. The publisher also knows about the subscribers.

Deleting a subscriber will keep all publishers alive until they are deleted or delegates the subscriber connected to are removed. For all intents and purposes, it has a callback and that callback can't simply go away.

When using the MRU-list we had problems with the shortened path names looking identical for several items. As a solution I have added tooltips to the MRU file list. Now when a user hoovers the mouse over a file name it shows the entire filename and path in a tooltip.

The minimum number of entries is 4, the maximum is 16. You can change both of these in the code, but I'd recommend against it. (The maximum number should arguably be 10; any more than that and it becomes confusing and even annoying to users.)

Well, I noticed the issue when loading the MRU from settings when the application starts up. I call AddFile on each file on the list. I guess the better way to do it is to call AddFiles which adds them in the correct order.

Thank you Joe for the great classes for MRU. This is just the sort of simple class I was looking for.

I needed to use the inline class with a ToolStripSplitButton. But I found it only accepted a ToolStripMenuItem as the owning item. Simply changing it take a ToolStripDropDownItem instead solved the problem. As that's the base class for menu items, drop down buttons and split buttons, it now enables the inline MRU list to be used in more places.