Get Users folder from different drive - c#

My question is about to delete a shortcut from all user's desktops.
I've updated my Dektop folder from C:\Users\\[User]\Desktop to G:\Users\\[User]\Desktop because I've some important data on desktop and I don't want to lose any user data if I re-install windows or my windows(any how) get corrupted. I've also updated documents and downloads folder to save the data to a drive other than '%SystemDrive%`.
I've done this by
- Open WindowsExplorer
-> Right click on Desktop (at the left panel and under the Quick Access list)
-> Properties
-> Location
-> Write new desktop folder location in textbox
-> Apply
-> OK.
Everything works fine, but when I want to delete a shortcut from all user's desktops, I get users folder only from C drive.
My code for deleting shortcut looks like
foreach (var userFolder in userFolders) //userFolders contains all sub directories of user directory
{
var shortcutFullName = userFolder + "\\Desktop\\" + shortcutName;
if (File.Exists(shortcutFullName))
{
File.Delete(shortcutFullName);
}
}
I've tried How do you get the Default Users folder
and
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);

You should be able to get Desktop folder using Win32 APIs.
[DllImport("shell32.dll")]
static extern bool SHGetSpecialFolderPath(IntPtr hwndOwner, [Out] StringBuilder lpszPath, int nFolder, bool fCreate);
const int CSIDL_COMMON_DESKTOPDIRECTORY = 0x19;
const int CSIDL_DESKTOP = 0x0;
public static void GetCommonProfilePath()
{
    StringBuilder allUserProfile = new StringBuilder(260);
    SHGetSpecialFolderPath(IntPtr.Zero, allUserProfile, CSIDL_COMMON_DESKTOPDIRECTORY, false);
    string commonDesktopPath = allUserProfile.ToString();
    //The above API call returns: C:\Users\Public\Desktop
    Console.WriteLine(commonDesktopPath);
 
 
    SHGetSpecialFolderPath(IntPtr.Zero, allUserProfile, CSIDL_DESKTOP = 0, false);
    // This should give you user specific path.
    Console.WriteLine(allUserProfile.ToString());
}
Refer this blog for more details.

Related

External Tools Visual Studio

I created an application to run, but build an External Tool from the application for Visual Studio. Up until 15.4.0 the application worked fine, but they modified the registry. Based on Microsoft's changes, the External Tools are saved under this sub key:
SOFTWARE\Microsoft\VisualStudio\14.0_Config\External Tools
When you observe the key, you clearly see folders (sub keys) of tools, Guid, Error Lookup, and a couple others that are included with Visual Studio. The issue though, if you manually create the tool in Visual Studio the key information is dumped directly to the root External Tool key.
So if I do either of these approaches:
var user = RegistryKey.OpenBaseKey(RegistryHive.CurrenUser, RegistryView.Default);
var tools = user.OpenSubKey(#"SOFTWARE\Microsoft\VisualStudio\14.0_Config\External Tools", true);
var path = Environment.GetCommandLineArgs()[0];
tools.SetValue("ToolArg", "$(ItemPath)");
tools.SetValue("ToolCmd", path);
tools.SetValue("ToolDir", String.Empty);
tools.SetValue("ToolOpt", 0x12);
tools.SetValue("ToolsSourceKey", String.Empty);
tools.SetValue("ToolTitle", "My Tool");
var user = RegistryKey.OpenBaseKey(RegistryHive.CurrenUser, RegistryView.Default);
var tools = user.OpenSubKey(#"SOFTWARE\Microsoft\VisualStudio\14.0_Config\External Tools", true);
var sub = tools.CreateSubKey("MyTool");
var path = Environment.GetCommandLineArgs()[0];
sub.SetValue("ToolArg", "$(ItemPath)");
sub.SetValue("ToolCmd", path);
sub.SetValue("ToolDir", String.Empty);
sub.SetValue("ToolOpt", 0x12);
sub.SetValue("ToolsSourceKey", String.Empty);
sub.SetValue("ToolTitle", "My Tool");
The tool doesn't appear in the list, or toolbar. Is there something different for Visual Studio 2017 15.5.* that makes this no longer work from code? To make matters worst, the key doesn't always appear when created in Visual Studio 2017 manually.
In Visual Studio 2017, External Tools are stored in a private registry hive in the user's local application data folder. If you run Sysinternals Process Monitor tool, you'll see Visual Studio reading/writing to a key that starts with \REGISTRY\A\ - that's how you know it's a private registry hive. To update them, you will need to load that registry hive by P/Invoking RegLoadAppKey and attaching to the resulting handle. An example of that can be found here:
RegLoadAppKey working fine on 32-bit OS, failing on 64-bit OS, even if both processes are 32-bit
The title may seem misleading at first, but the example given in the question shows exactly how to call RegLoadAppKey and open a sub key underneath.
The next thing you'll have to contend with is finding the private registry hive. Visual Studio stores the private registry hive in a sub folder of the user's local application data folder. The sub folder name will start with Microsoft\VisualStudio\15.0_ and will then be followed by a 32-bit hexadecimal value. I'm not really sure what that value is, or how to gracefully discover it. It's different for each user. My approach was to select the newest folder that starts with "15.0" and assume it's correct. If someone has a better way to identify this folder, I would love to see it.
I've dubbed the combination of version number and hexadecimal string a "version tag". You'll need to keep track of it, because the version tag will be used again as a sub key in the private registry hive.
Putting it all together, I created a VisualStudioContext class that locates the private registry hive and loads the root key.
public class VisualStudioContext : IDisposable
{
public string VersionTag { get; }
public string UserFolder { get; }
public string PrivateRegistryPath { get; }
public SafeRegistryHandle RegistryHandle { get; }
public RegistryKey RootKey { get; }
private static readonly Lazy<VisualStudioContext> LazyInstance = new Lazy<VisualStudioContext>(() => new VisualStudioContext());
public static VisualStudioContext Instance => LazyInstance.Value;
private VisualStudioContext()
{
try
{
string localAppDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string vsFolder = $"{localAppDataFolder}\\Microsoft\\VisualStudio";
var vsFolderInfo = new DirectoryInfo(vsFolder);
DateTime lastDateTime = DateTime.MinValue;
foreach (DirectoryInfo dirInfo in vsFolderInfo.GetDirectories("15.0_*"))
{
if (dirInfo.CreationTime <= lastDateTime)
continue;
UserFolder = dirInfo.FullName;
lastDateTime = dirInfo.CreationTime;
}
if (UserFolder == null)
throw new Exception($"No Visual Studio folders found in \"{vsFolder}\"");
}
catch (Exception ex)
{
throw new Exception("Unable to open Visual Studio folder", ex);
}
VersionTag = Path.GetFileName(UserFolder);
PrivateRegistryPath = $"{UserFolder}\\privateregistry.bin";
int handle = RegistryNativeMethods.RegLoadAppKey(PrivateRegistryPath);
RegistryHandle = new SafeRegistryHandle(new IntPtr(handle), true);
RootKey = RegistryKey.FromHandle(RegistryHandle);
}
public void Dispose()
{
RootKey?.Close();
RegistryHandle?.Dispose();
}
public class Exception : ApplicationException
{
public Exception(string message) : base(message)
{
}
public Exception(string message, Exception innerException) : base(message, innerException)
{
}
}
internal static class RegistryNativeMethods
{
[Flags]
public enum RegSAM
{
AllAccess = 0x000f003f
}
private const int REG_PROCESS_APPKEY = 0x00000001;
// approximated from pinvoke.net's RegLoadKey and RegOpenKey
// NOTE: changed return from long to int so we could do Win32Exception on it
[DllImport("advapi32.dll", SetLastError = true)]
private static extern int RegLoadAppKey(String hiveFile, out int hKey, RegSAM samDesired, int options, int reserved);
public static int RegLoadAppKey(String hiveFile)
{
int hKey;
int rc = RegLoadAppKey(hiveFile, out hKey, RegSAM.AllAccess, REG_PROCESS_APPKEY, 0);
if (rc != 0)
{
throw new Win32Exception(rc, "Failed during RegLoadAppKey of file " + hiveFile);
}
return hKey;
}
}
}
You can use it open the External Tools key like this:
using (var context = VisualStudioContext.Instance)
{
RegistryKey keyExternalTools =
context.RootKey.OpenSubKey($"Software\\Microsoft\\VisualStudio\\{context.VersionTag}\\External Tools", true);
// Do something interesting here
}
It seems you can register external tools in VS 2017 and 2019 by adding a <UserCreatedTool> in the <ExternalTools> section of the Settings/CurrentSettings.vssettings file in the local %AppData% directory of the respective VS version.
Simply create an external tool in VS manually, and search for the entry you created.
This describes how I found out ;)

Delete a directory where someone has opened a file

I am trying to programmatically delete and replace the contents of an application, "App A", using an "installer" program, which is just a custom WPF .exe app, we'll call "App B". (My question concerns code in "App B".)
GUI Setup (not particularly important)
App B has a GUI where a user can pick computer names to copy App A onto. A file picker is there the admin uses to fill in the source directory path on the local machine by clicking "App A.exe". There are also textboxes for a user name and password, so the admin can enter their credentials for the target file server where App A will be served - the code impersonates the user with these to prevent permission issues. A "Copy" button starts the routine.
Killing App A, File Processes, and Doing File Deletion
The Copy routine starts by killing the "App A.exe" process on all computers in the domain, as well as explorer.exe, in case they had App A's explorer folder open. Obviously this would be done afterhours, but someone may still have left things open and locked their machine before going home. And that's really the base of the problem I'm looking to solve.
Prior to copying over the updated files, we want to delete the entire old directory. In order to delete the directory (and its subdirectories), each file within them has to be deleted. But say they had a file open from App A's folder. The code finds any locking process on any file prior to deleting it (using code from Eric J.'s answer at How do I find out which process is locking a file using .NET? ), it kills that process on whatever computer it is running on. If local, it just uses:
public static void localProcessKill(string processName)
{
foreach (Process p in Process.GetProcessesByName(processName))
{
p.Kill();
}
}
If remote, it uses WMI:
public static void remoteProcessKill(string computerName, string fullUserName, string pword, string processName)
{
var connectoptions = new ConnectionOptions();
connectoptions.Username = fullUserName; // #"YourDomainName\UserName";
connectoptions.Password = pword;
ManagementScope scope = new ManagementScope(#"\\" + computerName + #"\root\cimv2", connectoptions);
// WMI query
var query = new SelectQuery("select * from Win32_process where name = '" + processName + "'");
using (var searcher = new ManagementObjectSearcher(scope, query))
{
foreach (ManagementObject process in searcher.Get())
{
process.InvokeMethod("Terminate", null);
process.Dispose();
}
}
}
Then it can delete the file. All is well.
Directory Deletion Failure
In my code below, it is doing the recursive deletion of the files, and does it fine, up until the Directory.Delete(), where it will say The process cannot access the file '\\\\SERVER\\C$\\APP_A_DIR' because it is being used by another process, because I am attempting to delete the directory while I had a file still open from it (even though the code was actually able to delete the physical file-the instance is still open).
public void DeleteDirectory(string target_dir)
{
string[] files = Directory.GetFiles(target_dir);
string[] dirs = Directory.GetDirectories(target_dir);
List<Process> lstProcs = new List<Process>();
foreach (string file in files)
{
File.SetAttributes(file, FileAttributes.Normal);
lstProcs = ProcessHandler.WhoIsLocking(file);
if (lstProcs.Count == 0)
File.Delete(file);
else // deal with the file lock
{
foreach (Process p in lstProcs)
{
if (p.MachineName == ".")
ProcessHandler.localProcessKill(p.ProcessName);
else
ProcessHandler.remoteProcessKill(p.MachineName, txtUserName.Text, txtPassword.Password, p.ProcessName);
}
File.Delete(file);
}
}
foreach (string dir in dirs)
{
DeleteDirectory(dir);
}
//ProcessStartInfo psi = new ProcessStartInfo();
//psi.Arguments = "/C choice /C Y /N /D Y /T 1 & Del " + target_dir;
//psi.WindowStyle = ProcessWindowStyle.Hidden;
//psi.CreateNoWindow = true;
//psi.FileName = "cmd.exe";
//Process.Start(psi);
//ProcessStartInfo psi = new ProcessStartInfo();
//psi.Arguments = "/C RMDIR /S /Q " + target_dir;
//psi.WindowStyle = ProcessWindowStyle.Hidden;
//psi.CreateNoWindow = true;
//psi.FileName = "cmd.exe";
//Process.Start(psi);
// This is where the failure occurs
//FileSystem.DeleteDirectory(target_dir, DeleteDirectoryOption.DeleteAllContents);
Directory.Delete(target_dir, false);
}
I've left things I've tried commented out in the code above. While I can kill processes attached to the files and delete them, is there a way to kill processes attached to folders, in order to delete them?
Everything online I saw tries to solve this using a loop-check with a delay. This will not work here. I need to kill the file that was opened-which I do-but also ensure the handle is released from the folder so it can also be deleted, at the end. Is there a way to do this?
Another option I considered that will not work:
I thought I might just freeze the "installation" (copying) process by marking that network folder for deletion in the registry and schedule a programmatic reboot of the file server, then re-run afterwards. How to delete Thumbs.db (it is being used by another process) gives this code by which to do this:
[DllImport("kernel32.dll")]
public static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, int dwFlags);
public const int MOVEFILE_DELAY_UNTIL_REBOOT = 0x4;
//Usage:
MoveFileEx(fileName, null, MOVEFILE_DELAY_UNTIL_REBOOT);
But it has in the documentation that If MOVEFILE_DELAY_UNTIL_REBOOT is used, "the file cannot exist on a remote share, because delayed operations are performed before the network is available." And that was assuming it might have allowed a folder path, instead of a file name. (Reference: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365240(v=vs.85).aspx ).
So there are 2 scenarios I wanted to handle - both are where the folder is prevented from being deleted:
1) A user has a file open on their local machine from the application's folder on the file server.
2) An admin has a file open from the application's folder, which they will see while remoted (RDP'ed) into the server.
I've settled on a way forward. If I run into this issue, I figure about all I can do is to either:
1) Freeze the "installation" (copying) process by simply scheduling a programmatic reboot of the file server in the IOException block if I really want to blow away the folder (not ideal and probably overkill, but others running across this same issue may be inspired by this option). The installer will need to be run again to copy the files after the server reboots.
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, out SafeTokenHandle phToken);
LogonUser(userName, domainName, password,
LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT,
out safeTokenHandle);
try
{
using (WindowsIdentity newId = new WindowsIdentity(safeTokenHandle.DangerousGetHandle()))
{
using (WindowsImpersonationContext impersonatedUser = newId.Impersonate())
{
foreach (Computer pc in selectedList) // selectedList is an ObservableCollection<Computer>
{
string newDir = "//" + pc.Name + txtExtension.Text; // the textbox has /C$/APP_A_DIR in it
if (Directory.Exists(newDir))
{
DeleteDirectory(newDir); // <-- this is where the exception happens
}
}
}
}
}
catch (IOException ex)
{
string msg = "There was a file left open, thereby preventing a full deletion of the previous folder, though all contents have been removed. Do you wish to proceed with installation, or reboot the server and begin again, in order to remove and replace the installation directory?";
MessageBoxResult result = MessageBox.Show(msg, "Reboot File Server?", MessageBoxButton.OKCancel);
if (result == MessageBoxResult.OK)
{
var psi = new ProcessStartInfo("shutdown","/s /t 0");
psi.CreateNoWindow = true;
psi.UseShellExecute = false;
Process.Start(psi);
}
else
{
MessageBox.Show("Copying files...");
FileSystem.CopyDirectory(sourcePath, newDir);
MessageBox.Show("Completed!");
}
}
Reference: How to shut down the computer from C#
OR
2) Ignore it altogether and perform my copy, anyway. The files actually do delete, and I found there's really no problem with having a folder I can't delete, as long as I can write to it, which I can. So this is the one I ultimately picked.
So again, in the IOException catch block:
catch (IOException ex)
{
if (ex.Message.Contains("The process cannot access the file") &&
ex.Message.Contains("because it is being used by another process") )
{
MessageBox.Show("Copying files...");
FileSystem.CopyDirectory(sourcePath, newDir);
MessageBox.Show("Completed!");
}
else
{
string err = "Issue when performing file copy: " + ex.Message;
MessageBox.Show(err);
}
}
Code above leaves out my model for Computer, which just has a Name node in it, and the rest of my Impersonation class, which is based on my own rendition of several different (but similar) code blocks of how they say to do it. If anyone needs that, here are a couple of links to some good answers:
Need Impersonation when accessing shared network drive
copy files with authentication in c#
Related: Cannot delete directory with Directory.Delete(path, true)

C# Get the Windows Explore Path which has the focus

I would like to get the Path of the windows which has the focus.
Ex: I have 3 windows Opened
a. C:\Windows
b. C:\Windows\System32
c. C:\Users\COMP-0\Documents
And i am working on c (C:\Users\COMP-0\Documents)
So i would like to get this path (C:\Users\COMP-0\Documents) programmatically in C#.
Expanding on this answer to get the selected files in a folder, you can use a similar approach to get the current folder and therefore it's path.
This needs some COM and requires:
Getting the active window using GetForegroundWindow
Find the current list of InternetExplorer windows using SHDocVw.ShellWindows,
Matching handle pointers to find the current window
Getting hold of the folder path inside the active window using the IShellFolderViewDual2 COM interface.
There are a couple of caveats to be aware of:
Special folders (Favourites, My Computer etc) will give you the file path as "::{GUID}" where the GUID points to the CLSID for that folder in the registry. It is probably possible to convert that value to a path.
Going to "Desktop" will return null for the current folder
Focussing Internet Explorer will trigger a match on the active window so we need to ensure we are in a Shell Folder
If in a special folder or Desktop this code will just return the current window title - usually the name of the special folder - using the details in this answer.
private static string GetActiveExplorerPath()
{
// get the active window
IntPtr handle = GetForegroundWindow();
// Required ref: SHDocVw (Microsoft Internet Controls COM Object) - C:\Windows\system32\ShDocVw.dll
ShellWindows shellWindows = new SHDocVw.ShellWindows();
// loop through all windows
foreach (InternetExplorer window in shellWindows)
{
// match active window
if (window.HWND == (int)handle)
{
// Required ref: Shell32 - C:\Windows\system32\Shell32.dll
var shellWindow = window.Document as Shell32.IShellFolderViewDual2;
// will be null if you are in Internet Explorer for example
if (shellWindow != null)
{
// Item without an index returns the current object
var currentFolder = shellWindow.Folder.Items().Item();
// special folder - use window title
// for some reason on "Desktop" gives null
if (currentFolder == null || currentFolder.Path.StartsWith("::"))
{
// Get window title instead
const int nChars = 256;
StringBuilder Buff = new StringBuilder(nChars);
if (GetWindowText(handle, Buff, nChars) > 0)
{
return Buff.ToString();
}
}
else
{
return currentFolder.Path;
}
}
break;
}
}
return null;
}
// COM Imports
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);

Getting Downloads Folder in C#? [duplicate]

This question already has answers here:
How to programmatically derive Windows Downloads folder "%USERPROFILE%/Downloads"?
(11 answers)
Closed 5 years ago.
I have made some code that will search directories and display files in a listbox.
DirectoryInfo dinfo2 = new DirectoryInfo(#"C:\Users\Hunter\Downloads");
FileInfo[] Files2 = dinfo2.GetFiles("*.sto");
foreach (FileInfo file2 in Files2)
{
listBox1.Items.Add(file2.Name);
}
However, where it says Users\Hunter - well, when people get my software, their name is not Hunter. So how can I automatically detect the user's Downloads folder?
I have tried this:
string path = Environment.SpecialFolder.UserProfile + #"\Downloads";
DirectoryInfo dinfo2 = new DirectoryInfo(Environment.SpecialFolder.UserProfile + path);
FileInfo[] Files2 = dinfo2.GetFiles("*.sto");
foreach (FileInfo file2 in Files2)
{
listBox1.Items.Add(file2.Name);
}
I get an error though.
The Downloads folder is a so called "known" folder, together with Documents, Videos, and others.
Do NOT:
combine hardcoded path segments to retrieve known folder paths
assume known folders are children of the user folder
abuse a long deprecated registry key storing outdated paths
Known folders can be redirected anywhere in their property sheets. I've gone into more detail on this several years ago in my CodeProject article.
Do:
use the WinAPI method SHGetKnownFolderPath as it is the intended and only correct method to retrieve those paths.
You can p/invoke it as follows (I've provided only a few GUIDs which cover the new user folders):
public enum KnownFolder
{
Contacts,
Downloads,
Favorites,
Links,
SavedGames,
SavedSearches
}
public static class KnownFolders
{
private static readonly Dictionary<KnownFolder, Guid> _guids = new()
{
[KnownFolder.Contacts] = new("56784854-C6CB-462B-8169-88E350ACB882"),
[KnownFolder.Downloads] = new("374DE290-123F-4565-9164-39C4925E467B"),
[KnownFolder.Favorites] = new("1777F761-68AD-4D8A-87BD-30B759FA33DD"),
[KnownFolder.Links] = new("BFB9D5E0-C6A9-404C-B2B2-AE6DB6AF4968"),
[KnownFolder.SavedGames] = new("4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4"),
[KnownFolder.SavedSearches] = new("7D1D3A04-DEBB-4115-95CF-2F29DA2920DA")
};
public static string GetPath(KnownFolder knownFolder)
{
return SHGetKnownFolderPath(_guids[knownFolder], 0);
}
[DllImport("shell32",
CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)]
private static extern string SHGetKnownFolderPath(
[MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags,
nint hToken = 0);
}
Here's an example of retrieving the path of the Downloads folder:
string downloadsPath = KnownFolders.GetPath(KnownFolder.Downloads);
Console.WriteLine($"Downloads folder path: {downloadsPath}");
NuGet Package
If you don't want to p/invoke yourself, have a look at my NuGet package (note that the usage is different, please check its README).
The easiest way is:
Process.Start("shell:Downloads");
If you only need to get the current user's download folder path, you can use this:
I extracted it from #PacMani 's code.
// using Microsoft.Win32;
string GetDownloadFolderPath()
{
return Registry.GetValue(#"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders", "{374DE290-123F-4565-9164-39C4925E467B}", String.Empty).ToString();
}
Note:
SHGetKnownFolderPath will return the WRONG value if you changed the download-folder.
The only thing that will ever return you the correct value is reading the shell-folders registry-key 374DE290-123F-4565-9164-39C4925E467B on Windows.
Now you can either use the "!Do not use this registry key", or you can get the wrong value.
You decide which is better for you.
Cross-Platform version:
public static string GetHomePath()
{
// Not in .NET 2.0
// System.Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (System.Environment.OSVersion.Platform == System.PlatformID.Unix)
return System.Environment.GetEnvironmentVariable("HOME");
return System.Environment.ExpandEnvironmentVariables("%HOMEDRIVE%%HOMEPATH%");
}
public static string GetDownloadFolderPath()
{
if (System.Environment.OSVersion.Platform == System.PlatformID.Unix)
{
string pathDownload = System.IO.Path.Combine(GetHomePath(), "Downloads");
return pathDownload;
}
return System.Convert.ToString(
Microsoft.Win32.Registry.GetValue(
#"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
,"{374DE290-123F-4565-9164-39C4925E467B}"
,String.Empty
)
);
}
string download = Environment.GetEnvironmentVariable("USERPROFILE")+#"\"+"Downloads";
http://msdn.microsoft.com/en-us//library/system.environment.specialfolder.aspx
There are the variables with the path to some special folders.
typically, your software shall have a configurable variable that stores the user's download folder, which can be assigned by the user, and provide a default value when not set. You can store the value in app config file or the registry.
Then in your code read the value from where it's stored.

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

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:
deactivate UAC on your computer [and restart!]
go to C:\Users\Public\
right click on "Public Documents" folder and select
Properties
select the 'Path' tab
click 'Move...' and select a (new) folder on drive D: called TestDocuments
click 'Apply'
accept to move all files to the new location start the minimal
application above
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.

Categories