I’m doing some work with a customer at the moment that has a large and complex application written in Progress, and they are wanting to do some work to add new features to it, but want to do so via Silverlight.

Now the way this application works from a UI perspective is that there is an application host shell with a menu and so forth, and when people request a screen it creates a new tab and hosts the appropriate screen in that tab.  That screen could be a Progress GUI screen, a console screen or a web page, and now we need to add Silverlight screens to the mix as well.

Something like this:

image

Now for this to work we have to jump through a few hoops.  We need Silverlight to be hosted in a web page, which means we’ll need to host a web browser control somewhere.  We’ll also need to host that browser control in a WinForms user control since in Progress we can add a wrapper around the user control to make the whole thing callable via the Progress shell, as well as being able to receive events.  When it’s put together it looks something like this:

image

What we’re also going to do is make sure we have just one Silverlight application containing all the Silverlight forms we want.  We don’t want to create an application per form since that would likely turn into a huge maintenance and deployment problem and make development overly complex.

Now for all of this to work we will use the Silverlight to HTML bridge as well as the ability to have pages in the WebBrowser control talk to a .NET object via COM.

Wrap the WebBrowser Control

Let’s start by wrapping the web browser control.  We could simply subclass it to do what we need, but if we do we won’t be able to get information back from the Silverlight UI the way we want, so we’ll instead create a custom user control and host the WebBrowser in that.

We’ll also create a few methods on the control to navigate to the page where the Silverlight application is hosted, and to request the particular Silverlight form we want.

Now this code isn’t quite so straightforward since we have some timing issues to deal with.  If the Progress code simply calls “Navigate” and then “Open Form” I could be requesting information from the Silverlight application before either the web page or the Silverlight app has finished loading and if that happens then we’ll have errors thrown in our UI.  This isn’t so good.  We need to wait until things are ready before commencing.

image

Here’s the code we need.  Note that a variable number of arguments for the Silverlight pages is catered for by passing an Object[] array as a parameter.

public partial class SilverlightHost : UserControl
{
public SilverlightHost()
{
InitializeComponent();
webBrowser1.AllowNavigation = false;
webBrowser1.AllowWebBrowserDrop = false;
webBrowser1.IsWebBrowserContextMenuEnabled = false;
webBrowser1.WebBrowserShortcutsEnabled = false;
webBrowser1.DocumentCompleted += (s, e) => documentLoaded = true;
}

public void LoadSilverlightFrom(string url)
{
NavigateToUrl(url);
}

private bool documentLoaded = false;

private void NavigateToUrl(string url)
{
documentLoaded = false;
webBrowser1.Navigate(url);
}

public void ShowForm(string formName, object[] parameters)
{
while (!documentLoaded)
Application.DoEvents();
var args = new List<object> {formName};
if (parameters != null)
args.AddRange(parameters);
webBrowser1.Document.InvokeScript("ShowForm", args.ToArray());
}
}

Edit the Web Page For Silverlight Interaction

The keen of sight will have noticed that in the ShowForm method above we call a JavaScript script on the web page called ShowForm.  This is not a standard script, we need to go and add it.

We’ll also need to do a little work in our JavaScript to handle the fact that the script can be called before the Silverlight object has actually completed initialisation.

<body>
<form id="form1" runat="server" style="height:100%">
<div id="silverlightControlHost">
<object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%" id="silverlightControl">
<param name="source" value="ClientBin/HostedSilverlightPOC.xap"/>
<param name="onError" value="onSilverlightError" />
<param name="background" value="white" />
<param name="minRuntimeVersion" value="4.0.50826.0" />
<param name="onload" value="SilverlightLoaded" />
<param name="autoUpgrade" value="true" />
<a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=4.0.50826.0" style="text-decoration:none">
<img src="http://go.microsoft.com/fwlink/?LinkId=161376" alt="Get Microsoft Silverlight" style="border-style:none"/>
</a>
</object><iframe id="_sl_historyFrame" style="visibility:hidden;height:0px;width:0px;border:0px"></iframe></div></form>

<script type="text/javascript">
var silverlightReady = false;
var navigateToFunction;
var formName;
var args;

function SilverlightLoaded(sender, args) {
silverlightReady = true;
if (typeof(navigateToFunction) == 'function')
navigateToFunction();
}

function ShowForm() {
args = Array.prototype.slice.call(arguments);
formName = args.shift();
var control = document.getElementById('silverlightControl');
if (silverlightReady)
control.Content.Page.NavigateTo(formName, args);
else
navigateToFunction = function () {
control.Content.Page.NavigateTo(formName, args);
};
}
</script>
</body>

So a few things to note. First we hook the Silverlight onLoad event (highlighted) and use it to set a Boolean indicating wether the control has finished loading or not.  Once the loading is complete the SilverlightLoaded function is processed where it toggles the flag and calls the navigateToFunction, if it’s been populated.

In the ShowForm function we check if the Silverlight control has loaded and if it has we call the NavigateTo method (we’ll get to that in a minute) and if not we populate the navigateToFunction which will be called once Silverlight has finished loading.

If you’re wondering the JavaScript call will mash the form name and the Object array for the parameters into a single arguments variable.  We strip the form

And yes, for those wondering, this code can be made DRYer and cleaner. I left it like this to (hopefully) aid understanding.

The HTML to Silverlight Bridge

So now we have our WinForms control calling the JavaScript on our web page, but we now need to do a few things in Silverlight to receive that call.  This is pretty simple:

public partial class NavigationControl : UserControl
{
public NavigationControl()
{
InitializeComponent();
HtmlPage.RegisterScriptableObject("Page", this);

pageLaunchers.Add("Main", (p) =>
{
var mp = new MainPage();
mp.textBox1.Text = (string)p[0];
return mp;
});
pageLaunchers.Add("AnotherOne", p =>
{
var page = new AnotherOne();
return page;
});
}

private readonly Dictionary<string, Func<IList<object>, UserControl>> pageLaunchers = new Dictionary<string, Func<IList<object>, UserControl>>();

[ScriptableMember]
public void NavigateTo(string controlName, ScriptObject parameterList)
{
var paramList = parameterList.ConvertTo<List<object>>();
var launcher = pageLaunchers[controlName];
LayoutRoot.Children.Clear();
var form = launcher(paramList);
LayoutRoot.Children.Add(form);
}
}

The Navigation control here is an empty user control.  There is nothing in it by default so we can load other controls up into it as the host.  These other controls are the pages that we’re wanting our Progress code to be able to call.

Now to get the Silverlight bridge working we simply register the user control as a scriptable object and then mark the methods we want available to JavaScript with the [ScriptableMember] attribute.  What you’ll note is that the Object[] array from JavaScript comes through to our Silverlight method as a ScriptObject.  We need to convert it in Silverlight to an object collection before we can do things with it.

You’ll also notice that we’re using a collection of initialiser methods to avoid huge case or if statements that map from the string form name we pass through from the WinForms control to the actual form we’re wishing to display.

When we receive a request to load a form we check the collection for an initialiser, clear the current page and initialise the new one.  Once we have a reference to it, we load the form up as a child of the navigation control which will automatically show it to our end users, and we’re done.

Raising Events Back In WinForms

So we can now display any form we want to from Progress.  We then want to be able to click a button in Silverlight and have an event with some attached data raised back in WinForms for Progress to listen to.  Let’s create an event indicating that the Silverlight app is asking the host app to open another form, such as a console or Progress GUI form.

In Silverlight we can create a simple method as follows:

public static void AskHostForForm(string formName, string p1 = "", string p2 = "", string p3 = "")
{
var form = (ScriptObject)HtmlPage.Window.GetProperty("external");
form.Invoke("AskHostToOpenForm", formName, p1, p2, p3);
}

And call this from a button click event.  For ease of use by all forms this has been made static and resides in the NavigationControl class.

The HtmlPage.Window.GetProperty(“external”) call will retrieve from the web page hosting the Silverlight control any external reference objects on it.  In our case that’s going to be the WinForms control.  And we’re going to need to ensure that we create an AskHostToOpenForm method

Back in our WinForms control we need to now expose our control as a ComVisible component and tell the WebBroswer control that it is an ObjectForScripting.  Doing so will automatically make all public methods on the control available for calling either from JavaScript or from Silverlight via the HTML Bridge as shown in the code snippet above.

We also need to add some code to our WinForms custom control to raise an event when the method is called (so that Progress can do something with it)

Here’s the code.

[ComVisible(true)]
public partial class SilverlightHost : UserControl
{
public event OpenFormRequestedHandler OpenFormRequested;

public SilverlightHost()
{
InitializeComponent();
...
webBrowser1.ObjectForScripting = this;
}

...

public void AskHostToOpenForm(string formName, string p1, string p2, string p3)
{
var args = new OpenFormRequestedArgs(formName, new object[] {p1, p2, p3});
if (OpenFormRequested != null)
{
OpenFormRequested(this, args);
}
}
}

public delegate void OpenFormRequestedHandler(object sender, OpenFormRequestedArgs args);

You’ll see that the number of parameters is fixed.  I tried messing about with a variable number of arguments but when I did so the parameters were simply COM objects and there’s no way I was going to mess around with COM :-)  I’m not that crazy!

Wrap Up

So there we have it, we can now have a Progress application (or any other WinForms app) that hosts a Silverlight form within a tab of the application and be able to get events back from that form when required.

The code for this is up on GitHub, so feel free to grab it and have a look at how it all fits together.  If you’ve got any improvements to it that you want to suggest then please let me know.  I’m always happy to get feedback!