SHGetKnownFolderPath / Environment.GetFolderPath() returning wrong value for public documents
Asked Answered
B

1

10

I got a somewhat strange error when trying to resolve the CommonDocuments directory. It keeps resolving to the wrong directory, after the CommonDocuments directory has been redirected / moved to a new location using Windows Explorer (Properties->Path from the context menu).

a minimal working piece of code would be:

namespace CommonDocumentsTest
{
    class Program
    {
        private static readonly Guid CommonDocumentsGuid = new Guid("ED4824AF-DCE4-45A8-81E2-FC7965083634");

        [Flags]
        public enum KnownFolderFlag : uint
        {
            None = 0x0,
            CREATE = 0x8000,
            DONT_VERFIY = 0x4000,
            DONT_UNEXPAND= 0x2000,
            NO_ALIAS = 0x1000,
            INIT = 0x800,
            DEFAULT_PATH = 0x400,
            NOT_PARENT_RELATIVE = 0x200,
            SIMPLE_IDLIST = 0x100,
            ALIAS_ONLY = 0x80000000
        }

        [DllImport("shell32.dll")]
        static extern int SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags, IntPtr hToken, out IntPtr pszPath);

        static void Main(string[] args)
        {
            KnownFolderFlag[] flags = new KnownFolderFlag[] {
                KnownFolderFlag.None,
                KnownFolderFlag.ALIAS_ONLY | KnownFolderFlag.DONT_VERFIY,
                KnownFolderFlag.DEFAULT_PATH | KnownFolderFlag.NOT_PARENT_RELATIVE,
            };


            foreach (var flag in flags)
            {
                Console.WriteLine(string.Format("{0}; P/Invoke==>{1}", flag, pinvokePath(flag)));
            }
            Console.ReadLine();
        }

        private static string pinvokePath(KnownFolderFlag flags)
        {
            IntPtr pPath;
            SHGetKnownFolderPath(CommonDocumentsGuid, (uint)flags, IntPtr.Zero, out pPath); // public documents

            string path = System.Runtime.InteropServices.Marshal.PtrToStringUni(pPath);
            System.Runtime.InteropServices.Marshal.FreeCoTaskMem(pPath);
            return path;
        }
    }
}

Expected behaviour:
Output is D:\TestDocuments

Actual behaviour:
Output is C:\Users\Public\Documents

None; P/Invoke==>C:\Users\Public\Documents
DONT_VERFIY, ALIAS_ONLY; P/Invoke==>
NOT_PARENT_RELATIVE, DEFAULT_PATH; P/Invoke==>C:\Users\Public\Documents

The correct value is stored in the Windows Registry (HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders\Common Documents), but it is not returned by SHGetKnownFolderPath (or Environment.GetFolderPath)

OS: Windows 7 Professional x64
.NET Framework v4.0.30319 Application is compiled for x86 CPU

What I tried so far:

  • restarting my application
  • restarting the computer
  • calling Environment.GetFolderPath(Environment.SpecialFolder.CommonDocuments);
  • direct calls to Win32-API SHGetKnownFolderPath

EDIT 2 Steps to reproduce:

  1. deactivate UAC on your computer [and restart!]
  2. go to C:\Users\Public\
  3. right click on "Public Documents" folder and select Properties
  4. select the 'Path' tab
  5. click 'Move...' and select a (new) folder on drive D: called TestDocuments
  6. click 'Apply'
  7. accept to move all files to the new location start the minimal application above
Bromo answered 26/8, 2011 at 6:14 Comment(7)
I suspect that redirecting/moving it isn't properly done... usually such a change would be done through some Group Policy... and there is also the aspect of "localization" (the real folder has a different name than is displayed) which seems to play a role in your case...Ironworks
The second line above where you combine EXPORT_REPOSITORY and "\\excel2007" will most likely give you a value of "\excel2007". At least according to documentation which states that "If path2 contains an absolute path, this method returns path2".Marionmarionette
@Marionmarionette thanks for pointing this out. I must have introduced that error on my local system - according to the versioning system.Bromo
possible duplicate of Function for getting localized path?Bandmaster
Am I missing something here? Why are you P/Invoking instead of using Environment.SpecialFolder.CommonDocuments?Uniformize
@SpikeX Please have a look at the history of this question. What you are suggesting was my first try and I've since been trying to get to the core of the problem.Bromo
Then from what it sounds like, the problem does not lie with this variable, instead, with how the user moved their documents folder. I suspect some value or some registry entry isn't getting updated somewhere, not to mention, some folders inside the C:\Users\ structure are junctions (not real folders), so moving them only moves the junction.Uniformize
B
5

tl;dr: Behaviour is by design and only appears when you're running an assembly that was compiled for x86 CPUs on a x64 OS


Longer version:
Environment.GetFolderPath(Environment.SpecialFolder.CommonDocuments) accesses the 32-bit hive of the Windows Registry.
The actual path to the folder is stored in the 64-bit hive. The issue has been forwarded to the Windows team and may be fixed in a future version of Windows.

For a bit more information see the Microsoft connect report


Workaround create a console application with the following code and compile it for ANY CPU

static void Main(string[] args)
{
        Console.WriteLine("{0}", Environment.GetFolderPath(System.Environment.SpecialFolder.CommonDocuments));
}

then call it from your main application:

Process proc = new Process();
ProcessStartInfo info = new ProcessStartInfo("myConsoleApp.exe");

// allow output to be read
info.RedirectStandardOutput = true;
info.RedirectStandardError = true;
info.UseShellExecute = false;
proc.StartInfo = info;

proc.Start(); 
proc.WaitForExit();
string path = proc.StandardOutput.ReadToEnd();

this will launch the ANY CPU executable, which only prints out the desired path to the standard output. The output then is read in the main application and you get the real path.

Bromo answered 13/9, 2011 at 14:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.