Click here to Skip to main content
Click here to Skip to main content

Accessing Hardware in Silverlight using COM

By , 26 May 2010
Rate this:
Please Sign up or sign in to vote.

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.

Sample App

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.

assembly info

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.

Register Object

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<automationeventargs />(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.

out of browser settings

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.

debug

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

License

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

About the Author

esaulsberry
Web Developer
United States United States
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 PinmemberMember 395959328-Jun-10 14:28 

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

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

| Advertise | Privacy | Mobile
Web04 | 2.8.140415.2 | Last Updated 26 May 2010
Article Copyright 2010 by esaulsberry
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid