1

In my program I use an OpenFileDialog component and I set an initial directory.

However, if I decide to open a file from another folder, next time I want to open a file, the OpenFileDialog remembers what was last folder I used to open a file and it will open that folder, not the folder specified in InitialDirectory. And I am happy it is this way. Even if I close and reopen the application, the last folder used will still be remembered.

Is there a way to find out which is folder? Let's say, when I click on a Button, I want the last path displayed in a Label.

OpenFileDialog ofd = new OpenFileDialog();
ofd.InitialDirectory = @"C:\My Initial Folder";
Jimi
  • 29,621
  • 8
  • 43
  • 61
Nick_F
  • 1,103
  • 2
  • 13
  • 30

4 Answers4

5

Disclaimer:
the procedure described here is not supported: anything can change at any time without notice.


▶ The last path accessed by an application when the OpenFileDialog or SaveFileDialog are used to open or save a file, is stored in the Registry, in the HKEY_CURRENT_USER branch, under this Key:

Software\Microsoft\Windows\CurrentVersion\Explorer\ComDlg32\LastVisitedPidlMRU

▶ The names of the more recent files opened or saved by an application, are stored - organized by file extension - under this other Key (same Registry branch):

Software\Microsoft\Windows\CurrentVersion\Explorer\ComDlg32\OpenSavePidlMRU

All the information is stored as a Binary Value.
The first Key - LastVisitedPidlMRU - stores the last path accessed by an application, in two possible flavors:

  • A Parent shell folder item, in the classic GUID form (usually representing the Desktop folder), followed by the child IDlist where the path is stored in both Unicode long path format and DOS short path format (and other small pieces of information not described here - see the Further reading links).
  • A null-terminated Unicode string, representing the Executable name, followed by an IDlist (same as before).
    Here, I'll be using this structure to retrieve the last path used by a specified executable.

The Binary Values contained in the Sub-Key items of the OpenSavePidlMRU Key are stored as a simple IDlist, so the stored Paths can be accessed directly using both SHGetPathFromIDListW() and SHGetPathFromIDListEx().

Here, I'm declaring both (may come in handy), but I'm using only the second, since it's slightly more flexible (well, it allows to retrieve the short path form, if required)

Further reading or testing:


There are three methods here:

ApplicationGetLastOpenSavePath is used to retrieve the last path used by an application, given its Executable name (just the name, e.g., app.exe).
Since this question is tagged WinForms, you can get it as:

string exeName = Path.GetFileName(Application.ExecutablePath);

ApplicationGetLastOpenSaveFileNames retrieves all file names opened by an application, in relation to the last accessed path (and this alone, since this is what the current question is about).
The method returns a named tuple, (string AppPath, List<string> FileNames):

  • AppPath is the last accessed path.
  • FileNames is a List of file names - where the path is AppPath - accessed by the application.

→ The third is a helper method: its task is to handle the call to SHGetPathFromIDListEx(), which requires some interop marshaling.
The method parameters represent the byte array extracted from the Registry Key and an offset, which represents the initial location of the IDlist (as described, the binary data starts with the null-terminated Unicode string representing the executable name).

private string ApplicationGetLastOpenSavePath(string executableName)
{
    if (string.IsNullOrEmpty(executableName)) return null;
    string lastVisitedPath = string.Empty;
    var lastVisitedKey = Registry.CurrentUser.OpenSubKey(
        @"Software\Microsoft\Windows\CurrentVersion\Explorer\ComDlg32\LastVisitedPidlMRU", false);

    string[] values = lastVisitedKey.GetValueNames();
    foreach (string value in values) {
        if (value == "MRUListEx") continue;
        var keyValue = (byte[])lastVisitedKey.GetValue(value);

        string appName = Encoding.Unicode.GetString(keyValue, 0, executableName.Length * 2);
        if (!appName.Equals(executableName)) continue;

        int offset = executableName.Length * 2 + "\0\0".Length;  // clearly null terminated :)
        lastVisitedPath = GetPathFromIDList(keyValue, offset);
        break;
    }
    return lastVisitedPath;
}

This second method calls the first to retrieve the last path accessed by the application represented by string executableName:

private (string AppPath, List<string> FileNames) ApplicationGetLastOpenSaveFileNames(string executableName)
{
    string appPath = ApplicationGetLastOpenSavePath(executableName);
    if (string.IsNullOrEmpty(appPath)) return (null, null);

    var fileNames = new List<string>();
    var extensionMRUKey = Registry.CurrentUser.OpenSubKey(
        @"Software\Microsoft\Windows\CurrentVersion\Explorer\ComDlg32\OpenSavePidlMRU", false);
    string[] subKeys = extensionMRUKey.GetSubKeyNames().ToArray();

    foreach (string key in subKeys) {
        var subKey = extensionMRUKey.OpenSubKey(key);

        foreach (string keyValue in subKey.GetValueNames()) {
            var value = (byte[])subKey.GetValue(keyValue);
            string filePath = GetPathFromIDList(value, 0);

            if (filePath.Contains(appPath)) {
                fileNames.Add(filePath);
            }
        }
    }
    return (appPath, fileNames);
}

Helper method and Win32 declarations:

private string GetPathFromIDList(byte[] idList, int offset)
{
    int buffer = 520;  // 520 = MAXPATH * 2
    var sb = new StringBuilder(buffer);

    IntPtr ptr = Marshal.AllocHGlobal(idList.Length);
    Marshal.Copy(idList, offset, ptr, idList.Length - offset);

    // or -> bool result = SHGetPathFromIDListW(ptr, sb);
    bool result = SHGetPathFromIDListEx(ptr, sb, buffer, GPFIDL_FLAGS.GPFIDL_UNCPRINTER);
    Marshal.FreeHGlobal(ptr);
    return result ? sb.ToString() : string.Empty;
}

[DllImport("shell32.dll", ExactSpelling = true, CharSet = CharSet.Unicode)]
internal static extern bool SHGetPathFromIDListW(
    IntPtr pidl, 
    [MarshalAs(UnmanagedType.LPTStr)]
    StringBuilder pszPath);

[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
internal static extern bool SHGetPathFromIDListEx(
    IntPtr pidl, 
    [MarshalAs(UnmanagedType.LPTStr)]
    [In,Out] StringBuilder pszPath, 
    int cchPath, 
    GPFIDL_FLAGS uOpts);

internal enum GPFIDL_FLAGS : uint {
    GPFIDL_DEFAULT = 0x0000,
    GPFIDL_ALTNAME = 0x0001,
    GPFIDL_UNCPRINTER = 0x0002
}
Jimi
  • 29,621
  • 8
  • 43
  • 61
0

There is no method which will give you that information, you simply need to track it yourself.

Something like this:

private string lastFolder;

private void button1_Click(object sender, System.EventArgs e)
{
    if (ofd.ShowDialog() == DialogResult.OK)
    {
        lastFolder = Path.GetDirectoryName(ofd.Filename);
        // Do something with the file.
    }
}
jscarle
  • 1,045
  • 9
  • 17
  • Actually, the Dialogs track it. Or rather Windows does, via the "Working Directory". A relic from ye olde DOS days, it is still very much in use. – Christopher May 03 '20 at 03:17
  • Yes, but there is no method to "retrieve" that information. – jscarle May 03 '20 at 03:18
  • Thanks, but that will not always work. If I close the application and I reopen it, I want to know that folder without opening the file. Maybe this is not an important feature, I thought maybe there is an easy way I don't know about. – Nick_F May 03 '20 at 03:26
  • I could save that folder name to a file, instead of a global variable though and that will work (although not very elegant). – Nick_F May 03 '20 at 03:33
  • @jscarle "Yes, but there is no method to "retrieve" that information." Reality disagrees: https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.getcurrentdirectory | Again, this is just the dang working dircetory. It has been a thing years before **I** was a thing. – Christopher May 03 '20 at 17:27
  • That's the application's working directory, which is not the same thing as the OpenFileDialog's working directory. – jscarle May 03 '20 at 20:01
0

When the user select a file, I automatically force InitialDirectory to be the Directory from that Filename. Doing that every time that I open again OpenFileDialog is going to the last folder. Maybe it help you. Bear in mind that I initiated InitiaDirectory in another place and not in the event that I'm opening the OpenFileDialog.

ofd.InitialDirectory = Path.GetDirectoryName(ofd.FileName);

-1

In Windows there is this concept of a "Working Directory". Basically the one folder the Application is looking at right now. That one is used to complete any relative paths and the folder is first checked for any executeables if you only put a random string that is not a keyword. The starting value is defined by the very command that started the application - this will always have been a full path.

Without a InitialDirectory, OpenFileDialog apparently will use that one. And by default, it will not set it back: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.filedialog.restoredirectory

If you just omit InitialDirectory for the 2nd call, it will work that way automagically. But of course you can propably extract the working directory value too. Some simple bool switches will do that, but you have to replicate them everywhere you set the directory for a dialog:

bool InitializedDialogs = false;

if(InitializedDialogs == false){
   ofd.InitialDirectory = @"C:\My Initial Folder";
   InitializedDialogs = true;
}

A single call to https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.setcurrentdirectory in a central location might be better.

Christopher
  • 9,634
  • 2
  • 17
  • 31
  • Thanks for the answer, but it does not answer my question. I don't want to change the behavior of how the program works, I just want to find beforehand, which directory will open when I call the OpenFileDialog component. If I use Directory.GetCurrentDirectory() that is the directory where the executable is located, not the last folder used by OpenFileDialog(). – Nick_F May 03 '20 at 03:22
  • @Nick_F You do understand that you will not get *around* changing what the programm does, if you want different results? | Also now the goal is a different one. However, the solution is the same: Use https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.getcurrentdirectory to get the directory to save on closing. Use https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.setcurrentdirectory to set it when starting. And on the first start of the programm, intialize it whatever you used for `InitialDirectory` before. – Christopher May 03 '20 at 17:25