Silverlight 3 – How to access peripherals

The previous post on Silverlight 3 for kiosk apps outlined some architecture options how you can build Silverlight applications with access to peripherals. This follow up post goes into more detail on the implementation.

I strongly encourage you to consider WPF, ClickOnce and POS for .NET as the foundation of your application before you go ahead building these types of applications. The major benefits of Silverlight are the small run-time and the cross-platform availability. Chances are that cross-platform is not necessary in a kiosk environment. The small runtime may not be a big advantage if you are deploying a local application, if you’re deploying, you may as well deploy the full .NET runtime. In fact, the lean runtime may be a disadvantage since it’s lacking functionality available in the .NET libraries. ClickOnce may provide similar benefits as a web deployment of Silverlight.

Now … if you’re still reading, you probably determined that Silverlight is the way to go.

The solution is based on several key features in Silverlight

  • Hosting Silverlight in a custom container via the hosting API – introduced with Silverlight 1
  • Scripting Silverlight applications – introduced with Silverlight 1
  • Local messaging between Silverlight applications – even across different containers, first introduced with Silverlight 3

Let’s take a look how we can string these together to build a solution that maintains the integrity and protection of the Silverlight sandbox, but also let us get to local computing resources and maintains the benefits of Silverlight development and deployment.

First, you can host Silverlight applications not only in a web browser, but also in a custom application container via the Hosting COM API. You’re in for a blast from the past since some experience with C++ and COM is definitely required to get this to work. There’s a sample for Silverlight Alternative Hosting on MSDN. You save yourself a lot of time and effort compared to implementing the various COM interfaces if you just download the sample and start from there.

Custom Silverlight Hosting

First, you decide how you’re going to load the Silverlight application in the custom container. You could load a .xap from the local disk, or you could local the .xap application over the web. Loading from a URL over the web preserves some of the deployment flexibilities of a browser app, but if you’re running an application without a network connection, loading the application from a http URL may not be an option.

Loading a XAP from from a URL, either a file:/// URL or an http:// URL will work. To load the Silverlight application, you pass the URL to the application as a named Source property in the PropertyBag during control activation (I didn’t think I would ever have to write about ActiveX control activation again). The TutorialXcpHost application from the MSDN sample makes this very easy. It includes a XcpControlHost helper class with the SetSource method. You call SetSource before activating the control:

int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR lpCmdLine, int nShowCmd) 
{  
   CXcpControlHost::SetSource(L"http://localhost/AppsComms/ClientBin/SenderApp.xap");
   return _AtlModule.WinMain(nShowCmd);
}

Even when running in a custom host, Silverlight is still validating zones and enforces some cross-domain security. If you’re loading additional application data or resources, then base URL and zone (file:/// or http://) need to match. You can play some trickery by implementing the container’s IXcpControlHost::GetBaseUrl() method to return a matching zone, but it’s safer to play by the rules and comply with Silverlight’s security policies. after all, they’ve been put in place for good reasons.

So far, we can load a Silverlight application into a custom container, that provides more flexibility than a browser. For example, the custom container can communicate with other local resources or devices. Next we need to enable communication between the container and the Silverlight application.

Defining an interface from Silverlight to the Custom Container

A Silverlight application can expose an interface to scripting engines to allow integration with the host. For example the javascript engine in a browser can get and set properties of a Silverlight application and invoke methods exposed to script. The Silverlight piece of the code is very simple.

  1. You define a class that represents your scripting interface and mark the class with the [ScriptableType] attribute.
    [ScriptableType]
    
    public class MyScriptableObject
    
    {
    
    // ... 
    
    }
  2. In that class, you mark methods you want to expose to the scripting engine with the [ScriptableMember] attribute.
    [ScriptableMember]
    
    public void ShowMessage()
    
    {
    
    // ...
    
    }
    

    Note: Properties and Methods must be public, or discovering the Method with GetIDsofNames will return E_FAIL.

  1. You register an instance of the ScriptableType with the Silverlight runtime after the application starts up by calling RegisterScriptableObject. It’s very important to note that you have to call RegisterScriptableObject after runtime and application are intialized, not during the MainPage’s constructor or in InitializeComponent(). Scripting will not work correctly if you register the object too early.
    private void Application_Startup(object sender, StartupEventArgs e)
    
    {
    
        this.RootVisual = new MainPage();
    
        HtmlPage.RegisterScriptableObject("MyReceiver", new MyScriptableObject(this.RootVisual as UserControl));
    
    }

Now the Silverlight application exposes an interface for the container to call. Since this application is running in a custom container, the container can call into the application in response to an event from a card reader for example.

Calling the Silverlight interface

The HTML bridge is part of Silverlight to integrate with browser’s java script engines. That doesn’t mean you can’t make use of it from other containers though. We’re calling the scriptable objects from the C++ container. It’s a little bit cumbersome in our scenario since you have to deal with late bound objects through COM interfaces, but the sample code may help you out a little bit.

Let’s look at the important pieces. The host for the Silverlight application must allow for access to objects that the  Silverlight application registered with the scripting bridge. IXcpControlHost::GetHostOptions() gets called during activation of the control by the hosted Silverlight runtime to find out which features the host allows. The options returned to Silverlight control must include XcpHostOption_EnableScriptableObjectAccess;

STDMETHODIMP CXcpControlHost::GetHostOptions(DWORD* pdwOptions)
{
  *pdwOptions = XcpHostOption_EnableCrossDomainDownloads | XcpHostOption_EnableScriptableObjectAccess;
  return S_OK;
}

Without the option set, attempts  by the container to obtain a reference (the DISPID as we’ll see shortly) will fail. RegisterScriptableObject, however, succeeds.

Getting to the registered object requires some understanding of COM. If you never had to deal with IUnknown and IDispatch, you may want to take a quick look at the COM reference.

The scriptable objects are a accessible from the Silverlight control’s Content Interface. Like all other late bound objects, properties and method on the Content object is available via IDispatch, which turns the inconspicuous lines of javascript:

slCtl  = sender.get_element();
slCtl.Content.MyReceiver.ShowMessage();

into the slightly more verbose C++ equivalent:

  _axWindow.QueryControl(IID_IXcpControl, (void**)&pxcpControl);
  HRESULT hr = pxcpControl->get_Content( &pContentDispatch );
  BSTR name = L"MyReceiver";
  hr = pContentDispatch->GetIDsOfNames(IID_NULL, &name, 1, LOCALE_SYSTEM_DEFAULT, &dispatchID);
  hr = pContentDispatch->Invoke(dispatchID, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYGET, &params, &varResult, NULL, NULL);
  pScriptableObjectDispatch = varResult.pdispVal;
  name = L"ShowMessage";
  hr = pScriptableObjectDispatch->GetIDsOfNames(IID_NULL, &name, 1, LOCALE_SYSTEM_DEFAULT, &dispatchID);
  hr = pScriptableObjectDispatch->Invoke(dispatchID, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &params, &varResult, NULL, NULL);

Let’s examine those lines a little bit more closely. First you get a reference to the Silverlight control. From the control you go to the control’s content. There’s an implicit assumption here that you don’t access the Content until after the VisualTree is constructed.

From the content, you get the ScriptableObject by the name passed to RegisterScriptableObject in the Silverlight application. Then finally, very important(!), you ask the ScriptableObject for its default dispatch interface, from where you get method or property references, which you can invoke via IDispatch::Invoke.

Now you have everything in place to listen to events from peripherals and pass them on to a Silverlight application running inside the custom host.

Communication between Applications

If you need run the Silverlight application running in a browser, then you can build a bridge from the custom host that communicates with the browser application with the silverlight application communication feature introduced with Silverlight 3.

Two applications can communicate with the LocalMessageSender and LocalMessageReceiver objects. If you need to forward local events to a browser application, then the browser application would start listening to messages.

Setting up Sender and Receiver is straight forward. The receiving applications starts the listener:

LocalMessageReceiver receiver = new LocalMessageReceiver(     
    "Receiver Name",    
    ReceiverNameScope.Global, LocalMessageReceiver.AnyDomain  );
receiver.MessageReceived += ( object sender, MessageReceivedEventArgs e ) =>
{
        items.Add( new ListItem() { Text= e.Message + " " + DateTime.Now.ToLongTimeString() } );
};
receiver.Listen();

and the sending application sends to a receiver with the registered name:

LocalMessageSender msgsender = new LocalMessageSender(    
    "Receiver Name",     
    System.Windows.Messaging.LocalMessageSender.Global );
msgsender.SendCompleted += ( object sender2, SendCompletedEventArgs e2 ) =>
{
    MessageBox.Show("Result: " );
};
msgsender.SendAsync(textBox1.Text);

MSDN has a good overview of local messaging in Silverlight, including some advanced features like sending complex XML messages and troubleshooting.

What’s important to note, is that the BaseUrl of the sending application has to match the BaseUrl of the receiving application. Even setting the receiver options and disabling zone checks are not sufficient for the receiver to process incoming messages. You may have to play some more tricks in the host if you’re not planning on loading the sending application from the same site.

For example, you could intercept the download request in IXcpControlHost::DownloadUrl() to load the .xap from other locations.

Closing Words

The upcoming Silverlight 4 release is going to simplify the architecture for out-of-browser scenarios. Silverlight 4 trusted applications can communicate with COM servers directly, which eliminates the need for hosting a Silverlight application in a custom container to get access to local resources. You still need the custom container bridge if your browser application needs access to devices and other local resources.

I’m planning a follow up post discussing the Silverlight 4 option. It’s going to be a little bit before SL4 ships – Scott Guthrie mentioned H1 2010 as the target timeframe during his PDC Day 2 keynote. It’s good to have a working option with Silverlight 3 before that.

And finally, the big thank you to Ashish, who patiently answered my questions about the COM APIs while I was looking for the magic combination ;)

13 Comments

Comments have been disabled for this content.