Introduction
Silverlight 4 gives access to the user's microphone and camera, and adds printing capabilities, but as far as hardware goes, that's it. Fortunately Silverlight 4 also provides access in Elevated Trust Out-Of-Browser applications to COM. If you need to access other local hardware, providing a COM component is the solution. This example provides a COM object written in C# and addressed by a Silverlight application.
Background
My company provides vertical market software and normally supplies peripherals as well. Typical devices include things like cash drawers and card swipe readers. These are usually serial devices. Customers have also been asking for web based solutions to avoid the usual distribution and update issues of locally installed software. Silverlight now provides a great user experience but hardware access is essential.
I have no experience writing COM objects (and not too much using them), so this was a voyage of discovery for me. I had to piece together a number of different articles and examples to get something to work. I hope you can benefit by having a complete example.
Example Problem
For my example, I'm simulating a three drawer cash drawer stack. The application needs three basic functions:
- Open a drawer by drawer number.
- Check the status of a particular drawer.
- Receive an event if a drawer is opened or closed.
The example solution contains a Windows class library which simulates the device class and exposes the necessary methods and events to COM. Also in the solution is a very simple Silverlight test application.
The COM object targets the 2.0 Framework. The Silverlight application that consumes it is a Silverlight 4 project.
Creating the COM Component
To create a class exposed to COM, create an interface for the public
class members except events, and another for the events. These must be segregated into separate interfaces.
Here's the interface for the methods:
using System;
using System.Runtime.InteropServices;
namespace ComExampleLib
{
[Guid("65152CDB-8530-4363-8036-E7F732E9E6F6")]
public interface IComExample
{
void OpenDrawer(int drawerNumber);
bool IsDrawerOpen(int drawerNumber);
void CloseAllOpenDrawers();
}
}
A couple of things to note:
- Decorate the interface with a guid. The guid attribute is in
System.Runtime.InteropServices
.
- The interface must be marked
public
.
- The parameters are simple types.
Here's the interface for the event:
using System;
using System.Runtime.InteropServices;
namespace ComExampleLib
{
[Guid("AF8B17FB-9A65-464E-AD19-E3849B97C2AA"),
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IComExampleEvents
{
void DrawerStateChange(int drawerNumber, bool isOpen);
}
}
Things to note:
- The interface must be
public
.
- The interface is decorated with the guid attribute and it's a different guid than is used for the other interface.
- The interface is also decorated with the interop interface type
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)
. This is essential, and IDispatch
is the type you should use.
- The biggest thing to note is that although this is the "event" interface, we're declaring a simple method that returns a
void
and not a C# event
or delegate
. Also important is that the parameters are simple types. The usual pattern using a complex type derived from EventArgs
doesn't work.
I've declared a delegate for the event in its own code file. The method in the event interface and this delegate will both be used in the actual class. Note that the signatures match but the names do not. In the actual class I'm exposing, I'll declare an event using the delegate. That event's name must match the name declared in the event interface shown above.
using System;
namespace ComExampleLib
{
public delegate void DrawerChange(int drawerNumber, bool isOpen);
}
Finally, here's the class:
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace ComExampleLib
{
[Guid("03EFC7CF-A57B-4F66-ABC9-FFBE13F1E927"),
ProgId("ComExample.Application"),
ClassInterface(ClassInterfaceType.None),
ComSourceInterfaces(typeof(IComExampleEvents))
]
public class ComExample : IComExample
{
private Dictionary<int, bool> m_DrawerList = new Dictionary<int, bool>();
public ComExample() { }
#region IComExample Members
public void OpenDrawer(int drawerNumber)
{
if (m_DrawerList.ContainsKey(drawerNumber))
{
m_DrawerList[drawerNumber] = true;
}
else
{
m_DrawerList.Add(drawerNumber, true);
}
OnDrawerStateChange(drawerNumber, true);
}
public bool IsDrawerOpen(int drawerNumber)
{
return m_DrawerList.ContainsKey(drawerNumber) && m_DrawerList[drawerNumber];
}
public void CloseAllOpenDrawers()
{
List<int> toClose = new List<int>();
foreach (KeyValuePair<int, bool> drawer in m_DrawerList)
{
if (drawer.Value)
{
toClose.Add(drawer.Key);
}
}
foreach (int n in toClose)
{
m_DrawerList[n] = false;
OnDrawerStateChange(n, false);
}
}
#endregion
#region Events
public event DrawerChange DrawerStateChange;
public void OnDrawerStateChange(int drawerNumber, bool isOpen)
{
if (DrawerStateChange != null)
{
DrawerStateChange(drawerNumber, isOpen);
}
}
#endregion
}
}
Important notes on the class:
- The class is
public
.
- The class is decorated with a guid attribute, and the guid is different from the ones on the two interfaces.
- the
ProgId("ComExample.Application")
attribute defines how the class will be referred to in the Silverlight application. You can provide any string
as long as it's unique. You can also omit this attribute, in which case you'll refer to the class in the form Assembly.Class
.
- The attribute
ClassInterface(ClassInterfaceType.None)
turns off an automatic generation of an interface. We're providing an explicit interface instead.
- The attribute
ComSourceInterfaces(typeof(IComExampleEvents)
declares the events the class raises. Note that the event interface isn't listed as implemented in the class definition. Strange perhaps, but correct.
- Note that the class does specifically implement the methods interface.
- Finally, note that the class declares a
public
event (using the delegate we defined earlier) with the exact same name as the event in the event interface. The event in the class and the method name in the event interface must match.
Visual Studio Settings
There are a couple of other things you need to do to get this to work. Open the project's properties and choose the Application tab. Click the Assembly Information button and check the "Make Assembly COM-visible" checkbox.
As an alternative to making the entire assembly COM visible, you can mark the pieces of the assembly you want to expose with the ComVisible(true)
attribute.
Finally, in order to run the solution without manually registering the COM object, go to the Build tab and check the "Register for COM interop" checkbox. When Visual Studio runs the project, it will register the object for you.
To install the COM object on another machine, use the RegAsm program from the .NET Framework:
C:\Windows\Microsoft.NET\Framework\v2.0.50727\regasm ComExampleLib.dll /tlb
The Silverlight Project
The Silverlight application is relatively straightforward due to the new Silverlight 4 features.
Note that I've added two references to the Silverlight application project: Microsoft.CSharp
and System.Core
.
I've declare two module level variables in the code-behind for the main page:
dynamic m_ComDrawerContoller;
AutomationEvent m_DrawerChangeEventHandler;
These aren't populated at start-up because we need to check to make sure we're running under the right conditions.
if (Application.Current.IsRunningOutOfBrowser &&
Application.Current.HasElevatedPermissions)
{
RegisterComObject();
}
If the conditions are right, we can then initialize the COM objects:
private void RegisterComObject()
{
try
{
m_ComDrawerContoller = AutomationFactory.CreateObject("ComExample.Application");
m_DrawerChangeEventHandler = AutomationFactory.GetEvent
(m_ComDrawerContoller, "DrawerStateChange");
m_DrawerChangeEventHandler.EventRaised +=
new EventHandler(HandleDrawerStateChange);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
A lamda expression could also be used for the EventRaised
handler.
When the event is raised, we can then handle it. The AutomationEventArgs
class defines an Arguments
array of objects. These are the parameters passed to our event method, and appear in the order declared.
private void HandleDrawerStateChange(object sender, AutomationEventArgs e)
{
switch ((int)e.Arguments[0])
{
case 1:
Drawer1Status.Text = (bool)e.Arguments[1] ? "Open" : "Closed";
break;
case 2:
Drawer2Status.Text = (bool)e.Arguments[1] ? "Open" : "Closed";
break;
case 3:
Drawer3Status.Text = (bool)e.Arguments[1] ? "Open" : "Closed";
break;
}
}
Finally, the regular methods can be called in the regular way. Note that they aren't asynchronous.
private void btnReset_Click(object sender, RoutedEventArgs e)
{
m_ComDrawerContoller.CloseAllOpenDrawers();
}
private void check_click(object sender, System.Windows.RoutedEventArgs e)
{
int drawerNumberToCheck;
int.TryParse(txtDrawerToCheck.Text, out drawerNumberToCheck);
if (drawerNumberToCheck > 0)
{
bool isOpen = m_ComDrawerContoller.IsDrawerOpen(drawerNumberToCheck);
if (isOpen) lblCheckStatus.Text = "Open"; else lblCheckStatus.Text = "Closed";
}
}
Intellisense won't help you with the method names or parameters so you'll have to have good documentation on your COM object.
Getting the Silverlight Project Running
Note that the Silverlight project is set to run out of the browser and with elevated trust. These settings are on the Silverlight project's property page on the Silverlight tab. Click the Out Of Browser Settings button to set the elevated trust setting.
In order to run the project, first set the web application as the startup project. When the Silverlight application loads, right click and install it locally. Close the project and return to the Silverlight applications web host project, and change the debug properties to debug the Silverlight application (instead of the web application). Now when you set the startup project to the web application, it will debug the correct application.
When the program runs, you should be able to call the COM object's methods and receive its events.
Conclusion
Having to have the user install a COM object to get your Silverlight project to work obviously isn't a workable idea for a general purpose application for the public. However in a more controlled line-of-business or enterprise situation, it can provide the final bridge between what you need to do and what Silverlight can do.
History
- 25th May, 2010: Initial version