In Part 1, I outlined how we handled screen management in the Silverlight 1.1 Carbon Calculator. In Part 2, I'll present a basic framework for handling similar screen management in your own Silverlight applications. If you haven't yet read Part 1, please do so, as it presents the basics we're building upon here.
Links for the demo application and the source code are both posted at the end of this article. For all images, click the thumbnails for larger versions.
The pattern illustrated here provides a way to simulate windows in what is inherently a non-windowed system: the Silverlight plug-in running inside the browser. While I may toss around words like framework, what I have here isn't necessarily a framework to be used as-is, but it is an implementation of a pattern you may find useful in your own Silverlight applications. Feel free to take the code and adjust as you see fit.
Before we start, if you have Silverlight 1.1 installed, you may want to run the demo first, to get a feel for how it operates. You can run the demo from my site here.
Basics
As is typical in a Silverlight application, it all starts with Page.xaml. Atypically, however, Page does next to nothing in this application. Instead, Page instantiates MainScreen which is the first screen in the application to follow the UserControl-as-screen pattern. The downside of that is the vast majority of the application has to resolve control names using FindName, but I explained some ways to ease that pain in Part 1.
MainScreen hosts on it a couple buttons and some hidden screens. Those screens are shown or hidden based upon which buttons the user clicks. While I could have put those all directly on Page, having a MainScreen like I do here allows me to animate the main screen the same way I do all the others.
Unlike a true windowing system, everything here exists in the same screen space, so you have to understand zorder and the natural placement of xaml elements so that, for example, your dialog box doesn't display underneath the caption for something on your main screen. Luckily, this is pretty simple to do as long as you don't muck around with z-order early in the process. Simply put things in the order they should be on the visual stack with the items most on top listed last in the xaml file (the xaml processor puts them in in the order it reads them so last=on top)
If you do mess with z-order, do so understanding you'll probably lose a lot of hair in the process. At least in the earlier versions of 1.1, z-order didn't work quite correctly, and it caused all sorts of issues. My main recommendation here is to establish a range of zorders to be used for each "layer" in the application, and stick to those numbers; that will make it easier for you to manage.
Class Model
Since I wrote this demo from scratch, I took the opportunity to refactor a bit and use more intuitive (no baggage) class names. The class diagram follows below.
Whereas Part 1 presented ControlBaseEx for the primary base class, I simplified that here to ControlBase. I also refactored the wizard functionality into a separate set of classes rather than roll it all into the main screen base class. In the end, I think you'll find the code easier to follow. While the code is pretty self-explanitory, here are the descriptions of the main classes:
ControlBase
This is the root class from which all my controls and screens derive. It has three things we're interested in:
LoadXaml() - This function wraps the ugliness that comes with the usercontrol template by default. It also handles assigning the RootElement needed for FindByName
RootElement - This is the element returned from InitializeFromXaml. You need this to do any FindName-type name resolution as calling FindName on the control itself will not return what you are looking for. (it took me a while to figure this out on the Carbon Calculator project)
FindByName - As mentioned in Part 1, FindByName cleans up the FindName functionality a bit, automatically going agains the correct root element and returning a strongly-typed reference to the element.
ScreenBase
The primary class upon which the basic screens build is ScreenBase. ScreenBase includes a number of useful functions with Show() and Hide() being the most important.
Show() handles displaying a window on the screen. Since this calls out to an animation with a known name (the constant in this file), the way the window is shown can be almost anything from a simple display to rolling in from the side of the screen while zooming in and out and varying opacity. Of course, that may make the user cry out for Dramamine and ginger root, but I'm not concerned about that here :)
Hide() does the opposite of show. Like Show, if an animation with a known name exists, if will use that to hide the screen. Otherwise, it will just set the screen to Visiblity.Collapsed.
Basic Screens
Basic screens, like MainScreen.* are the foundation upon which we build these types of applications. While I used one only for the root screen here, one could use them for any number of other screens in a typical application.
Keep in mind that screens need not be rectangular. You could use this pattern to implement simple scene management functionality to keep the application components encapsulated when using very fluid screens with complately non-rectangular shapes.
Wizard Pages
Like most people with a Windows Forms (and VB3-6) background, I have have written countless wizards over the years. For this Silverlight example, I whipped up a very simplified version of the pattern I followed in those wizards.
The wizard itself controls the navigation from page to page. Each page, however, controls whether or not it allows moving forward or moving backwards, typically based upon its position in the chain and whether or not the data entered on that page is valid.
When you get into more complex wizards (with multiple brances, and the ability to skip around based on user entries) I'll typically have a navigation graph/tree at the controller page level and handle all navigation through it. In that case, each page will simply implement an IsValid method to let you know if the page data is ok or not.
When storing data for the wizards, a publically-accessible class (typically a singleton) is the easiest way. Each page of the wizard will then plug its values into the wizard pages.
In the carbon calculator, we did basically that. Steve wrote the data and calculation classes and I wrote the singleton state manager which maintained the application state. The wizards simply get/set data in those classes via the state manager.
There are lots of different ways to handle wizard management. Feel free to implement your own patterns directly on top of the usercontrol screen framework presented here.
Modal Windows
Modal windows (dialog and message boxes in standard Windows applications) are a special type of window which blocks all other processing until the user takes an action to close the window. To simulate the most important part of this behavior: disallowing clicking on anything outside of this window, I use a screen-covering canvas to absorb the clicks and visually darken the rest of the Silverlight control. Ideally you'll make your covering some variant on the primary background color from your main screen. In the Carbon Calculator, that was white; in this example, it is black.
Hey, dig those enormous touchscreen-friendly buttons :)
The "modal" window here has a DialogResult just like real modal windows. Interestingly enough, Microsoft left the DialogResult enum in System.Windows.Controls for Silverlight. I simply re-used it here.
Like the other windows, one simply uses the .Show() method to display the dialog. Unlike standard Windows modal dialogs, the .Show() method immediately returns, so you need an event handler to capture the dialog result once the user closes the window. The code you use to retrieve the dialog result looks like this:
// Raised when the dialog hide animation has completed
void _exampleDialogScreen_Closed(object sender, EventArgs e)
{
// Operate on the selection made in that window
if (_exampleDialogScreen.DialogResult == DialogResult.OK)
{
_messages.Text = "You clicked \"OK\"";
}
else
{
_messages.Text = "You cancelled the dialog.";
}
}
Debugging Tip
One main debugging tip to offer you when you build your own extensions to this: put exception handlers in your constructors. I usually put exception handlers in there with a break point on the catch statement (or you can set the compiler to break on all exceptions). The reason is that exceptions in the usercontrol constructors show up as XAML Parser errors further up the stack. The deeper you nest your screens and controls, the harder it will be for you to find problems.
Wrap-Up
You can run the demo from my site here.
The Silverlight 1.1 September Refresh version of the source code may be found here. You'll need Visual Studio 2008 Beta 2 to compile it. In the code I have included a sample button, a sample dialog box, and a sample wizard. The base classes are all also present.
There is a lot more that you can do with this pattern to make a true and robust windowing system in Silverlight, especially around handling modal windows and handling reuse/dynamic instantiation of multiple instances of a single window. If Microsoft doesn't provide a windowing system in the release of 1.1 (most RIAs can function well without one), I'll revisit this project and make it into something serious.
If you have any questions or ideas for this, please post a comment right to this post. I'd love to get other ideas on how you might be interested in using a pattern such as this.