Silverlight Visual Plugins

Silverlight is Microsoft's application framework for creating web applications. It provides a retained mode graphics system similar to WPF with an ability to use multimedia, graphics and animations.

Starting from version 4, Hydra supports Silverlight plugins and allows you to easily embed them into your host applications.

In this article we will describe how to create a new Silverlight visual plugin and talk about what features it provides and how they can be used.

Getting Started

Hydra for Silverlight brings support for Silverlight visual plugins for all available hosting platforms - VCL, WinForms and FireMonkey.

Hydra supports Silverlight plugins built with SL 4 and 5. However, you can still use SL 3 plugins, but with a limited set of features, for example you won't be able to communicate with the host, because SL 3 doesn't support AutomationFactory.

First, let's create a new Silverlight plugin.

Creating a Plugin

Creating a new Silverlight plugin is pretty much like creating a regular Silverlight application.

In File -> New -> Project select the Silverlight group and then the Silverlight Application template:

In the appeared dialog, uncheck Host the Silverlight application in a Web site and select a version of Silverlight that you would like to use:

This is it, a very basic Silverlight application is created and can already be used by the Hydra host.

In order to use extended abilities of the OOB application like access to System.IO, using AutomationFactory or performing p/invoke (in Silverlight 5), we will need some more settings.

In Project -> Properties go to the Silverlight page and set Enable running application out of the browser:

Click on the Out-of-Browser Settings button and check Require elevated trust when running outside the browser:

Now our plugin is fully ready, and we need to talk about the limitations of Silverlight plugins.

Limitations

Originally Silverlight was designed to run inside a browser sandbox and use a subset of the .NET Framework and thus is subject to some limitations. For example, it gives you restricted access to the file system, limited cross-domain downloads, doesn't allow to work with the registry, etc.

In version 3, Silverlight introduces support for Out-of-Browser (OOB) applications, which reduce sandbox limitations and provide a specific set of options that isn't available in browser applications.

Hydra uses the Microsoft Hosting API that allows to host Silverlight plugins in native windows. This API places our hosts somewhere in the middle between regular and OOB applications. Because of this, Silverlight plugins can benefit from the reduced OOB limitations and the additional set of features.

Here is a list of known limitations for Hydra Silverlight plugins:

  • Unable to run plugins in Fullscreen mode.
  • Unable to work with the Window class.
  • Like in OOB, you won't be able to get access to the underlying browser, so you wont be able to read its settings or access DOM.
  • General restriction of the Microsoft API. Silverlight applications that are hosted by native Windows will always have IsRunningOutOfBrowser set to false. So all features that rely on this property will not work, for example you won't be able to use WebBrowser in your plugins.

Also, Silverlight projects do not allow to set an external exe like in regular .NET application. This complicates the debugging process, but there are two options available:

  • Debug as OOB application. In Project -> Properties go to the Debug page and set Out-of-Browser application:

This method has one disadvantage though, it doesn't allow you to test communication with the host. This where the second method comes into play:

  • Attach to process. You need to run your host application first, and then go to the Debug -> Attach to Process menu and select your host process:

This will allow you to debug host communication as well as the other parts of the project.

Hosting the Plugin

By now you will have a complete project that, without any additional work, can be loaded by all supported host platforms.

There is no major difference in the loading procedure between Silverlight and any other plugins, let's take a look at a small example:

VCL/FMX:

procedure TMainForm.FormCreate(Sender: TObject);
begin
  ModuleManager.LoadModule('SilverlightPlugin.xap');
  ModuleManager.CreateVisualPlugin('SilverlightPlugin', fInstance, Panel1);
end;

WinForms:

private void MainForm_Load(object sender, System.EventArgs e)
{
  moduleManager.LoadModule("SilverlightPlugin.xap");
  fInstance = moduleManager.CreateInstance("SilverlightPlugin");
  hostPanel1.HostPlugin(fInstance as IBasePlugin);
}

Now let's review the example step by step:

  • ModuleManager.LoadModule('SilverlightPlugin.xap'); - Loads a file that holds our plugin. The LoadModule method automatically detects the type of the plugin and calls the appropriate method, however, you can directly tell the module manager to load a Silverlight plugin by calling LoadSilverlightModule.
  • ModuleManager.CreateVisualPlugin('SilverlightPlugin', fInstance, Panel1); - Creates an instance of the plugin and assigns a reference to this instance to the fInstance property and shows it in a plugin container.
  • fInstance = moduleManager.CreateInstance("SilverlightPlugin"); - Same for the .NET side, except you need to show the plugin content in the host panel manually.
  • hostPanel1.HostPlugin(fInstance as IBasePlugin); - Shows the plugin content in the host panel.
  • ModuleManager.ReleaseInstance(fInstance); - We must release the instance of the plugin before the module is unloaded.

The name of a Silverlight plugin instance will always be the same as the name of the xap file.

This is it, with just few lines of code we are able to load and show our plugin. So now we have one last topic to discuss - communication between host and plugin.

Communication with a Plugin

Starting from Silverlight 4, Microsoft introduces the ability for Silverlight applications to access automation objects like Office or scripting by using the AutomationFactory.

Hydra allows you to hook the calls from AutomationFactory to provide a custom object that can be used from the Silverlight side.

Because Silverlight is running in the sandbox, communication in the Silverlight plugins is really different than communication with other types of plugins.

The first difference is that host cannot acquire a reference to the plugin instance, the only reference it gets is to the IXcpControl interface that is used internally to control Silverlight object behavior and which cannot be used to communicate with the plugin. So only the plugin itself can initiate communication with the host, but not vice versa.

The second difference is that Silverlight supports only late binding, which means that unlike in other supported platforms, you cannot have a predefined interface and all work must be done with help of the dynamic type.

Let's discuss in detail how communication can be achieved.

Delphi Host

Silverlight can use only based automation objects, so in Hydra 4 we are introducing a new THYDispatchWrapper class that you can use as a base class for your custom objects. This class implements IDispatch and allows Silverlight to call its public methods and access its published properties.

Both VCL and FireMonkey use the same methods to communicate with the plugin.

Let's take a look at this small example:

TCustomObject = class (THYDispatchWrapper)
private
  function GetIntProperty: Integer;
  function GetStringProperty: WideString;
  procedure SetIntProperty(const Value: Integer);
  procedure SetStringProperty(const Value: WideString);
public
  procedure SendMessage(const Message: WideString);
published
  property IntProperty: Integer read GetIntProperty write SetIntProperty;
  property StringProperty: WideString read GetStringProperty write SetStringProperty;
end;

As you can see, our custom object is inherited from the THYDispatchWrapper class. By default this class provides reference counting, and if the reference count hits 0, it will be destroyed, but you can change this behavior by overriding the \_AddRef and \_Release methods. This class will automatically process calls from the Silverlight side and route them to the appropriate methods or properties.

Please note that in order to be visible to Silverlight, your methods must be placed in the public section and your properties should be in the published section.

You can pass data types as described in this article. There are, however, a couple of limitations: For one, you need to use the type for arrays, as in this example:

TCustomObject = class (THYDispatchWrapper)
private
  function GetArrayProperty: Variant;
  procedure SetArrayProperty(const Value: Variant);
published
  property ArrayProperty: Variant read GetArrayProperty write SetArrayProperty;
end;

function TCustomObject.GetArrayProperty: Variant;
begin
  Result := VarArrayCreate([0, 2], varOleStr);
  VarArrayPut(Result, 'Delphi', [0]);
  VarArrayPut(Result, 'Host', [1]);
  VarArrayPut(Result, 'Data', [2]);
end;

procedure TCustomObject.SetArrayProperty(const Value: Variant);
var
  LBound,HBound: Integer;
  StrValue: String;
  I: Integer;
begin
  LBound := VarArrayLowBound(Value, 1);
  HBound := VarArrayHighBound(Value, 1);

  for I := LBound to HBound do
    StrValue := StrValue + VarToStr(VarArrayGet(Value, [i]));
end;

You can see how to create and fill a new string array and how to read a string array that is coming from Silverlight.

The second limitation is that you can't pass custom types such as records.

If you need to pass a custom object to a Silverlight plugin, and, as stated before, only the plugin itself can initiate communication, the host needs to react. To achieve this we've introduced a new event in the THYModuleManager and the THYFMXModuleManager that is called OnGetAutomationObject.

Let's take a look at the following sample:

procedure TMainForm.ModuleManagerGetAutomationObject(Sender: THYModuleManager; const ProgId: string;
  const Flags: THYGetAutomationObjectFlags; var Disp: IDispatch);
begin
  if ProgId='Hydra.Host' then
    Disp := TCustomObject.Create;
end;
  • This event will fire every time the Silverlight plugin calls '' AutomationFactory.CreateObject'' or AutomationFactory.GetObject.
  • Depending on the call, the Flags parameter will be set to aoGet or to aoCreate. You can handle this manually to create a new object or return an existing instance.
  • We're checking for our custom ProgId value which can be set on the Silverlight side to ensure that we are reacting on a proper call.
  • If you set the Disp property that means the call is handled and this object will be returned to the plugin; if you set this property to nil, Silverlight will perform a default call that allows to create an automation object that is registered in the system as Excel.Application or WScript.Shell.

There is one more event in the module manager that is related to Silverlight: OnNotifySilverlightError. This event handler invokes whenever Silverlight reports an error within XAML DOM of the hosted control.

WinForms Host

On the .NET side you don't need any base object to be able to communicate with the plugin since .NET already handles this for you. First of all, we need to do some basic setup for the host:

public partial class Main : Form, IHYCrossPlatformHost, IHYCrossPlatformSilverlightHost
{
  [..]
  public void GetAutomationObject(string ProgId, GetAutomationObjectFlag Flags, ref object Dispatch)
  {
    if (ProgId == "Hydra.Host")
    {
      Dispatch = this;
    }
  }

  public void NotifyError(string Error, string Source, int Line, int Column)
  {
    MessageBox.Show(Error);
  }
}

Unlike Delphi, WinForms hosts don't come with a default host object, so you need to define one manually. To do so, you need to implement the IHYCrossPlatformHost interface, in the example it is implemented it in the main form, but you can choose whatever object you like.

To be able to process calls from Silverlight, you also need to implement the IHYCrossPlatformSilverlightHost. This interface has two methods, GetAutomationObject and NotifyError, which act exactly the same as in Delphi.

GetAutomationObject allows you to react to AutomationFactory.CreateObject and AutomationFactory.GetObject calls (the Flags parameter will be set accordingly). NotifyError will be invoked whenever Silverlight reports an error within XAML DOM of the hosted control.

Note that we passing this as our automation object, so all calls will go to the main form, but you can pass every object you want, not just the main form.

As a second step, we need to assign our host to the plugin, for example:

private void Main_Load(object sender, EventArgs e)
{
  moduleManager.LoadModule("SilverlightPlugin.xap");
  Instance = moduleManager.CreateInstance("SilverlightPlugin");
  Instance.Host = this;
  hostPanel1.HostPlugin(Instance as IBasePlugin);
}

We've assigned the host reference directly to the plugin instance by setting the Instance.Host property, but it is also possible to set the moduleManager.Host property so every new plugin instance will receive a reference to the host automatically.

When the host is ready to work with Silverlight, you can add methods and properties that will be visible to the plugin.

public void SendMessage(string Message)
{
  MessageBox.Show(Message);
}

public int IntProperty
{
  get { return 1; }
  set { MessageBox.Show(value.ToString()); }
}

public string StringProperty
{
  get { return "Hello for WinForms"; }
  set { MessageBox.Show(value); }
}

public string[] ArrayProperty
{
  get { return new string[]{"The", "Net", "Data"}; }
}

public void SetArrayProperty(string[] Arr)
{
  //work with array
}

As you can see, all methods and properties that should be accessible for the plugin should be declared as public.

There is a problem in Silverlight with using array properties, you can read them just fine, but attempting to set them will lead to an exception. But there is a workaround: you can use array parameters in methods to set a property, and this approach works just fine.

Now we are ready to discuss how to talk to a host from a plugin.

Plugin

First we need to adjust our project. In order to use the dynamic type, we need to add a reference to the Microsoft.CSharp.dll assembly.

Go to Project -> Add Reference and select Microsoft.CSharp.dll.

Now we are ready to work with the host, so let's take a look at the following example:

private void button1_Click(object sender, RoutedEventArgs e)
{
  if (AutomationFactory.IsAvailable)
  {
    dynamic Host = AutomationFactory.CreateObject("Hydra.Host");

    int HostInt = Host.IntProperty;
    double HostDouble = Host.DoubleProperty;

    Host.IntProperty = 42;
    Host.StringProperty = "Message from Silverlight";

    Host.SendMessage("Host data, integer: "+ HostInt.ToString() + " string: " + HostString);
  }
}

Now let's take closer look at each line:

  • First we need to check if our project can use AutomationFactory, we do this by checking the AutomationFactory.IsAvailable property.
  • dynamic Host = AutomationFactory.CreateObject("Hydra.Host"); - We define a variable of dynamic type that will hold a reference to the host, and call the CreateObject method to get a new instance of the host object. The ProgId parameter identifies the automation object that we requested. You can also use the GetObject method to get an existing reference to a host object. As described in the hosting sections, you will be able to handle this call by subscribing to the module manager's OnGetAutomationObject event (in Delphi) or by providing an implementation for the GetAutomationObject method (in .NET).
  • Please note that when using the CreateObject or GetObject methods, you will need to provide proper exception handling in case AutomationFactory is unable to get or create an object.

Now that we have a reference to the host object, we can use its properties or call its methods.

Please note that since Silverlight allows only late binding, there will be no IntelliSense support or compiler warnings to help prevent errors. As a result, you should pay special attention to member's names and test your code thoroughly.

And this is all you need to do to be able to communicate with a host from your Silverlight plugin.