Click here to Skip to main content
15,860,861 members
Articles / Web Development / HTML

.NET Interop for Gadgets – A C# GMail Inbox Reader Example

Rate me:
Please Sign up or sign in to vote.
4.84/5 (108 votes)
30 Jan 2007CPOL9 min read 651.6K   61.4K   259   180
How to call absolutely any .NET code from your Vista Sidebar Gadget

Sample Image - GadgetInterop.png

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.

C#
[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.

C#
[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.

C#
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.

C#
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.

C#
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.

JavaScript
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.

C#
[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.

JavaScript
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.

JavaScript
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.

Gadget Settings

C#
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:

C#
gmailClient.GetUnreadMail();
var count = gmailClient.UnreadMailCount;
var mailLink = document.getElementById('mailCountLink');
mailLink.innerText = count;

New Mail

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.

No 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.

Mail Details

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!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer PageLabs
United States United States
I'm the founder of PageLabs, a web-based performance and SEO optimization site.

Give your site a boost in performance, even take a free speed test!

http://www.pagelabs.com

Comments and Discussions

 
GeneralRe: brilliant but not working... 'Automation server can't create object' Pin
Filip Bastian Stanek14-Apr-08 9:17
Filip Bastian Stanek14-Apr-08 9:17 
GeneralExcellent Pin
mr.stick5-Dec-07 5:33
mr.stick5-Dec-07 5:33 
QuestionNotification for Label Pin
qur22-Nov-07 23:33
qur22-Nov-07 23:33 
GeneralIt's really Cool! Pin
ericwmy17-Oct-07 1:58
ericwmy17-Oct-07 1:58 
GeneralRe: It's really Cool! Pin
TylerBrinks17-Oct-07 5:03
TylerBrinks17-Oct-07 5:03 
Questionsome Vista user cannot use ???? Pin
Alpha Liu8-Oct-07 17:42
Alpha Liu8-Oct-07 17:42 
AnswerRe: some Vista user cannot use ???? Pin
TylerBrinks17-Oct-07 5:03
TylerBrinks17-Oct-07 5:03 
Generaluse it for non-commercial project Pin
zeroflag2-Oct-07 8:46
zeroflag2-Oct-07 8:46 
GeneralRe: use it for non-commercial project Pin
TylerBrinks2-Oct-07 11:18
TylerBrinks2-Oct-07 11:18 
GeneralRe: use it for non-commercial project Pin
zeroflag2-Oct-07 12:25
zeroflag2-Oct-07 12:25 
QuestionGoogle Apps? Pin
Alexander Kojevnikov9-Sep-07 23:41
Alexander Kojevnikov9-Sep-07 23:41 
AnswerRe: Google Apps? Pin
TylerBrinks30-Oct-07 5:23
TylerBrinks30-Oct-07 5:23 
GeneralProblems creating ActiveXObject Pin
rodrigo8826-Aug-07 17:36
rodrigo8826-Aug-07 17:36 
GeneralRe: Problems creating ActiveXObject Pin
TylerBrinks26-Aug-07 17:51
TylerBrinks26-Aug-07 17:51 
GeneralRe: Problems creating ActiveXObject Pin
rodrigo8827-Aug-07 4:57
rodrigo8827-Aug-07 4:57 
GeneralRe: Problems creating ActiveXObject Pin
TylerBrinks29-Aug-07 19:44
TylerBrinks29-Aug-07 19:44 
GeneralRe: Problems creating ActiveXObject [modified] Pin
eduncan911.com5-Oct-07 9:10
eduncan911.com5-Oct-07 9:10 
Generalcannot invok my own dll?? which made it wrong??// Pin
Alpha Liu22-Aug-07 18:25
Alpha Liu22-Aug-07 18:25 
GeneralRe: cannot invok my own dll?? which made it wrong??// Pin
TylerBrinks23-Aug-07 6:01
TylerBrinks23-Aug-07 6:01 
Generalreuse Pin
wasaweso17-Aug-07 3:58
wasaweso17-Aug-07 3:58 
GeneralRe: reuse Pin
TylerBrinks17-Aug-07 4:24
TylerBrinks17-Aug-07 4:24 
GeneralRe: reuse Pin
wasaweso18-Aug-07 5:58
wasaweso18-Aug-07 5:58 
GeneralPlease contact me. Thanks! Pin
weimenglee14-Aug-07 19:29
weimenglee14-Aug-07 19:29 
QuestionProblm with LoadType Pin
Rajni P13-Aug-07 3:23
Rajni P13-Aug-07 3:23 
AnswerRe: Problm with LoadType Pin
TylerBrinks15-Aug-07 2:51
TylerBrinks15-Aug-07 2:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.