Last night, while working on the REST chapter in Silverlight 5 in Action, I
started playing around with ASP.NET MVC 3 and Razor. The REST
library I'm using: WCF Web API, which works with MVC 3.
(so yes, that one chapter has REST, WCF Web API, ASP.NET MVC 3,
Razor and NuGet. fun!) While it does work, I didn't want to stick
an .aspx file in the MVC 3 project as it just didn't seem right.
So, I looked into how to get Silverlight on one of the MVC
pages.
Today, I built the start of a Silverlight helper, something I'll
likely expand upon later, and something you can modify yourself if
you desire.
There's an existing helper, @Video, which apparently works with
Silverlight projects. However, I didn't like that helper name for
Silverlight apps, and I also wanted to try to build a helper myself
for grins.
Warning!
I am generally a client developer (WPF, Silverlight, XAML
in-general) and am completely new to MVC 3 and Razor. While I may
have blindly stumbled into some best practices, don't assume there
are any consciously included in this post.
To date, I have done absolutely nothing with Razor or MVC, so
this was a learning experience. That said, I did find it very
intuitive (well, as intuitive as HTML development ever is), and was
able to quickly produce something usable. Let's get to it.
Prerequisites
- Make sure you have ASP.NET MVC3 installed on
your machine.
- Make sure you have Silverlight 5 installed
MVC 3 Project Setup
Create a new ASP.NET MVC 3 Web Application named
MvcSilverlightHelper
Go with the default (in my case) options on the project setup
dialog. You want an empty project using the Razor view engine. You
could pick one of the other application types, but I wanted to go
through the process of creating the home page and controller
myself.
When complete, you'll have a solution with a single MVC 3
project named MvcSilverlightHelper. The default MVC template sets
up a pretty good project structure with all the necessary
folders.
Silverlight Project Setup
Now we're ready to add in the Silverlight project. Right-click
the solution and add a new Silverlight Application project. Name
the project "SilverlightApp".
When prompted, you want to add this to the existing MVC web
site. We'll eventually delete them, but for now, leave the options
to add the test pages checked. We'll snag some information from
those pages.
Silverlight App UI
You'll be popped right into familiar territory once that is
created. We're not going to do anything spectacular with the
Silverlight app, but we do want to make its existence obvious. So,
use this XAML for the Silverlight app's main page.
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Rectangle Fill="Orange" />
<TextBlock FontSize="30"
Foreground="White"
Margin="10"
VerticalAlignment="Center"
Text="Hello World!" />
</Grid>
<Grid Grid.Row="1"
Background="LightBlue"
Margin="0 5 0 0">
</Grid>
</Grid>
Once in, you should see an absolutely stunning orange and blue
block like this on the design surface:
That's it for the Silverlight application. Of course, you can
add any Silverlight application you want, but I chose this simple
example so you could easily see the entire bounds of the
Silverlight element on the page.
Build the solution, then right-click the
SilverlightAppTextPage.aspx file and choose "View in Browser". If
you see the designed UI in the browser, you are good to move on to
the next step.
With the MVC project created, and the Silverlight project
created and linked to the web site, the next step is to create the
Silverlight helper itself.
The Silverlight Helper Placeholder
In the MVC project add a new folder named "App_Code". While this
is a standard ASP.NET folder, it doesn't show up in the standard
folder selections. However, once you add it, it will get magically
recognized and the icon will change.
Then, in that folder, add MVC 3 Partial Page named
"Helpers.cshtml". For this example, this file will simply be
located in the root of the project.
Inside that page, add the following bit of code. The code here
defines the Silverlight helper which takes two parameters. Inside
the razor code we simply spit out a little bit of HTML that echoes
back the parameter values.
@helper Silverlight(string xapPath, string version) {
<div>
<p>Silverlight Helper</p>
<p>@xapPath</p>
<p>@version</p>
</div>
}
Make sure you put that opening parenthesis on the same line as
the @helper declaration. It took me a bit to figure out that was
why my helper wasn't being recognized.
That's going to serve as a placeholder so we can test to make
sure our helper can be used on a page. The next step is to create a
page we can use to test this helper and make sure it works.
MVC Page Setup
In the Views folder, create a subfolder named "Home". Inside
that home folder, add a new MVC 3 Partial Page named
Index.cshtml.
This is going to be the home page for the site. The contents of
that page are pretty basic, just a generated reference to the
layout file, and a call to our Silverlight helper.
@Helpers.Silverlight("~/ClientBin/SilverlightApp.xap", "5.0")
Controller and Testing
The next step is to add the controller for this home page.
Right-click the Controllers folder and select Add->Controller.
Name the controller HomeController. This is the code responsible
for rendering our view.
That will generate a controller which does nothing more than
return the correct view for our home page. I didn't change any of
the auto-generated code.
Before we test, there's just one more thing we need to do:
eliminate the default document. Right-click the project, open the
properties pages and navigate to the Web tab. On the web tab,
change the Start Action to "Specific Page" and set it to blank
Now, run the project. You should see the output from your
Silverlight helper as shown here:
Success! We now have an MVC project working with our own custom
helper. Let's have it actually output something useful now.
The Silverlight Helper Itself
Now we get into the part that may require some debugging and
testing should you expand upon this example. I hope you have a copy
of the supported browsers hanging around - I have IE9, Chrome and
Firefox all on my machine; that's the set I'll use for testing.
Crack open the SilverlightAppTestPage.html and grab all the HTML
from the silverlightControlHost div through to the end of that div
(on the same line as the closing object tag). It's important that
you retain the format and not break apart any lines that are
squished together, otherwise you'll end up with strangeness in
various browsers.
Take that markup and paste it right over the
<div></div> in our Silverlight helper. You should end
up with a helper that looks like this (your Silverlight version
number will likely be different as I'm running an internal interim
build):
@helper Silverlight(string xapPath, string version)
{
<div id="silverlightControlHost">
<object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%">
<param name="source" value="ClientBin/SilverlightApp.xap"/>
<param name="onError" value="onSilverlightError" />
<param name="background" value="white" />
<param name="minRuntimeVersion" value="5.0.60526.0" />
<param name="autoUpgrade" value="true" />
<a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=5.0.60526.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>
}
The next step is to substitute the parameters passed in.
The first parameter is for the XAP path. This tells the plug-in the
location of our Silverlight application. I want it to support
project-root-relative URLs starting with the tilde, so I'm using
the @Href function.
<param name="source" value="@Href(xapPath)"/>
The second is for the version number. This needs to be changed
on two lines, the minRuntimeVersion and the link to learn more
about Silverlight.
<param name="minRuntimeVersion" value="@version" />
<a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=@version"
style="text-decoration:none">
Error Handling and Styles
Now, we'll add in the div styles from the original page
(in-lining them into the div), as well as the error handler. The
final Silverlight helper markup and code looks like this:
@helper Silverlight(string xapPath, string version)
{
<script type="text/javascript">
function onSilverlightError(sender, args) {
var appSource = "";
if (sender != null && sender != 0) {
appSource = sender.getHost().Source;
}
var errorType = args.ErrorType;
var iErrorCode = args.ErrorCode;
if (errorType == "ImageError" || errorType == "MediaError") {
return;
}
var errMsg = "Unhandled Error in Silverlight Application " + appSource + "\n";
errMsg += "Code: " + iErrorCode + " \n";
errMsg += "Category: " + errorType + " \n";
errMsg += "Message: " + args.ErrorMessage + " \n";
if (errorType == "ParserError") {
errMsg += "File: " + args.xamlFile + " \n";
errMsg += "Line: " + args.lineNumber + " \n";
errMsg += "Position: " + args.charPosition + " \n";
}
else if (errorType == "RuntimeError") {
if (args.lineNumber != 0) {
errMsg += "Line: " + args.lineNumber + " \n";
errMsg += "Position: " + args.charPosition + " \n";
}
errMsg += "MethodName: " + args.methodName + " \n";
}
throw new Error(errMsg);
}
</script>
<div id="silverlightControlHost" style="height: 100%;text-align:center;">
<object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%">
<param name="source" value="@Href(xapPath)"/>
<param name="onError" value="onSilverlightError" />
<param name="background" value="white" />
<param name="minRuntimeVersion" value="@version" />
<param name="autoUpgrade" value="true" />
<a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=@version" 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>
}
Note that I left out the silverlight.js line. That JavaScript
file is very useful, but I didn't want to link it in on this
example as it isn't strictly required here.
That's all there is to it! Be sure to read on below as I explain
a few limitations of my implementation. Those are based simply on
my inexperience with Razor and MVC 3.
Running
With that in place, run the application. You should end up with
a page that looks like this:
Note the differences in how IE9 and Chrome render. One is too
small, one is too big. Where's Goldilocks? Read on.
Woah. Firefox won't even show the plug-in. This is an issue we
found out a loooong time ago with rendering differences, and is the
reason behind the styles in the test pages. I bring it up here in
case others run into it when incorporating Silverlight into their
applications. Without setting the height to 100% and overflow to
auto for the page, you get the inconsistent rendering above.
Firefox is actually rendering the Silverlight control, just in a
0px high div.
One way to fix this, and in fact the correct way for apps that
aren't full screen, is to wrap the Silverlight rendering inside a
div of the correct dimensions. Here, I've set it to be 500px.
<div style="height:500px;">
@Helpers.Silverlight("~/ClientBin/SilverlightApp.xap", "5.0")
</div>
And you can see that on all three browsers, the rendering is
correct at 500px high.
I'm going to stop this example for now and save handling sizing
and whatnot for a potential future post. You can now delete the
.aspx and .html pages if you want. Or, you can keep them around so
you can refer back to the original source when modifying this
helper.
Limitations and Options
I don't modify the HTML page and body styles in this helper. For
full-page Silverlight applications, the following styles are very
important, as we saw above:
<style type="text/css">
html, body {
height: 100%;
overflow: auto;
}
body {
padding: 0;
margin: 0;
}
#silverlightControlHost {
height: 100%;
text-align:center;
}
</style>
Without the styles, you end up with the Silverlight application
only taking up a portion of the page, or in the case of Firefox,
not appearing at all. However, it would be completely in
appropriate to modify the page style from inside this helper.
Instead, I'll likely modify the helper so it can take a set of
optional dimensions
I don't want to make any changes or publish this until I decide
on an appropriate course of action for the helper's styles and
dimensions. It needs to be respectful of the rest of the page,
without nasty side effects.
This version of the control also supports only a single
Silverlight instance on a page. You'd need to dynamically generate
the div IDs, avoid creating a second iframe, and only generate a
single error handler if you wanted to use more than one of these on
a single page.
This version doesn't include any of the optional parameters,
such as background color, initparams and more.
You can also create helpers completely from code. I'm sure in
many instances, that will be what you need to do in order to have
more flexibility in generating the output. You'll also want to
package the helper into a library if you want to publish it on
NuGet. I may visit that in a future post.
Note that there's no downloadable source for this at the
moment, as I'm using pre-release Silverlight components, and this
post is just to give you an idea of how to create a helper. The
Helpers.cshtml file is really all you need if you have an existing
MVC 3 project in any case.