Almost all of the Silverlight demos you see out there are completely client-side, and invoke Web services to post/get data. This approach makes sense, but when you want to use Silverlight to functionally enhance your existing website, you may want to have some kind of integration between Silverlight objects and your server-side controls.
You may also want to enable some kind of Silverlight object composition. For example, my AsyncFileUpload component has two visualizers. It should be easy to add a new visualizer without modifying any existing code.
To address these problems, I’ve put together some experimental code.
Object composition
Each Silverlight instance has its own AppDomain. You can pass managed objects from one Silverlight instance to another through the HTML/JS bridge, but you would only be able to program against it like any other JavaScript object. The ScriptObject.ManagedObject property for such an object would be null if the underlying managed object isn’t part of the current AppDomain. As a result, we ideally compose objects inside a single Silverlight application, such that we can directly reference other objects. For example, the visualizers for our AsyncFileUploader can each reference the actual uploader, handle its events, get a stream to files being uploaded, etc.
By composing the objects inside a single Silverlight instance, we have to solve a new problem, which is visual composition. We are suddenly limited visually to a single 'canvas' if you will. I’ve addressed this to a certain extent by introducing a concept called viewports. Per Silverlight instance there can be one viewport, which will be set as the root visual. All visual objects will be added to this viewport. It’s up to the implementation of the viewport to handle how it adds the visual objects. For example, a StackViewPort may add all visuals sequentially. A GridViewPort on the other hand may add visuals to the grid based on information that was set server-side:
1
2
3
4
5
6
7
8
9
10
11
12
|
<wilco:AsyncFileUploadVisualizer ID="v1" runat="server" UploaderID="AsyncFileUpload1" />
<wilco:AsyncFileUploadProgressVisualizer ID="v2" runat="server" UploaderID="AsyncFileUpload1" />
<wilco:GridViewPort ID="GridViewPort1" runat="server" Height="200px" Width="100%">
<ColumnDefinitions>
<wilco:ColumnDefinition Width="2*" />
<wilco:ColumnDefinition Width="1*" />
</ColumnDefinitions>
<Locations>
<wilco:GridViewPortItem ControlID="v1" Column="0" />
<wilco:GridViewPortItem ControlID="v2" Column="1" />
</Locations>
</wilco:GridViewPort>
|
To do the actual composition on the client, we need a few things. We need is a loader which downloads all the right things. As I mentioned before, we want to be able to implement new components. Those components will be added to their own XAP. Our loader needs to download the XAPs for all the objects before those objects can be loaded.
Once the loader has downloaded the XAPs, we have to load the main assembly from this XAP. We can do this by parsing the AppManifest’s XAML as a deployment object and then find the main assembly. In code this looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
private static Assembly LoadAssemblyFromStream(Stream xapStream) {
string appManifestString;
using (var reader = new StreamReader(Application.GetResourceStream(new StreamResourceInfo(xapStream, null), new Uri("AppManifest.xaml", UriKind.Relative)).Stream)) {
appManifestString = reader.ReadToEnd();
}
Assembly entryPointAssembly = null;
Deployment deployment = (Deployment)XamlReader.Load(appManifestString);
foreach (var part in deployment.Parts) {
var streamInfo = Application.GetResourceStream(new StreamResourceInfo(xapStream, null), new Uri(part.Source, UriKind.Relative));
Assembly assembly = part.Load(streamInfo.Stream);
if (assembly.FullName.Split(',')[0].Equals(deployment.EntryPointAssembly)) {
entryPointAssembly = assembly;
}
}
xapStream.Close();
return entryPointAssembly;
}
|
(This technique can be useful for any kind of loader. Be careful though about the consequences. For example, if someone can use your loader to load their own XAP, that XAP would essentially impersonate your XAP, and thus be able to access its isolated store, etc. Certain things may also not work, such as resources defined in the delay-loaded XAP.)
After we’ve loaded all the right assemblies into the current AppDomain, we can instantiate every object. The easiest way to do this is if we would simply render XAML directly from our server-side controls. We can then just go through each piece of XAML and load it using the XamlReader.
To allow references between objects, we need to first instantiate all the objects and register them with a unique (user-defined) name. We then need to notify the objects that all objects have been registered such that they can actually get a reference to other objects. The following interface captures this:
1
2
3
4
5
6
7
8
|
public interface ISilverlightObject {
string Name {
get;
}
void Initialize();
}
|
For visual composition we also need a way to add visuals to a viewport if there is one. We can add another interface to take care of this:
1
2
3
4
|
public interface IViewPort : ISilverlightObject {
void AddVisual(ISilverlightObject visual);
}
|
All visuals can add themselves to the viewport if there is one by using this interface. The viewport should take care of the rest. Viewports can set themselves as the root visual if there isn’t one already. The non-visual objects don’t need to do anything special. They can just do their thing based on their property values and/or a call to their Initialize method.
Now that we have most of the client-side plumbing in place, let’s see what we can do to add some kind of integration with ASP.NET.
ASP.NET integration
So far we have a Silverlight application which is essentially a host for composite objects. To host this application, and also to group composite objects, I introduced a SilverlightDomain ASP.NET control. I also introduced a SilverlightObject base class which you implement for every Silverlight object. This base class will register its instances with the SilverlightDomain that its part of. During rendering, the SilverlightDomain will ask every registered SilverlightObject to render XAML.
The SilverlightObject base type automatically renders a few things. Based on the value of its ClientClrNamespace and ClientTypeName properties it will render the tag-name. All properties marked with a SilverlightPropertyAttribute will be added to the XAML. It will also register callbacks for all of its events marked with the SilverlightEventAttribute. On the client those events do need to be marked with ScriptableMemberAttribute, such that our plumbing code can properly attach a handler to it which initiates the postback. When the client-side event is raised and a postback is initiated, our SilverlightObject’s RaisePostBackEvent method is invoked. By default it will try to invoke a public On<EventName>(EventArgs) method, if one exists. If you don’t want to have such a public method, or you want to raise a more complex event, you can override RaisePostBackEvent and manually raise an event.
As I just mentioned, you need to implement a few abstract properties such as ClientClrNamespace and ClientTypeName. You also need to implement PackageUrl. The SilverlightObject type will automatically render these as part of the XAML it renders. On the client, our package loader will use these attributes to find out what packages need to be downloaded. It then removes those attributes such that it ends up with 'clean XAML' that the XamlReader can load the remaining XAML directly.
A Silverlight powered ASP.NET button
Although there’s usually not much point in using Silverlight just to render primitive controls such as a button, this does concisely illustrate what it takes to put together a simple ASP.NET component that uses Silverlight for rendering. Here’s the client-side code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public class Button : System.Windows.Controls.Button, ISilverlightObject {
[ScriptableMember(ScriptAlias = "click")]
public new event EventHandler Click;
public Button() {
base.Click += SimpleButton_Click;
}
void SimpleButton_Click(object sender, RoutedEventArgs e) {
if (Click != null) {
Click(sender, e);
}
}
void ISilverlightObject.Initialize() {
App.Current.LoadVisual(this);
}
}
|
And the server-side code looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
public class Button : SilverlightObject {
protected internal override string ClientClrNamespace {
get {
return "clr-namespace:Wilco.Windows.Browser;assembly=Wilco.Windows.Browser";
}
}
protected internal override string ClientTypeName {
get {
return "Button";
}
}
protected internal override string PackageUrl {
get {
return Page.ClientScript.GetWebResourceUrl(typeof(Button), "Wilco.Web.Silverlight.Resources.Wilco.Windows.Browser.xap");
}
}
[SilverlightProperty(ClientPropertyName="Background", Converter=typeof(SilverlightColorConverter))]
public Color BackgroundColor {
get;
set;
}
[SilverlightProperty(ClientPropertyName="Content")]
public string Text {
get;
set;
}
[DefaultValue(0), SilverlightProperty]
public int Height {
get;
set;
}
[DefaultValue(0), SilverlightProperty]
public int Width {
get;
set;
}
[SilverlightEvent(ClientEventName = "click")]
public event EventHandler Click;
public Button() {
//
}
protected override void RaisePostBackEvent(string eventArgument) {
if (Click != null) {
Click(this, EventArgs.Empty);
}
}
}
|
To use this on our page, we can write the following code in ASP.NET:
1
2
3
4
|
<wilco:SilverlightDomain runat="server">
<wilco:Button ID="button1" runat="server" BackgroundColor="Red" OnClick="button_Click" Text="Button 1" />
<wilco:GridViewPort runat="server" Height="300" Width="100%" />
</wilco:SilverlightDomain>
|
To see a demo of this, go to this page with a few Silverlight buttons on it. For the source code, you can download Wilco.Web.Silverlight.zip.