Hosting a Silverlight Plugin

There are a couple of differences between Silverlight and all the other plugins, so in order to properly host this kind of plugin, you may need to perform some specific actions.

Loading the Plugin

Let's take a look at this 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);
}

The loading procedure looks pretty much like for any other plugin, so let's review the example step by step:

  • ModuleManager.LoadModule('SilverlightPlugin.xap'); - Loads a *.xap file that holds our plugin. The LoadModule method automatically detects the type of the plugin and calls the appropriate method, you can, however, directly tell the module manager to load a Silverlight plugin by calling LoadSilverlightModule.
  • ModuleManager.CreateVisualPlugin('SilverlightPlugin', fInstance, Panel1); - Creates an instance of the plugin, 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 a host panel manually.
  • hostPanel1.HostPlugin(fInstance as IBasePlugin); - Shows the plugin content in a host panel.
  • ModuleManager.ReleaseInstance(fInstance); - You must release an instance of a plugin before the module is unloaded.

Please note: The major difference between Silverlight and other plugins is that the name of the Silverlight plugin instance will always be the same as the name of the xap file.

Another difference lies in the communication between host and plugin.

Communicating with the 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 with the Silverlight plugins are really different than communication with other types of plugin.

The first difference is that the host cannot acquire a reference to the plugin instance, the only reference it gets is to the interface that is used internally to control Silverlight object behavior, 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, that 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're 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.