|
|||||||||||||||||||||||
|
|||||||||||||||||||||||
|
Announcements
Services
Chapters
Feature Zones
|
IntroductionC# and .NET have been hailed by Microsoft as the Windows programming environment of the future. Just what does that actually mean? Is the programmer shielded so much from Windows that attempting to do anything useful is difficult if not impossible? Is it just another VB? This article demonstrates a Windows Shell hook/extension in C#, demonstrating how easy it is to consume COM interfaces and to deploy the final code as though it is a bona-fida COM object. Hooking into the ShellOne of the simplest forms of shell extension is to hook into all the HRESULT Execute(
LPSHELLEXECUTEINFO pei
);
To register a concrete implementation of this interface, we need to register the CLSID of our component in the So, now that we know what the shell expects us to do, how do we do it from C#? First of all, we need to create a C# object which implements [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("000214FB-0000-0000-C000-000000000046")]
public interface IShellExecuteHook{
[PreserveSig()]
int Execute(SHELLEXECUTEINFO sei);
}
Notice the The [StructLayout(LayoutKind.Sequential)]
public class SHELLEXECUTEINFO {
public int cbSize;
public int fMask;
public int hwnd;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpVerb;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpFile;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpParameters;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpDirectory;
public int nShow;
public int hInstApp;
public int lpIDList;
public string lpClass;
public int hkeyClass;
public int dwHotKey;
public int hIcon;
public int hProcess;
}
For our sample, we are only interested in fields up to We've now got a fully defined interface which is declared to be a COM interface. As a quick test, this should all compile fine. We're still missing a concrete implementation of this interface, so let's declare one: [Guid("6156C6FC-4DD9-4f82-8200-0446DABB7F35"), ComVisible(true)]
public class DateParser : IShellExecuteHook {
}
Here, we've said that public int Execute(SHELLEXECUTEINFO sei) {
try {
DateTime oTime=DateTime.Parse(sei.lpFile + " " + sei.lpParameters);
MessageBox.Show(null, "Date '" + sei.lpFile + " " +
sei.lpParameters + "' in ISO 8601 is " +
oTime.ToString("s"), "ISO 8601 Date",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return S_OK;
}
catch(FormatException) {
return S_FALSE;
} catch(Exception e) {
// Unknown exception. Report it to stderr
Console.Error.WriteLine("Unknown exception parsing Date: " + e.ToString());
}
return S_FALSE;
}
The shell treats the first space-delimited word in the command line as the filename and everything after the first space as parameters. We want to parse the whole command line, so we concatenate the filename and parameters together. RegistrationNow that we have our COM implementation, it needs to be registered. COM components have always needed registering, so this should come as no surprise; but this is .NET, so our components need to be registered in two places. The way it works is that your COM class is registered in the usual place, under
The progid key is in the following format: <namespace>.<class>. This is obviously a problem, because nowhere is there a file path to your assembly on disk. To allow the CLR to find your code, it needs to be registered. This registration location is called the Global Assembly Cache, or GAC. It's a location where all shared assemblies must be placed, and it resides in a series of folders underneath %windows%\assembly. You can inspect this through Windows Explorer and you'll see a nice, friendly list of all the assemblies placed into the GAC. Under the surface (through DOS), you can see how they are really stored. The GAC has the codename fusion during development and this name appears in several places, such as DLL names. Registry RegistrationInstalling to the registry is pretty easy: Assembly asm=Assembly.GetExecutingAssembly();
RegistrationServices reg=new RegistrationServices();
reg.RegisterAssembly(asm, 0);
This instructs the CLR to add the appropriate entries into [System.Runtime.InteropServices.ComRegisterFunctionAttribute()]
static void RegisterServer(String zRegKey) {
try {
RegistryKey root;
RegistryKey rk;
root = Registry.LocalMachine;
rk = root.OpenSubKey(@"Software\Microsoft\Windows\"
"CurrentVersion\Explorer\ShellExecuteHooks", true);
rk.SetValue(clsid, ".Net ISO 8601 Date Parser Shell Extension");
rk.Close();
}
catch(Exception e) {
System.Console.Error.WriteLine(e.ToString());
}
}
In addition to the standard GAC InstallInstalling to the GAC can be done in several ways. The easiest is to simply drag and drop into the %windows%\assembly folder. Another way is to use the GACUtil.exe tool provided with the .NET SDK. For an end user, neither of these methods are particularly intuitive, so we elect to install it programmatically. Included with a couple of the samples in the SDK is a file called fusioninstall.cs. As the name implies, this provides a couple of functions which use PInvoke to install an assembly in the GAC. Doing this is as simple as: if (FusionInstall.AddAssemblyToCache("DateParser.exe") == 0) {
Console.WriteLine("DateParser - shell extension successfully registered");
}
Strong NamingThere is one final point I've missed from GAC installation. To prevent name clashes, the GAC requires each assembly to have a strong name. That is, a name constructed from a public key and assembly version attributes. The current VB.NET IDE provides a nice property page to add a strong name, whereas with C#, it has to be done manually. First of all, a public/private key pair is generated using sn.exe -k, the strong name utility in the SDK. Then, this key pair should be referenced in code using the key attribute: [assembly: AssemblyKeyFile(@"..\..\KeyFile.snk")] Finally, registering the built executable is simply a case of double-clicking the EXE. This will register the component, and Windows Explorer will load it into its process space and issue every
ConclusionIt's amazing just how short the finished code is. Probably shorter than the equivalent ATL C++ code. It shows that C# and .NET have very good COM interop and legacy integration story and could well become the preferred means of shell programming, taking over from C++.
|
||||||||||||||||||||||