|
|||||||||||||||||||||||||
|
|||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
ContentsIntroductionSince 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 ActiveXIt's not fair to say that gadget's 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 InterfaceThe 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 AdapterNow 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 private ArrayList paramList = new ArrayList();
public void AddConstructorParam(object parameter)
{
paramList.Add(parameter);
}
The next method is where all the magic happens. The 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 Finally, because we're in the COM world, we have to be careful to do our own object disposal. The 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 Automatic Registration at RuntimeNow 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 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\\,file://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 CodeReading 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 [ComVisible(true)]
public class GmailClient : IDisposable
{
...
}
Byfar the most important step is to add the Loading an AssemblyAt this point lets 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 function Initialize()
{
if(InteropRegistered() == false)
{
RegisterGadgetInterop();
}
_builder = GetActiveXObject();
}
Putting it all TogetherThe nextstep is to load the 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.GetUnreadMail();
var count = gmailClient.UnreadMailCount;
var mailLink = document.getElementById('mailCountLink');
mailLink.innerText = count;
Notice that except for the Even though we're working with an inferred type,
Lastly, because the
SummaryThat'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 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! | ||||||||||||||||||||||||