Contents
Introduction
Since the writing of my sidebar gadget introduction, I've been wrestling with the frustration of not being able to do with a gadget what can be easily done with .NET. I love how compact and simple gadgets can be, but I find it hard to build a truly useful gadget simply because there's no real power using JavaScript. Unfortunately for the .NET community, gadgets rely almost purely on JavaScript. It's hard to mix my excitement for gadgets with their huge limitations.
If Only I Could Run .NET Code from Gadgets…
Enter Gadget .NET Interop!
In this article, we'll explore how to build an interop layer between gadgets and .NET so you can run any .NET code from your sidebar gadget. We'll do that by building a C# project to read your GMail inbox.
COM and ActiveX
It's not fair to say that gadgets can't run .NET code. The truth is that it's very easy to create COM object instances from scripting languages. The real problem is that it's terribly inconvenient to have to register all your code for COM interop. Doing so would require you to first modify all your code to be COM compatible. Then you'd have to re-package your code and distribute an MSI file along with each and every gadget just to install and register your assembly (and possibly add it to the GAC if it's going to be shared across gadgets). That kind of workaround isn't a realistic solution, especially if you already have code that you don't want to rewrite and package just for COM interop. Further, you can't assume your users have the knowledge or permissions to install a COM component.
What's the answer then?
No matter what, there must be some COM pieces in place; otherwise we'll never get past the limitations of JavaScript. We'll get to the GMail part once we have a suitable COM layer (see below if you're comfortable with COM in .NET). Let's start with the real nuts and bolts of the solution by creating a basic COM object that can be used load any .NET assembly. See this article for more details on how .NET COM objects.
.NET COM Interface
The idea is simple; create a small, lightweight .NET COM component that uses reflection to load any assembly and type. Then, that type can be called directly from JavaScript. Let's take a look at the interface for the "Gadget Adapter" that will do the bulk of the work.
[ComVisible(true),
GuidAttribute("618ACBAF-B4BC-4165-8689-A0B7D7115B05"),
InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IGadgetInterop
{
object LoadType(string assemblyFullPath, string className);
object LoadTypeWithParams(string assemblyFullPath, string className,
bool preserveParams);
void AddConstructorParam(object parameter);
void UnloadType(object typeToUnload);
}
There are only four methods that the implementing Gadget Adapter class will need to handle. The point to take note of is that the interface has three attributes that will allow us to expose the implementing class as a COM object. Four methods are all we need to create and call any type in managed code.
Gadget Adapter
Now that the interface is defined, let's look at the actual Gadget Adapter implementation of this interface. We'll break it out piece-by-piece starting with the class attributes.
[ComVisible(true),
GuidAttribute("89BB4535-5AE9-43a0-89C5-19B4697E5C5E"),
ProgId("GadgetInterop.GadgetAdapter"),
ClassInterface(ClassInterfaceType.None)]
public class GadgetAdapter : IGadgetInterop
{
...
}
There are a few differences between these attributes and the attributes on the interface. The most important attribute for our purposes is the "ProgId
" attribute. This attribute represents the string
we'll use to create the ActiveX object via JavaScript. Now that the GadgetAdapter
is decorated properly, the next step is loading assemblies and creating class instances. The AddConstructorParam
method allows JavaScript code to add values that will be passed to the class constructor's arguments. This is only necessary when want to load a .NET type using a constructor with one or more arguments.
private ArrayList paramList = new ArrayList();
public void AddConstructorParam(object parameter)
{
paramList.Add(parameter);
}
The next method is where all the magic happens. The LoadTypeWithParams
method has the three arguments that allow any .NET assembly to be loaded. The method takes the path to the assembly, the type to create, and a flat for handling constructor parameter disposal.
public object LoadTypeWithParams(string assemblyFullPath, string className,
bool preserveParams)
{
...
Assembly assembly = Assembly.LoadFile(assemblyFullPath);
object[] arguments = null;
if (paramList != null && paramList.Count > 0)
{
arguments = new object[paramList.Count];
paramList.CopyTo(arguments);
}
BindingFlags bindings = BindingFlags.CreateInstance |
BindingFlags.Instance |
BindingFlags.Public;
object loadedType = assembly.CreateInstance(className, false, bindings,
null, arguments, CultureInfo.InvariantCulture,
null);
...
return loadedType;
}
Using standard .NET reflection, the specified assembly is loaded and an instance of the input type is created. That instance is returned and is then directly callable by JavaScript (more on that to come). The preserveParams
flag prevents the constructor arguments from being cleared after the object is created. This is only necessary when you're creating multiple instances of a class with the same constructor arguments.
Finally, because we're in the COM world, we have to be careful to do our own object disposal. The UnloadType
method calls dispose of the incoming object to allow for graceful cleanup.
public void UnloadType(object typeToUnload)
{
...
if (typeToUnload != null && typeToUnload is IDisposable)
{
(typeToUnload as IDisposable).Dispose();
typeToUnload = null;
}
catch { }
...
}
The one convention I opted for is that classes exposed to gadgets must implement IDisposable
, so only types implementing that interface will work with the sample code. That's all there is to the interop layer. It creates .NET objects and it destroys .NET objects; nothing more, nothing less.
Automatic Registration at Runtime
Now we have a working COM-friendly Gadget Adapter, but how does it get registered? Normally you would rely on an MSI installer to register and GAC your COM components. Remember that the goal here is to run .NET code in a gadget without the user having to install an MSI. To get around the MIS (or RegAsm.exe) we can "fake" the registration by adding the right values directly to the registry (My thanks to Frederic Queudret for this idea). The GadgetInterop.js, a JavaScript library, is designed to facilitate the Gadget Adapter registration (as well as all the COM object wrapping). The RegAsmInstall
JavaScript method takes all the information about the Gadget Adapter interop assembly and creates all the necessary registry entries to register it. The beauty of this step is that any gadget can register the interop layer at runtime the first time the gadget executes.
function RegAsmInstall(root, progId, cls, clsid, assembly, version, codebase)
{
var wshShell;
wshShell = new ActiveXObject("WScript.Shell");
wshShell.RegWrite(root + "\\Software\\Classes\\", progId);
wshShell.RegWrite(root + "\\Software\\Classes\\" + progId + "\\", cls);
wshShell.RegWrite(root + "\\Software\\Classes\\" + progId + \\CLSID\\,
clsid);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\", cls);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\", "mscoree.dll");
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\ThreadingModel", "Both");
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\Class", cls);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\Assembly", assembly);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\RuntimeVersion", "v2.0.50727");
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\CodeBase", codebase);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\" + version + "\\Class", cls);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\" + version + "\\Assembly", assembly);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\" + version + \\RuntimeVersion,
"v2.0.50727");
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\" + version + "\\CodeBase", codebase);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\ProgId\\", progId);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
\\Implemented Categories\\{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}\\,
"");
}
Nothing too complex, just a few basic registry entries. The rest of the JavaScript library serves as a wrapper for the Gadget Adapter's methods. You can use the GadgetBuilder
's methods from your own script to load or unload any .NET type.
Now that we have the interop layer in place, let's take a look at the GMail example (included in the source code download) to see how the interop layer actually gets used.
GMail Reader - Running .NET Code
Reading a GMail account inbox is as easy as reading an XML feed. The feed is so simple that it actually could be parsed using JavaScript. I purposely added some complexity to the GmailReader
assembly to have something in code managed that I couldn't do in JavaScript. Plus, using .NET code adds speed and the ability to debug (That's far more than you can ask of JavaScript). To that end, the response XML from the GMail feed is run through an XslCompiledTransform
to deserialize the response into a generic list (i.e., strongly typed) of a type I created. The types in that generic list can then be exposed directly to JavaScript. I won't go into GMail code here as it's easily understandable and well commented. What's really important is understanding what was done to make the code JavaScript-friendly, and how to call that code from JavaScript. There are a few key steps that are required.
[ComVisible(true)]
public class GmailClient : IDisposable
{
...
}
By far, the most important step is to add the [ComVisible(true)]
attribute to any class that will be used by your JavaScript. The class will simply not be callable by JavaScript without this attribute. The other convention is implementing the IDisposable
interface. This is really just preventative maintenance and good practice since our object is exposed to COM.
Loading an Assembly
At this point, let's examine the Gmail.js file and take a look at how a .NET assembly is used from the gadget. The first thing to do is create and initialize an instance of the GadgetBuilder
wrapper found in the GadgetInterop.js file. We'll use that wrapper to load and unload .NET types.
var builder = new GadgetBuilder();
builder.Initialize();
Calling the Initialize
method does a few important tasks. First, it checks if the Gadget Adapter is already registered by trying to create an ActiveX object instance of it. If that fails, the builder attempts to run the registration code listed above. The beauty here is that you never need to manually register the Gadget Adapter COM object. The JavaScript library will do it for you the first time it's run.
function Initialize()
{
if(InteropRegistered() == false)
{
RegisterGadgetInterop();
}
_builder = GetActiveXObject();
}
Putting It All Together
The next step is to load the GmailReader
assembly and create an instance of the client type. The GmailClient
has two constructor arguments, userName
and password
which are required to create an instance. The values for both arguments come from the gadget's settings page, and are stored using the Gadget
API, so once they exist for the lifetime of the gadget.
builder.AddConstructorParam(userID);
builder.AddConstructorParam(password);
gmailClient = builder.LoadType(System.Gadget.path +
"\\bin\\GmailReader.dll", "GmailReader.GmailClient");
We're telling the builder to load the GmailReader.dll assembly located in the gadget's bin directory. There's no need to put your assembly in a "bin" directory, or even under the same folder structure as your gadget. I simply did that for convenience in this example.
At this point, the gmailClient
JavaScript variable holds a reference to a fully-loaded .NET GmailClient
type. Now we can directly invoke the objects
methods just like you would in managed code. To get enough information to display something meaningful on the gadget UI, we can call the following code:
gmailClient.GetUnreadMail();
var count = gmailClient.UnreadMailCount;
var mailLink = document.getElementById('mailCountLink');
mailLink.innerText = count;
Notice that except for the var
data type, it's no different that a .NET equivalent. In other words, you now have full access to any method or property you want to expose in your own object, and there's no further COM work to do. This is true for any assembly. With the Gadget Adapter in place, you don't have to do any COM work again.
Even though we're working with an inferred type, var
, once we get a value back from the .NET code, it can be used like any other JavaScript value. In this case, the number of unread mail items is displayed to the user, and the and the background is changed to reflect no mail or new mail.
Lastly, because the gmailClient
is kept in memory, the mail contents can be displayed any time the user clicks on the unread mail count link. In other words, the .NET object maintains it (hence the need for manual cleanup). Here's how the details are displayed in the gadget.
Summary
That's really all there is to it. Once your object is created, you can use it as if you were calling it from managed .NET code. Also, because the interop layer is registered after the first time you run your gadget, it's reusable across all your gadgets. The best part is that you can package your assembly, the interop assembly, and the interop JavaScript library with your gadget, and Vista will handle the entire install process just like any other gadget.
It's unfortunate that Microsoft left managed code out of the gadget framework, especially when they have support for it in so many other areas. Still, the truth is that managed code can still be easily used, so there's still hope for some really useful gadget development.
Enjoy!