Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Adding Files to the Windows MRU/Recent Document List


:P
On this page:

If you have a Windows application that uses custom file extensions for documents, it might be nice to add the documents you open inside of the application to the Most Recently Used list of the application. For example in West Wind Web Surge I want to show the most recent documents so they show up like this from the task bar Jump List:

MRUListTaskBar

You can see the recent list above the actual start link with the most recently used Web Surge Url Sessions shown.

If you have a custom extension registered for your application and the extension points at your application’s exe, the taskbar is automatically updated anytime you open a file with that extension from the Windows Shell. So if I’m in Explorer and I open this file:

FileOpen

This file is launched and automatically added the MRU list. Nothing else to do there.

However, if I also want to do this from within the application when I open a file, I have to explicitly add the file to the MRU list. There are a couple of things required for this:

  • You need to create a registered file extension
  • You need to add documents to the recent documents collection through an API

The former is crucial in order for any of what follows to work – you have to have a file association that links a specfic file extension to your installed application (the EXE that you are running). In fact that’s how the Explorer integration works when you double click on ‘known’ extension when it tracks the recently used files.

I’ll come back to the file association at the end of this post. But first, let’s look at how you can use an API to programmatically add files to the MRU list.

Using the ShAddToRecentDocs API

Windows has a really simple API called ShAddToRecentDocs that makes its super easy to add files to the MRU list. It’s easy to access from most clients as it’s a simple string and flag based API.

void SHAddToRecentDocs(
    UINT uFlags,
    _In_opt_  LPCVOID pv
    );

In C# it’s easy to set this up with some PInvoke code (courtesy of PInvoke.NET):

public static class MostRecentlyUsedList
{
    /// <summary>
    /// Adds Recently Used Document to the MRU list in Windows.
    /// Item is added to the global MRU list as well as to the
    /// application specific shortcut that is associated with
    /// the application and shows up in the task bar icon MRU list.
    /// </summary>
    /// <param name="path">Full path of the file</param>
    public static void AddToRecentlyUsedDocs(string path)
    {
        SHAddToRecentDocs(ShellAddToRecentDocsFlags.Path, path);
    }


    private enum ShellAddToRecentDocsFlags
    {
        Pidl = 0x001,
        Path = 0x002,
    }

    [DllImport("shell32.dll", CharSet = CharSet.Ansi)]
    private static extern void
        SHAddToRecentDocs(ShellAddToRecentDocsFlags flag, string path);

}

You can now push this code into your application anytime a new file is opened:

Requests = StressTester.ParseSessionFile(FileName);
if (Requests == null)
    Requests = new List<HttpRequestData>();
else
    MostRecentlyUsedList.AddToRecentlyUsedDocs(Path.GetFullPath(FileName));

What’s nice about this API is that it manages the recently used file list for you by adding the new item to the top and making sure that if the file is already in the list that the old item is removed. In short – it manages the MRU list for you nicely.

Reading the MRU List

Since Windows manages the list for you, it would also be nice to read the list back so you can use it in your own application’s MRU list. For example in Web Surge I have a Recent Sessions list menu option:

WebSurgeMru

It sure would be nice if this list could be populated from the same MRU list that Windows already keeps.

The good news is that the Windows Recent file list is stored as a bunch of .lnk files in %USERPROFILE%\Recent, so it’s easy to iterate over the list. Unfortunately the .lnk files are a binary format that are not easily readable directly. However, there are various shell extensions and some helpers in the Windows Scripting host that can help make sense of this.

The following code uses directory iteration of the User’s Recent folder, and using Windows Scripting host to get the actual target filenames and return them as a list:

/// <summary>
/// Returns a list of the most recent files as stored by windows for the given 
/// filespec. Specify the files spec as *.websurge where .websurge is the extension
/// you'd like to retrieve. The length of the list will depend on your Windows
/// settings for the maximum number of list items configured.
/// </summary>
/// <param name="fileSpec">A wildcard file spec: *.websurge for example</param>
/// <returns>List of strings or an empty list if none exist</returns>
public static List<string> GetMostRecentDocs(string fileSpec)
{
    var recentFiles = new List<string>();

    var path = Environment.GetFolderPath(Environment.SpecialFolder.Recent);

    var di = new DirectoryInfo(path);
    var files = di.GetFiles(fileSpec + ".lnk")
        .OrderByDescending(fi => fi.LastWriteTimeUtc)
        .ToList();
    if (files.Count < 1)
        return recentFiles;

    dynamic script = ReflectionUtils.CreateComInstance("Wscript.Shell");

    foreach (var file in files)
    {
        dynamic sc = script.CreateShortcut(file.FullName);
        recentFiles.Add(sc.TargetPath);
        Marshal.FinalReleaseComObject(sc);
    }
    Marshal.FinalReleaseComObject(script);

    return recentFiles;
}

Note that I’m using dynamic COM interop here against the WScript.Shell host to avoid having to import an interop assembly. ReflectionUtils.CreateInstance is a helper function I use that’s part of the Westwind.Utilities library – the code is on GitHub so you can inline it or use the Westwind.Utilities NuGet package. I use dynamic to avoid having to import the COM types and I can easily just two members I’m interested in.

Putting it all Together

To put all this together I added this functionality to my configuration class.

The class is used to serialize configuration data to disk. I use Westwind.ApplictionConfiguration and the AppConfiguration base class to handle the configuration serialization for me, but that’s really not all that relevant here. Suffice it to say the configuration is loaded when the class is instantiated and can be optionally written at any point to write out it’s internal state including the nested configuration subobjects.

Previously I stored the configuration as part of the configuration which was written out to a JSON file. Since this is still a configuration feature I continue to include the RecentFilesProperty in this configuration as well as the LastFileName. But I can now automatically load the RecentList from the Windows MRU list and update it when a new LastFileName is assigned.

Here’s what this looks like:

public class WebSurgeConfiguration : AppConfiguration { public string AppName { get; set; } public StressTesterConfiguration StressTester { get; set; } public UrlCaptureConfiguration UrlCapture { get; set; } public WindowSettings WindowSettings { get; set; } public CheckForUpdates CheckForUpdates { get; set; }

 


[JsonIgnore]
public List<string> RecentFiles { get { if (_recentFileList == null) { try { _recentFileList = MostRecentlyUsedList.GetMostRecentDocs("*.websurge"); } catch { _recentFileList = new List<string>(); } } return _recentFileList; } } private List<string> _recentFileList; public string LastFileName { get { return _LastFileName; } set { _LastFileName = value; try { MostRecentlyUsedList.AddToRecentlyUsedDocs(value); // reload recent file list _recentFileList = MostRecentlyUsedList.GetMostRecentDocs("*.websurge"); } catch {} } } private string _LastFileName; }

Note that I wrap these calls into try/catch blocks just in case there are some machine restrictions that disallow use of Windows Scripting Host, PInvoke calls or registry access (which is what Windows uses behind the scenes). Even if those calls fail the app doesn’t crash since this is just a convenience feature rather than a vital, critical feature.

File Associations are Required!

What I’ve described is a simple way to manage recent files, but keep in mind that this works reliably only if you actually have a mapped Windows extension that correlates with the application you are executing.

To do that you need to create Windows File association in the registry which is typically done as part of an installation routine for an application. Here’s what this looks like in my Installer (I use a nice install tool called InstallMate I’ve been using for years).

FileAssociation

There are basically two top level keys under HKCR that have to be created:

  • The actual file extension mapping
  • The “File Type” association

The reason for this split configuration is that you can potentially have multiple extensions pointing at the same file type.

Here’s what the reg export looks like which is probably the easiest way to show the values:

.websurge key (extension mapping)

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\.websurge]
@="westwind.websurge.requestfile"

westwind.websurge.requestfile key (Shell file type definition)

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\westwind.websurge.requestfile]

[HKEY_CLASSES_ROOT\westwind.websurge.requestfile\DefaultIcon]
@="C:\\Program Files\\West Wind WebSurge\\websurge.exe,0"

[HKEY_CLASSES_ROOT\westwind.websurge.requestfile\shell]

[HKEY_CLASSES_ROOT\westwind.websurge.requestfile\shell\open]

[HKEY_CLASSES_ROOT\westwind.websurge.requestfile\shell\open\command]
@="\"C:\\Program Files\\West Wind WebSurge\\WebSurge.exe\" \"%1\""

The file extension simply has a key that points at the file definition. The file definition in turn includes the shell extension options that are used to display the icon and handle the shell open command which is executed when you click on a shortcut link.

It’s easy enough to add this to an application as part of the installation routine – or even as part of first time startup code (assuming you have permissions to write to the HKCR key).

Summary

Using the Windows MRU list is pretty straight forward, as long as you have one or more registered file extensions that you are managing on internal MRU lists. You can easily add new items to the list and also easily retrieve the items back out. And you can let Windows worry about managing the list beyond that – sorting out dupes, keeping the list ordered properly etc. is taken care of. This way your application can use the same list that you see on a Windows Jump list without any extra code or management.

I’ve written this up since I’ve used this sort of code in a few different apps but had it scattered about as part of application code – sitting down and writing this up forced me to isolate it out into something a little more reusable and add support for reading the values out as well as stuffing them in, so I can remember all of this in the future when I might need it again. Hopefully some of you will find this useful as well.

If you want to see how this works in the actual running application I’ve described here you can take a look at West Wind WebSurge which is a small stresstesting/Url testing utility:

Posted in Windows  C#  .NET  

The Voices of Reason


 

Jerome Viveiros
April 15, 2015

# re: Adding Files to the Windows MRU/Recent Document List

Very nice. I've used SHAddToRecentDocs before, since my app was a tool capable of viewing other app's file types, but this only worked for the global Windows Recent Files list in Windows 7. I had no idea of all the steps required to do this with my own file types, so this will come in very handy.

It may be worth adding, for the sake of anybody who takes MS code analysis warnings seriously, that the character set passed *must* be CharSet.Ansi; otherwise the function will fail. My pinvoke declaration looked like this:
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", 
    "CA2101:SpecifyMarshalingForPInvokeStringArguments", MessageId = "1"),
DllImport("shell32.dll", CharSet = CharSet.Ansi)]
public static extern void 
    SHAddToRecentDocs(ShellAddToRecentDocsFlags flag, 
    [MarshalAs(UnmanagedType.LPStr)] string path);


Also, since I'm a new reader here, I don't know if anyone else has mentioned, but when you scroll down this blog in Firefox and Chrome (but not IE), when you scroll past the bottom of the content populated in the left pane, the right one animates and scrolls to the left to take up the whole width. It's cool, but <em>really</em> distracting.

Rick Strahl
April 15, 2015

# re: Adding Files to the Windows MRU/Recent Document List

@Jerome, thanks for the heads up on the Charset.Ansi flag.

As to the scroll-resizing that's on purpose. Once you scroll past the sidebar content the screen widens to give you more space for text and images. I find that useful for my own reading - didn't think it annoying, but curious if others think the same thing. Not such a big deal on big screens but certainly nice when you're on a tablet.

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024