The main pattern I followed in the Silverlight 1.1 Carbon Calculator UI development was the pattern of using UserControls as screens. I spoke about this pattern in my Remix07 Presentation in Boston this week during my Real World Silverlight session.
I presented about this pattern back in the late 90s when I talked about implementing it in VB6 for Outlook-like and Wizard-like application user interfaces. I was not the only one by far to independently come up with this, as it was a very intuitive approach to managing screen design in that technology. After leaving it alone for years, I found myself returning to that pattern in Silverlight 1.1.
This pattern requires that you either dynamically load screens, or more commonly, hide and show pages within a common container on the main screen. Each screen in the application is encapsulated in a single usercontrol. Screens themselves may have other nested screens (the carbon calculator does that on the survey pages), or may simply host input/display controls.
Advantages when used with Silverlight vs multiple Page xaml files or dynamic xaml loading:
- No need to context switch between different main pages. Instead, you keep one main controller page and show/hide bits as needed
- Each screen can be designed separately in Blend as each has a separate xaml file
- Each screen can have standard windows-like methods (Show, Hide) and can manage its own state and incoming/outgoing animations
- No need to call CreateFromXaml and have a master page that contains all possible logic
Class Hierarchy
System.Windows.Control
ControlBaseEx
ControlPageBase
Derived Screens
ControlBaseEx
ControlBaseEx (named as much because there was a ControlBase that came with the SDK, and I originally used that in the project as well) is the base class for all the controls in the project. That includes controls like the drop down list box as well as the individual screens. It had a lot of logic in it to handle a number of application-specific situations, but I have listed out below only the parts important to this pattern.
public abstract class ControlBaseEx : Control
{
private DependencyObject _rootElement;
public ControlBaseEx()
{
this.Loaded += new EventHandler(OnLoaded);
}
// Simplifies calls to FindName by selecting the correct root
// element from which to base the search and also by returning
// back a strongly-typed reference to the element
// a common stumbling block in SL code is to call Control.FindName
// which doesn't return what you want (at leat in 1.1a). Instead,
// you need to call FindName from the root element.
protected T FindByName<T>(string name) where T : DependencyObject
{
return _rootElement.FindName(name) as T;
}
// Set in the derived class after it loads the xaml file
// This is the root element from which FindByName works
protected DependencyObject RootElement
{
get { return _rootElement; }
set { _rootElement = value; }
}
...
ControlPageBase
ControlPageBase is the base class for all the usercontrol screens in the application. Deriving from ControlBaseEx, it also adds in Show and Hide functionality as described above.
public abstract class ControlPageBase: ControlBaseEx
{
protected const string _windowShowAnimationName = "AnimateShow";
protected const string _windowHideAnimationName = "AnimateHide";
public event EventHandler MoveNext;
public event EventHandler MoveBack;
...
protected void LoadXaml(string xamlResourceName)
{
try
{
System.IO.Stream s = this.GetType().Assembly.GetManifestResourceStream(xamlResourceName);
RootElement = this.InitializeFromXaml(new System.IO.StreamReader(s).ReadToEnd());
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.ToString());
throw;
}
}
protected void RaiseMoveNext()
{
if (MoveNext != null)
MoveNext(this, new EventArgs());
}
protected void RaiseMoveBack()
{
if (MoveBack != null)
MoveBack(this, new EventArgs());
}
public virtual void Show()
{
...
this.Visibility = Visibility.Visible;
if (FindByName<Storyboard>(_windowShowAnimationName) == null)
{
this.Opacity = 1;
}
else
{
FindByName<Storyboard>(_windowShowAnimationName).Begin();
}
LogPageToHitbox();
}
//this will invoke a javascript event that logs to hitbox
private void LogPageToHitbox()
{
//Get the name of this class it will be logged as the link
try
{
string pageName = this.GetType().Name;
HitBox.Current.LogPageToHitBox(pageName);
}
catch
{
//eat it
}
}
public virtual void Hide()
{
if (FindByName<Storyboard>(_windowHideAnimationName) == null || !_animationsEnabled)
{
this.Visibility = Visibility.Collapsed;
}
else
{
FindByName<Storyboard>(_windowHideAnimationName).Begin();
}
}
...
Example Derived Screen
Below is an example of a derived screen. To create this screen, just add a usercontrol to your project (xaml and .xaml.cs) and change it to derive from ControlPageBase instead of from Control
public class GiftOffsetPage : ControlPageBase, ...
{
private Canvas _offsetsContainer;
private TextBlock _offsetAmountDisplay;
public GiftOffsetPage()
{
try
{
LoadXaml("CarbonCalculator.UI.GiftOffsetPages.GiftOffsetPage.xaml");
_offsetsContainer = FindByName("OffsetsContainer");
_offsetAmountDisplay = FindByName("OffsetAmount");
...
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.ToString());
throw;
}
}
...
Usage and placement
To use all of this, simply create the base classes, derive your screens from the base class, and then position your screens appropriately. You will then show and hide
In Part 2, I'll post an example application with AnimateShow and AnimateHide and pull it all together along with some class diagrams and a discussion of simulating modal windows.
For more information on the HitBox tracking, see my post on that topic.
[ Continued in Part 2 ]