Dynamic Silverlight Part 3: Integrating Silverlight with ASP.NET MVC
We'll finish off our tour of Dynamic Silverlight (DSL) by looking at how we can connect it with an ASP.NET MVC app. Building on our flickr photo search app from Part 2, we'll add support for saving links to your favorite photos to your MVC app. This is what the final app looks like:
You should download the solution for this part of the lab if you want to follow along.
Configuring ASP.NET MVC
This tutorial shows Visual Studio integration so you'll need to have the Silverlight Tools for Visual Studio installed along with the Silverlight SDK. Let's open up the solution in the demos\mvc_start sub-directory using Visual Studio. The first thing you should notice is how we've organized things in the project:
This is pretty much the standard ASP.NET MVC directory layout (I started from an ASP.NET MVC project); I added the App and Assets directories.
If you look at the App directory, you'll see that I've also added all of the DSL assemblies that are needed to run the app. Since we're going to be running Chiron statically (eg zap everything in this directory) instead of dynamically (look in this directory and figure out what languages I'm using and then find and zap all of those files), those files must be in that directory.
Next, we'll make sure that Chiron is integrated into the build system. If you recall from Part 1 of this series, Chiron is our local web server that will automatically generate XAP files. Since we're now going to be running using the ASP.NET Development Web Server, we'll need to statically generate our XAP file. The good news is that it's pretty easy to do this: you just need to add a post-build step to your project. The bad news is that you'll need to build the project to run changes in your script files - just refreshing the page in the browser won't do the trick. I know I've been caught by this once or twice while writing this app.
Let's add the post-build step. Open your Project properties page, click on the Build Events tab, and add this string to your project:
$(ProjectDir)DynamicSilverlight\chiron.exe /d:$(ProjectDir)App /x:$(ProjectDir)App.xap.
It should look like this when you're done:
Notice that there's a DynamicSilverlight subdirectory which contains a local copy of Chiron. I did this so that this would be a self contained project. Once we work out the details of how we'll setup the paths and ship Chiron as part of a DSL SDK, you won't need the DynamicSilverlight directory.
We'll need to make other change to the project file: enable Silverlight debugging on the client. Click on the Web tab in the project's properties dialog and make sure you select the Silverlight checkbox under the Debuggers section:
Next, we have to generate the correct HTML to boot our Silverlight app. We'll edit the code under Views\Home to add the Silverlight boostrap <object> tag to our MVC app:
If you build and run your solution, you should see the app start up in your browser. If you look at the file system, you should see that Visual Studio + Chiron has generated the App.xap file for you.
Tour of the Ruby code
The Ruby code looks an awful lot like the JScript code from Part 2 except for the parts of the app that generate WPF. In the JScript code, we were generating XAML strings and injecting them into the page. But we can do much better in IronRuby :)
We build up the WPF element tree for the photo and its border using this fragment of Ruby:
One of the reasons why programming with XAML is so easy is that the XAML parser does a lot of the heavy lifting involved in doing the proper type conversions. If you're programming against WPF directly, you're going to have to ensure that properties are assigned values of the correct type.
There's a fair amount of code in Silverlight.rb that does property transformations. For example, take a look at the code that sets render_transform_origin:
Notice how we convert the Ruby array parameter into a WPF Point object before assigning to the property. Also notice how we've monkey-patched this behavior in the UIElement class, which is where the render_transform_origin property is introduced in the type hierarchy. All current and future descendents of UIElement will automatically inherit this behavior.
Another thing to point out is BounceAnimation. This is a class that encapsulates a WPF storyboard that knows how to generate the 'bounce' effect that you'll see when the photos appear (side note: I'm not really a Silverlight guy, I only play one on TV, and I'm too embarrassed to say just how long it took me to figure this out last year). If you look at the definition for BounceAnimation in Silverlight.rb, you'll see that it's quite compact:
The other important thing to point out is that you now have a named animation. So you can now nicely encapsulate different animations, like Twirl or Zoom or Fade and simply apply them to your elements without having to copy and paste XAML.
While generating WPF trees using Ruby is more compact than generating them using XAML, there are other reasons why it is useful. Recall the code from Part 2 that loaded the images:
This code uses a grotesque hack to ensure that the WPF for the photo isn't inserted into the scene until the image has finished loading. How does it do this? By loading the image twice.
Let's look at how we can elegantly solve this problem by programmatically loading the photo from flickr:
Here, we're using the Silverlight WebClient API to asynchronously download the image. The open_read_async method initiates the download, and the open_read_completed event fires when the download is complete. The args.result property references a Silverlight Stream object which we pass to the set_source method of the Bitmap object (created earlier). Notice how we can use a Ruby closure to close over the bitmap object without resorting to passing it as a parameter to the event handler. Finally, we inject the canvas object into the scene, and tell Silverlight to begin playing the animation that we assigned to the scene.
Posting back to MVC
Let's add some additional functionality to the app. First, let's add an event handler to each photo so that it sends the photo's URI back to the MVC app when you click on it:
Notice that we're just posting the URI back to the server. Let's look at the ASP.NET MVC code that handles the HTTP GET request. This code lives in the Controllers\HomeController.cs file:
Notice how we're just stuffing URLs into the ASP.NET Session object. It's an exercise left to the reader to add user management and hook this up to a database to make this a real multi-user app.
Once the user has selected some favorites, we want to let them see their collection of favorite photos. First, we'll need to add some code to our HomeController to display the view:
Notice how we're adding our list of semi-colon separated URIs to our ViewData.
Next we'll generate a new <object> tag that injects a list of favorites into the initParams property. We'll need to add a Favorites view to our MVC project. Everything in Favorites.aspx is identical to the code that I showed you earlier for Index.aspx, except for the part where we set the initParams property:
<param name="initParams"
value="urls=<%= ViewData["urls"] %>,debug=true,reportErrors=errorLocation" />
Here, we retrieve our list of URIs from ViewData and inject it into the page.
Finally, we'll add a button that connects the UI on the client to the Favorites view. Open Views\Shared\Site.Master, and edit the code in there to add a Favorites button:
If you run the app now, you'll be able to add photos to favorites, and look at your favorites. The solution is also in the zip file - look in the mvc_end sub-directory.
It's an exercise left to the reader to improve the UI of the app so that favorite photos are selected when you click on them :)
Summary
We saw how IronRuby can generate WPF scenes without using any XAML at all. The heavy lifting of doing the type conversions is done behind the scenes by the code in Silverlight.rb. We also saw how straightforward it is to add some Dynamic Silverlight code to an MVC app. It's as simple as adding a post-build step and adding some the DSL files to a standard MVC project.
Hello, I'm new to Silverlight & MVC. I'm planning to use Silverlight for a web project.
I would like your opinion. Should I use Silverlight with asynchronous postbacks, or should I use Silverlight with the MVC extensions as you've provided in your example?
Any feedback/opinions greatly appreciated.
Posted by: Swami | March 20, 2008 at 11:27 AM
Instead of a post-build step, couldn't you just create an ASP.NET build provider, and register that? That may allow you to avoid the manual recompilation when you edit script files.
This is, I think, how the app_code directory works.
Posted by: Keith Farmer | March 24, 2008 at 12:12 PM
Keith - you're of course assuming that I knew what an ASP.NET build provider is :) I'll pass this feedback along to the MVC folks - at the end of the day they'll be owning the project system. I'm just a tourist in their world :)
Thanks!
Posted by: John Lam | March 25, 2008 at 06:30 AM
Hi John,
I tried to compile Demo3 solution, but I got this error:
Error 1 The command "C:\Documents and Settings\Administrator\Desktop\Silverlight\silverlight mvc\demos\mvc_end\DynamicSilverlight\chiron.exe /d:C:\Documents and Settings\Administrator\Desktop\Silverlight\silverlight mvc\demos\mvc_end\App /x:C:\Documents and Settings\Administrator\Desktop\Silverlight\silverlight mvc\demos\mvc_end\App.xap
" exited with code 9009. MvcApplication
Any idea what is wrong with that?
Posted by: Chua Wen Ching | March 27, 2008 at 07:35 PM
@Chua Wen Ching
I had the same issue. I resolved it by downloading and installing the Dynamic Silverlight SDK then making sure chiron.exe was in my path. I then modified the post-build step like so:
chiron.exe /d:"$(ProjectDir)App" /x:"$(ProjectDir)App.xap"
Notice the quotes around /d and /x switch variables.
John: Thanks for the sample. Very cool!
Posted by: Michael Gerfen | March 27, 2008 at 11:06 PM
Is there a method to follows this example without having visual studio 2008 installed?
thanks a lot,
zazi
Posted by: zazi | April 29, 2008 at 03:25 AM