.. a journey getting to the contents
The last time I have been getting familiar with Silverlight. As a .NET developer with a big focus on the web I am interested in the possibilities of this plugin. (Eventhough, I also keep a close eye on the interesting stuff around HTML5.)
Image via Wikipedia
I always wanted to add a little more to web applications and sites that are maybe possible using clever Javascript, but are probably easier to achieve using a plugin like Flash. Being quite fluent in .NET and C# and reasonably agile in Javascript I didn't want to learn yet another language and different IDE. So, when Silverlight came on stage I was immediatelly interested. I could be using the familiar Visual Studio, familiar C#, familiar (though slightly limited implementation of) .NET framework to do some exciting new stuff. XAML came in as a new aspect, but that was already part of the WPF stuff, so not completely new. Expression Blend to create all sorts of horrible interfaces and well, the sky is not even the limit anymore.
After some useless experimentation (extending "Hello world!") I decided to stop right there and drop everything and make a good start right from the beginning. Fundamentals first.
A framework
Within our company we use a clever framework that implements the MVC pattern. We use the same framework for Windows Forms application and ASP.NET applications. By completely separating the View logic from the Model and Controller (as it should be done) we can use the same Model and Controller logic in both types of applications.
The framework has a Mediator and all functionality is handled through that using Commands. All View controls are registered with the Mediator and are signalled to update the view when "things" change in the Model. The controls then update themselves and show or hide bits of themselves.
At first there was a bit of getting used to this way of programming, but now it strikes me again and again how little code is needed to implement logic.
All logic is handled in commands. When a button is clicked the Mediator is called to execute a command with some optional parameters. These Commands are implemented in various classes and are decorated with a Command attribute and implement an ICommand interface. The ICommand interface makes that the methods that a Command needs are implemented. The Command also has access the Model through ApplicationData object. I made a SilverlightApplicationData type with some small changes to the WebApplicationData type. For instance in the WebApplicationData we grab the buildnumber from the Web.Config and there is no such thing in Silverlight, so it is dropped for this first implementation.
Anyway, just add the attribute and we are good to go.
[Command("COMMANDNAME")]
The code the ICommand interface has among others a Do() method that can be called by the Mediator. A typical Button Click EventHandler is really simple.
Mediator.Execute("COMMANDNAME", "string parameter");
Or ..
Mediator.Execute("COMMANDNAME", "string parameter", intParameter, someOtherClassParameter);
Every parameter after the command name is passed to the actual Command as argument for its constructor.
The Mediator then executes the Command. However, the framework doesn't know which Commands will be implemented. Neither do I. What is needed is a Dictionary with all available Commands.
Dictionary<string, Command> commands
How to can the framework know which commands there are? Do we need to maintain a list in a config file? A bit nasty really. But the backbone of .NET Framework comes to the rescue: Relfection.
Reflection
Using reflection we can iterate all application assemblies and scan all types and make a Dictionary with all types that have the Command attribute. These can then be stored in the Dictionary.
Sounds easy, but is it?
Windows and web is simple
For Windows and Web applications iterating the assemblies is quite simple. We just look at the content of the Bin folder and load all *.dll (and *.exe) files. Then using reflection we scan all Types for the Command attribute. When found we add them to the Dictionary.
foreach (string file in Directory.GetFiles( path, searchPattern, SearchOption.AllDirectories )) { Assembly assembly = Assembly.LoadFrom( file ); Type[] types = assembly.GetTypes(); foreach (Type type in types) { object[] attr = type.GetCustomAttributes(typeof(CommandAttribute), false); if (attr.Length == 1) { CommandAttribute attribute = (CommandAttribute)attr[0]; Command command = (Command)assembly.CreateInstance( type.FullName ); commands.Add( attribute.CommandName, command ); } } }
This is all done in a static CommandManager so we only need to scan the files once.
Silverlight is slightly different
With Silverlight the situation is a bit more complicated. There is no Bin folder that can be scanned. There is a ClientBin folder, but that's on the web server and even then the files are inside the Xap file!
Inside that renamed Zip file we find the assemblies and AppManifest.xaml file in which these files are listed. Well how do we get there?
Doing quite a bit of Googling I finally got on the right track. The above code sample needs quite a bit of rework. Whe need to do a number things:
- Open the Xap file
- Read the AppManifest.xaml.
- Parse the names of the assemblies
- Load the assemblies from the Xap file
Eventhough the Silverlight application is downloaded by the browser to the client computer we can not really get at the Xap file directly. That's the sandboxing security model working against us. So we need a clever workaround.
Luckily there is a way to get there. We can download the file to Local Storage. Since we are on the web this happens asynchronously. Using the OpenReadCompletedEventHandler we can continue the process.
Note that we also check for the existence of the Xap file in LocalStorage, just to prevent repeated downloads when you refresh the web page.
Uri xapUri = Application.Current.Host.Source; string[] parts = xapUri.AbsolutePath.Split('/'); xapFile = parts[parts.Length - 1]; // the filename is the last part of the path using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication()) { // check if the file is available if (!store.FileExists(xapFile)) { // when not available download it WebClient webClient = new WebClient(); webClient.OpenReadCompleted += new OpenReadCompletedEventHandler(WebClientOpenReadCompleted); webClient.OpenReadAsync(new Uri(xapFile, UriKind.Relative)); } else { LoadAssembliesFromIsolatedStorage(); } }
When WebClient has completed download the stream is copied to IsolatedStorage
private static void WebClientOpenReadCompleted(object sender, OpenReadCompletedEventArgs e) { using (IsolatedStorageFile storageFile = IsolatedStorageFile.GetUserStoreForApplication()) { IsolatedStorageFileStream fileStream = storageFile.CreateFile(xapFile); WriteStream(e.Result, fileStream); fileStream.Close(); } LoadAssembliesFromIsolatedStorage(); }
Now the assembly processing can begin.
private static void LoadAssembliesFromIsolatedStorage() { using (IsolatedStorageFile storageFile = IsolatedStorageFile.GetUserStoreForApplication()) { IsolatedStorageFileStream fileStream = storageFile.OpenFile(xapFile, FileMode.Open, FileAccess.Read); // parse the AppManifest.xaml file Stream manifestStream = Application.GetResourceStream(new StreamResourceInfo(fileStream, "application/binary"), new Uri("AppManifest.xaml", UriKind.Relative)).Stream; string appManifest = new StreamReader(manifestStream).ReadToEnd(); XElement deploymentRoot = XDocument.Parse(appManifest).Root; ListdeploymentParts = (from assemblyParts in deploymentRoot.Elements().Elements() select assemblyParts).ToList(); // now load assemblies one by one foreach (XElement xElement in deploymentParts) { string source = xElement.Attribute("Source").Value; AssemblyPart asmPart = new AssemblyPart(); fileStream = storageFile.OpenFile(xapFile, FileMode.Open, FileAccess.Read); StreamResourceInfo streamInfo = Application.GetResourceStream(new StreamResourceInfo(fileStream, "application/binary"), new Uri(source, UriKind.Relative)); Assembly assembly = asmPart.Load(streamInfo.Stream); // from here the code is unchanged Type[] types = assembly.GetTypes(); foreach (Type type in types) { object[] attr = type.GetCustomAttributes(typeof(CommandAttribute), false); if (attr.Length == 1) { CommandAttribute attribute = (CommandAttribute)attr[0]; Command command = (Command)assembly.CreateInstance(type.FullName); commands.Add(attribute.CommandName, command); } } } } }
Well, it took quite some googling around and advice from a friend to get this working, but in the end the steps are quite easy to understand.
Almost there ..
So now we have everything we need. Almost that is.
As I mentioned earlier Silverlight has a slightly limited implementation of .NET framework in the sense that not all namespaces are completely implemented in the same way. And that threw up another hurdle to take.
To create an instance of Command we use an overload of the CreateInstance method in which we can pass the parameters for the command in the args parameter.
ICommand command = (ICommand)assembly.CreateInstance( commandType.FullName, false, BindingFlags.Default, null, args, CultureInfo.InvariantCulture, null );
Unfortunately Silverlight does not have this overload. We cannot pass any parameters to the instance using CreateInstance. In effect only the default constructor is called. Why the overload is omitted in Silverlight strikes me as odd.
Yet, more research was needed to solve this problem. More Reflection was needed to find the correct constructor for the type.
Type[] parameterTypes = new Type[args.Length]; // Initialize a ParameterModifier with the number of parameterTypes. ParameterModifier parameterModifiers = new ParameterModifier(args.Length); for (int i = 0; i < args.Length; i++) { parameterTypes[i] = args[i].GetType(); parameterModifiers[i] = true; } // The ParameterModifier must be passed as the single element of an array. ParameterModifier[] paramMods = { parameterModifiers }; // Get the constructor that takes an integer as a parameter. ConstructorInfo constructorInfo = commandType.GetConstructor(BindingFlags.Instance | BindingFlags.Public, Type.DefaultBinder, parameterTypes, paramMods); ICommand command = (ICommand)constructorInfo.Invoke(args);
This made it all work.
Mediator.Execute("COMMANDNAME", "string parameter");
The Mediator can now make a deep copy of the Command referenced by the name "COMMANDNAME" (using the constructor that takes string parameter) and then call that Command's Do() method to actually execute it.
A really interesting journey it was and now I can start to make something useful using this framework.
Thanks.
ReplyDeleteThis *almost* solved a very similar problem I am facing. It may be that my need differs from yours somehow, or maybe I've not implemented it the same way.
I tried to make the call to download the xap, load the assemblies and extract the types from each assembly from the App.Startup method (event). I want to use the loaded types (lookup a type that has a particular attribute using reflection) in this (the App startup) method.
However, it looks like the 'WebClientOpenReadCompleted' is only fired once the calls to startup the App are complete. So - I can't have the type data I want when App.Startup is called.
I'm thinking that maybe the solution is to have the ASP.NET app embed a zero size Silverlight control in its main page to force the Silverlight project to perform the XAP download and then know for all subsequent Silverlight instances that the Xap is already downloaded, and to just use the existing file.
Does this sound reasonable, or is there a neater way?
Thanks
Trevor
Trevor,
ReplyDeleteThanks for the comment.
All of the above code is being used in a static class (CommandManager). In the constructor of that class I call the method HandleXapFileContent() that then loads all types that I need.
In the Mediator.Execute("COMMANDNAME", "string parameter"); method I use the CommandManager and if it's not created yet, the constructor gets called. This is exactly at the earliest moment that I need it. No need to have it earlier, like in the App.Startup method.
So, your approach may need some refactoring in the sense that our approach might work for you as well.
General advice in this respect: don't try to do stuff before it is actually needed.
Roho,
ReplyDeleteThanks for the response.
I'm not sure our usages are close enough for me to use your technique exactly as you state it.
[ I'm new to Silverlight, so I could be missing something fundamental here, but ...]
What I need is to reflect on available types (classes) when the first Application.Startup event is called (i.e. the first time any of the code in the Silverlight 'project' is called).
Why? Well that could be where I'm going wrong. My problem is: Assuming a Silverling assembly with multiple controls (pages/XAML) in it, how to I get to choose (on the ASP side) which of those SL controls gets placed in a particular Silverlight 'object' placeholder/tag. I noticed that the Silverlight app displays a partoicular control by assigning its 'RootVisual' property to a particular control instance. My plan then was to have the code decide which SL control to instantiate and assign that to the 'RootVisual' property.
The idea I came up with is to specify a 'Control Name' in an 'initParams' parameter of the 'object' tag, and, within the Silverlight app, extract this parameter and use it as follows:
In my (Silverlight) App.Startup event, determine the parameter value and, based on this, set:
this.RootVisual = {instance_of_specified_SL_control}
Rather than add a Switch statement I thought I'd create an attribute to decorate each Silverlight user control with, and then use reflection to determine the UserControl that was 'asked' for.
So - I need to perform Reflection right at the start - the first time a Silverlight control is loaded. And the first 'entry point' I can tap into within the Silverlight app appears to be the App.Startup event. So I need to load the XAP and all the rest immediately upon the first 'call'. The problem is: I can get the load process to complete while I'm still in the app startup phase (the Async call does not return until I exit the App startup).
The solution I proposed does work, but it's not practical. Basically, I've placed a Silverlight node on my web sites main page, and this causes a call to the Silverlight app, but it does not specify a control to load: Instead it effectively sends the message 'Prepare yourself' (on the Silverlight side I do the XAP load etc. and DO NOT set the App.RootVisual).
Subsequent calls to the Silverlight app then work as the reflection data is available. This is not a practical solution as one cannot guarantee that a web user comes in through the site's home page of course.
Alternatives welcome :)
[sorry for the long post]
Roho,
ReplyDeleteThanks for the response.
I'm not sure our usages are close enough for me to use your technique exactly as you state it.
[ I'm new to Silverlight, so I could be missing something fundamental here, but ...]
What I need is to reflect on available types (classes) when the first Application.Startup event is called (i.e. the first time any of the code in the Silverlight 'project' is called).
Why? Well that could be where I'm going wrong. My problem is: Assuming a Silverling assembly with multiple controls (pages/XAML) in it, how to I get to choose (on the ASP side) which of those SL controls gets placed in a particular Silverlight 'object' placeholder/tag. I noticed that the Silverlight app displays a partoicular control by assigning its 'RootVisual' property to a particular control instance. My plan then was to have the code decide which SL control to instantiate and assign that to the 'RootVisual' property.
The idea I came up with is to specify a 'Control Name' in an 'initParams' parameter of the 'object' tag, and, within the Silverlight app, extract this parameter and use it as follows:
In my (Silverlight) App.Startup event, determine the parameter value and, based on this, set:
this.RootVisual = {instance_of_specified_SL_control}
Rather than add a Switch statement I thought I'd create an attribute to decorate each Silverlight user control with, and then use reflection to determine the UserControl that was 'asked' for.
So - I need to perform Reflection right at the start - the first time a Silverlight control is loaded. And the first 'entry point' I can tap into within the Silverlight app appears to be the App.Startup event. So I need to load the XAP and all the rest immediately upon the first 'call'. The problem is: I can get the load process to complete while I'm still in the app startup phase (the Async call does not return until I exit the App startup).
The solution I proposed before does work, but it's not practical. I've placed a Silverlight tag on my web sites main page, and this causes a call to the Silverlight app, but it does not specify a control to load: Instead it specifies that Silverlight must 'Prepare yourself'. On the Silverlight side when I get this 'instruction' I do the XAP load etc. and DO NOT set the App.RootVisual property, so no control shows.
Subsequent calls to the Silverlight app (made when attempting to show actual SL controls referenced in other ASP pages) then work as the reflection data is available. This is not a practical solution as one cannot guarantee that a web user comes in through the site's home page of course.
Alternatives welcome :)
[sorry for the long post]
Trevor,
ReplyDeleteYes, there is some difference in my implementation compared to yours.
In my App.Startup event I first make a call to my Mediator (that subsequently instantiates the CommandManager which then does all the loading of available types) and right after that I do a
this.RootVisual = new MySpecialControl();
In which MySpecialControl is the control I want to use and basically doesn't do much by itself. However, within that Control I have collection of controls that are displayed based on the State my application is in.
States like 'Edit', 'View' or 'ViewList' decide which of the controls will be shown and how they are shown. The states 'Edit' and 'View' can be served with the same control with read-only off or on.
As you want to dynamically set control to use through the initParams' parameter I can imagine that you could use your own MySpecialControl where you add the control to the Children of a "root" control. In my control I use a StackPanel named "ContentStackPanel" to host the controls.
this.ContentStackPanel.Children.Add(specifiedControl);
Perhaps that could be a way to implement your use case.
Good luck!