Introduction
Last week, I was scouring the internet trying to find a solution to an interesting problem that I was encountering while building up a "Windows Explorer"-like application. I was having difficulty accessing the Network Locations that are normally seen under the "This PC" node of the Windows Explorer.
After looking around for a good hour, I finally was able to find what I needed and create a quick helper library for those that are interested in browsing these programmatically.
Background
For this, we will just need a working knowledge of C#, and it may be helpful to have some knowledge of how to browse directories in C#, the System.IO.DriveInfo
& System.IO.DirectoryInfo
classes, static
methods, and the Shell32
DLL.
Walking through the Process
The first challenge I ran into was trying to find where these network locations were even stored. They had to be SOMEWHERE, after all. After looking around for a little while, I finally found that they are actually stored as shortcuts in the following directory: "%AppData%/Microsoft/Windows/Network Shortcuts".
Great! We found the "Network Shortcuts" for the current user! Now, to get to this location programmatically using C#. First, we cannot simply enter new System.IO.DirectoryInfo("%AppData%/Microsoft/Windows/Network Shortcuts")
because the System.IO.DirectoryInfo
class (which we will be using to browse these shortcuts) does not have any concept of Environment Variables. We would actually have to enter new System.IO.DirectoryInfo("C:\Users\<<current_user>>\AppData\Roaming\Microsoft\Windows\Network Shortcuts")
(replacing <<current_user>>
with your user in Windows) although this would take us to the correct location, this would not compensate for any changes to where this data was stored. What we would actually want to use is the enum
System.Environment.SpecialFolder
. To complete this, we would enter System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData)
; this is the programmatic equivalent of the "%AppData%
" environment variable that we use in the Windows Explorer. Using System.IO.Path.Combine
, we can create the full directory path to our network shortcuts:
private static string NetworkLocationsPath { get; } =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Microsoft", "Windows", "Network Shortcuts");
Now that we have the location of the NetworkShortcuts saved and ready to use, next, we need to iterate through each of the shortcuts in this directory. For this, as stated above, we will be using the System.IO.DirectoryInfo
class.
DirectoryInfo networkShortcuts = new DirectoryInfo(NetworkLocationsPath);
These shortcuts are not file shortcuts, but instead are folder shortcuts, so, instead of using System.IO.DirectoryInfo.GetFiles
, we would instead want to use System.IO.DirectoryInfo.GetDirectories
. Now with a quick foreach
statement, we will iterate through each of the "directories" in this folder.
After looking through each of the properties and methods within an instance of the DirectoryInfo
class for one of my shortcuts, I ran into two problems. First, I could not tell if the directory was a shortcut or not, and second, I could not get the Target of the shortcut if the instance was referencing a shortcut!
After looking around for a while, I found two solutions to my problem:
The first solution I found, was a brilliantly coded custom class that would extract the target path from the shortcut (code for that is below) the benefit of this class was the fact that I would not need to reference the Shell32
DLL. I manipulated the class slightly to be a static
class and hid a few of the classes and properties that were exposed that I felt didn't necessarily need to be exposed. The following is NOT my code. I have looked for the original source that I pulled the code from and could not find it - if you happen to know it, please let me know and I will update this article accordingly.
using System;
using System.Runtime.InteropServices;
using System.Text;
internal static class ShortcutResolver
{
#region Signatures imported from http://pinvoke.net
[DllImport("shfolder.dll", CharSet = CharSet.Auto)]
private static extern int SHGetFolderPath
(IntPtr hwndOwner, int nFolder, IntPtr hToken, int dwFlags, StringBuilder lpszPath);
[Flags()]
enum SLGP_FLAGS
{
SLGP_SHORTPATH = 0x1,
SLGP_UNCPRIORITY = 0x2,
SLGP_RAWPATH = 0x4
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
struct WIN32_FIND_DATAW
{
public uint dwFileAttributes;
public long ftCreationTime;
public long ftLastAccessTime;
public long ftLastWriteTime;
public uint nFileSizeHigh;
public uint nFileSizeLow;
public uint dwReserved0;
public uint dwReserved1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string cFileName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
public string cAlternateFileName;
}
[Flags()]
enum SLR_FLAGS
{
SLR_NO_UI = 0x1,
SLR_ANY_MATCH = 0x2,
SLR_UPDATE = 0x4,
SLR_NOUPDATE = 0x8,
SLR_NOSEARCH = 0x10,
SLR_NOTRACK = 0x20,
SLR_NOLINKINFO = 0x40,
SLR_INVOKE_MSI = 0x80
}
[ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("000214F9-0000-0000-C000-000000000046")]
interface IShellLinkW
{
void GetPath([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile,
int cchMaxPath, out WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags);
void GetIDList(out IntPtr ppidl);
void SetIDList(IntPtr pidl);
void GetDescription([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName,
int cchMaxName);
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
void GetWorkingDirectory([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir,
int cchMaxPath);
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
void GetArguments([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs,
int cchMaxPath);
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
void GetHotkey(out short pwHotkey);
void SetHotkey(short wHotkey);
void GetShowCmd(out int piShowCmd);
void SetShowCmd(int iShowCmd);
void GetIconLocation([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath,
int cchIconPath, out int piIcon);
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
void Resolve(IntPtr hwnd, SLR_FLAGS fFlags);
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
[ComImport, Guid("0000010c-0000-0000-c000-000000000046"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IPersist
{
[PreserveSig]
void GetClassID(out Guid pClassID);
}
[ComImport, Guid("0000010b-0000-0000-C000-000000000046"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IPersistFile : IPersist
{
new void GetClassID(out Guid pClassID);
[PreserveSig]
int IsDirty();
[PreserveSig]
void Load([In, MarshalAs(UnmanagedType.LPWStr)]
string pszFileName, uint dwMode);
[PreserveSig]
void Save([In, MarshalAs(UnmanagedType.LPWStr)] string pszFileName,
[In, MarshalAs(UnmanagedType.Bool)] bool fRemember);
[PreserveSig]
void SaveCompleted([In, MarshalAs(UnmanagedType.LPWStr)] string pszFileName);
[PreserveSig]
void GetCurFile([In, MarshalAs(UnmanagedType.LPWStr)] string ppszFileName);
}
const uint STGM_READ = 0;
const int MAX_PATH = 260;
[
ComImport(),
Guid("00021401-0000-0000-C000-000000000046")
]
private class ShellLink
{
}
#endregion
public static string GetPath(string filename)
{
ShellLink link = new ShellLink();
((IPersistFile)link).Load(filename, STGM_READ);
StringBuilder sb = new StringBuilder(MAX_PATH);
WIN32_FIND_DATAW data = new WIN32_FIND_DATAW();
((IShellLinkW)link).GetPath(sb, sb.Capacity, out data, 0);
return sb.ToString();
}
}
The second solution I found was through referencing the Shell32
library in the Microsoft Shell Controls and Automation COM type library. To add this in Visual Studio, in your project, right click on References, click Add Reference, expand the COM option on the left hand side of the Reference Manager, click on Type Libraries, scroll down to Microsoft Shell Controls And Automation, check the checkbox, and click OK.
Now, to access the information, we will use the below code. I set up this code to use static
methods and variables so that it incorporates into the helper class that I will outline later.
private static string NetworkLocationsPath { get; } =
System.IO.Path.Combine(System.Environment.GetFolderPath
(System.Environment.SpecialFolder.ApplicationData),
"Microsoft", "Windows", "Network Shortcuts");
#region Shell Support
private static Shell32.Folder _networkLocationsFolder;
private static Shell32.Folder NetworkLocationsFolder
{
get
{
if (_networkLocationsFolder == null)
{
Shell32.Shell shell = new Shell32.Shell();
_networkLocationsFolder = shell.NameSpace(NetworkLocationsPath);
}
return _networkLocationsFolder;
}
}
private static string GetShortCutPath(string name)
{
try
{
Shell32.FolderItem folderItem = NetworkLocationsFolder.ParseName(name);
if (folderItem == null || !folderItem.IsLink)
return null;
Shell32.ShellLinkObject link = (Shell32.ShellLinkObject)folderItem.GetLink;
return link.Path;
}
catch
{
return null;
}
}
#endregion
With the extrapolated path information, I was able to create a new System.IO.DirectoryInfo
instance with the path.
The above was convenient, however, I wanted to wrap all of this up into a simple class that would act similarly to the System.IO.DriveInfo
class that would allow me to get all of the network locations and handle all of the name parsing, shortcut target extraction and effectively provide me with a one-stop-shop for my needs.
I decided to create this class in the System.IO
namespace, although feel free to change that as you see fit.
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Security;
namespace System.IO
{
[ComVisible(true)]
public sealed class NetworkLocationInfo : ISerializable
{
public bool IsReady { get { return RootDirectory.Exists; } }
public bool IsMapped { get { return ShortcutFile.Exists; } }
public string Name { get { return RootDirectory.FullName; } }
public string ShareName { get; }
public string ServerName { get; }
public string ShareLabel
{
get { return ShortcutFile.Name; }
set {
if (!ShortcutFile.Exists)
throw new FileNotFoundException("Cannot find the network location shortcut file");
ShortcutFile.MoveTo(Path.Combine(ShortcutFile.Parent.FullName, value));
}
}
public DirectoryInfo RootDirectory { get; }
private DirectoryInfo _shortcutFile;
private DirectoryInfo ShortcutFile {
get
{
if (_shortcutFile == null)
_shortcutFile = FindShortCutFile();
return _shortcutFile;
}
}
[SecuritySafeCritical]
public NetworkLocationInfo(string networkLocationPath)
: this(networkLocationPath, null) { }
private NetworkLocationInfo(string networkLocationPath, DirectoryInfo shortcutFile)
{
if (string.IsNullOrWhiteSpace(networkLocationPath))
throw new ArgumentNullException(nameof(networkLocationPath));
if (!networkLocationPath.StartsWith("\\\\"))
throw new ArgumentException("The UNC path should be of the form \\\\server\\share");
string root = networkLocationPath.TrimStart("\\");
int i = root.IndexOf("\\");
if (i < 0 || i + 1 == root.Length)
throw new ArgumentException("The UNC path should be of the form \\\\server\\share");
ServerName = root.Substring(0, i);
root = root.Substring(i + 1);
i = root.IndexOf("\\");
ShareName = i < 0 ? root : root.Substring(0, i);
if (string.IsNullOrWhiteSpace(ShareName))
throw new ArgumentException("The UNC path should be of the form \\\\server\\share");
RootDirectory = new DirectoryInfo(string.Concat("\\\\", ServerName, "\\", ShareName));
_shortcutFile = shortcutFile ?? FindShortCutFile();
}
private DirectoryInfo FindShortCutFile()
{
DirectoryInfo network = new DirectoryInfo(NetworkLocationsPath);
foreach (DirectoryInfo dir in network.EnumerateDirectories())
{
string path = GetShortCutPath(dir.Name);
if (string.Equals(RootDirectory.FullName, path, StringComparison.OrdinalIgnoreCase))
return dir;
}
return new DirectoryInfo(Path.Combine(NetworkLocationsPath,
string.Concat(ShareName, " (", ServerName, ")")));
}
#region ISerializable methods
internal NetworkLocationInfo(SerializationInfo info, StreamingContext context)
{
if (info == null)
throw new ArgumentNullException(nameof(info));
ShareName = info.GetString(nameof(ShareName));
ServerName = info.GetString(nameof(ServerName));
RootDirectory = new DirectoryInfo(info.GetString(nameof(RootDirectory)));
_shortcutFile = new DirectoryInfo(info.GetString(nameof(ShortcutFile)));
}
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
if (info == null)
throw new ArgumentNullException(nameof(info));
info.AddValue(nameof(ShareName), ShareName);
info.AddValue(nameof(ServerName), ServerName);
info.AddValue(nameof(RootDirectory), RootDirectory.FullName);
info.AddValue(nameof(ShortcutFile), ShortcutFile.FullName);
}
#endregion
#region Object methods
public override string ToString()
{
return Name;
}
public override bool Equals(object obj)
{
if (obj == null)
return false;
if (obj is NetworkLocationInfo)
return string.Equals(((NetworkLocationInfo)obj).Name,
Name, StringComparison.OrdinalIgnoreCase);
return false;
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
#endregion
#region Static Methods
private static string NetworkLocationsPath { get; } =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Microsoft", "Windows", "Network Shortcuts");
#region Shell Support
private static Shell32.Folder _networkLocationsFolder;
private static Shell32.Folder NetworkLocationsFolder
{
get
{
if (_networkLocationsFolder == null)
{
Shell32.Shell shell = new Shell32.Shell();
_networkLocationsFolder = shell.NameSpace(NetworkLocationsPath);
}
return _networkLocationsFolder;
}
}
private static string GetShortCutPath(string name)
{
try
{
Shell32.FolderItem folderItem = NetworkLocationsFolder?.ParseName(name);
if (folderItem == null || !folderItem.IsLink)
return null;
Shell32.ShellLinkObject link = (Shell32.ShellLinkObject)folderItem.GetLink;
return link.Path;
}
catch
{
return null;
}
}
#endregion
public static NetworkLocationInfo[] GetNetworkLocations()
{
if (NetworkLocationsFolder == null)
return new NetworkLocationInfo[] { };
DirectoryInfo networkShortcuts = new DirectoryInfo(NetworkLocationsPath);
DirectoryInfo[] subDirectories = networkShortcuts.GetDirectories();
NetworkLocationInfo[] locations = new NetworkLocationInfo[subDirectories.Length];
if (subDirectories.Length == 0)
return locations;
int i = 0;
foreach (DirectoryInfo dir in subDirectories)
{
string networkLocationPath = GetShortCutPath(dir.Name);
if (string.IsNullOrWhiteSpace(networkLocationPath))
continue;
try
{
NetworkLocationInfo info = new NetworkLocationInfo(networkLocationPath, dir);
locations[i++] = info;
}
catch { continue; }
}
if (i < locations.Length)
Array.Resize(ref locations, i);
return locations;
}
#endregion
}
}
With this helper class, we can now browse our network location shortcuts by simply calling NetworkLocationInfo.GetNetworkLocations()
.
For my uses, I was creating a Tree
with all of the network locations, below is how I was able to implement this helper class.
foreach (NetworkLocationInfo n in NetworkLocationInfo.GetNetworkLocations())
{
if (!n.IsReady)
continue;
TreeNode aNode = new TreeNode(n.ShareLabel,
(int)ImageKeys.NetworkDrive, (int)ImageKeys.NetworkDrive);
aNode.Tag = n.RootDirectory;
aNode.ImageKey = "network-location";
try
{
GetDirectories(n.RootDirectory.EnumerateDirectories(), aNode);
nodeToAddTo.Nodes.Add(aNode);
}
catch { }
}
Enjoy & happy coding!
</the_madness>
I'm a software consultant who would like to help your company grow.
Need someone to bring a fresh idea to the table?
Want a sounding board for proposed solutions?
Have some projects that "just need to get done"?
I can help.
Architectural Design, or Reviews
Database Design, Development, Optimization, or Administration
Backend Review or Development
Devops Scripting or Implementation
Technical Interviewing
I've worked in both AWS and Azure, have experience in Node and .NETCore stack in the healthcare and IoT. I've worked with small platforms servicing thousands of transactions per second and global corporations servicing 500,000+ transactions per second.
I favor open source solutions but can adopt whatever standards you already have in place. I have extensive knowledge about multiple architectural patterns and can help you sort through which is best for you.
I'd like to help you streamline and optimize your software and processes to take full advantage of the environment you are developing in, enabling you to fully serve your customers.