In the previous two parts of this article (I had planned only 2 parts total, but this ended up a three-part article), I explained how to use Pub/Sub and the EventBus for chaining service calls in Silverlight 2 applications. You can access those articles here:
In this part, I'll cover how I use this same pattern for UI navigation in a Silverlight application.
Problem
You have multiple "screens" in the application and you can navigate to them in a variety of ways. For example, Screen 1 could contains links to screens 2, 3 and 4 and Screen 6 could contain a link back to Screen 3. Add to that the idea that a couple of the screens screens could, say, pop up a media player overlay and you see where the code could get messy quickly.
A Little Background
It may help visualize this a bit if I describe the UI in the application I'm building. The application has the following screens at this point:
- Home Screen
- Subset of Events Listing and link to Events Page
- Subset of Blog Listing and link to Blogs Page
- Subset of Media List and link to Media Page
- Link to play a featured video (shows media player overlay)
- Events Listing Page
- Blog Listing Page (General Page)
- Media Listing Page
- Videos and screencasts
- Link to show Media Player overlay
- User Options Page
- Various options the user can set
- Media Player Overlay
- Slides out over current content and plays a video.
- Loading Screen
- Last screen in the slider. Just contains a “loading” animation
The implementation of the screens is interesting. Home, Events, Blog, Media, Loading and Options are all contained in a single stack panel that slides right or left on the screen to bring the selected screen into focus. Think of it like a filmstrip, or a simple 2d carousel (but with hard stops at the end) containing screens.
When the application goes live, I’ll post a link here and you’ll be able to see the animation in action. It was pretty simple to do:
<UserControl x:Class="AppliedIS.PartnerHuddle.Screens.PageScroller"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:screens="clr-namespace:AppliedIS.PartnerHuddle.Screens"
Width="640" Height="400">
<UserControl.Resources>
<Storyboard x:Name="ScrollToPageStoryboard">
<DoubleAnimationUsingKeyFrames
BeginTime="00:00:00"
Storyboard.TargetName="PageContainer"
Storyboard.TargetProperty="(Canvas.Left)">
<SplineDoubleKeyFrame
KeyTime="00:00:00.8000000"
Value="0" x:Name="ScrollToPageStoryboardTargetX">
<SplineDoubleKeyFrame.KeySpline>
<KeySpline ControlPoint1="0,0.009"
ControlPoint2="0,0.989"/>
</SplineDoubleKeyFrame.KeySpline>
</SplineDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</UserControl.Resources>
<Grid x:Name="LayoutRoot">
<Canvas Width="600" Height="400">
<StackPanel Orientation="Horizontal" x:Name="PageContainer"
Canvas.Left="0">
<screens:HomePage x:Name="HomePage" />
<screens:EventsPage x:Name="EventsPage" />
<screens:MediaPage x:Name="MediaPage" />
<screens:GeneralPage x:Name="GeneralPage" />
<screens:UserSettingsPage x:Name="UserSettingsPage" />
<screens:DataLoadingPage x:Name="DataLoadingPage" />
</StackPanel>
<Canvas.Clip>
<RectangleGeometry Rect="0 0 600 400" />
</Canvas.Clip>
</Canvas>
</Grid>
</UserControl>
Here’s a subset of the scroller code in the scrolling screen container/panel:
private void ScrollToPage(UserControl page)
{
int i = PageContainer.Children.IndexOf(page);
if (i < 0)
{
throw new ArgumentException("Specified page is not contained in the scrollable container.", "page");
}
else
{
double currentPosition =
(double)PageContainer.GetValue(Canvas.LeftProperty);
double newPosition = i * page.Width *-1; // ASSUMPTION: All pages are the same width
if (ScrollToPageStoryboard.GetCurrentState() == ClockState.Active)
ScrollToPageStoryboard.Stop();
// build storyboard to scroll to that page
ScrollToPageStoryboardTargetX.Value = newPosition;
ScrollToPageStoryboard.Begin();
}
}
My Solution - A Specialized Event Bus
I don't deal with data here at all, this is solely a navigation and screen presentation pattern, so I'm not offering up a full MVC (or MVP) solution. I have nothing against MVC, I just decided against building that type of framework for this application (and some others) I've built. Those patterns are also much more comprehensive than what I have here. That said, I certainly borrowed some concepts.
The other point I'd like to make is that this solution does not attempt to solve the "can screen x be shown now" problem yet.
In the diagram, I left out the lines connecting the scrolling screen container from the screens themselves, as that would just be clutter. Those lines will become more important in the future when I add in events to indicate when the screen is actually displayed, and when we are about to nav off the screen (for validation purposes)
// This enum needs to contain all possible nav targets for the application
public enum NavigationScreenTarget
{
LoadingScreen,
HomePage,
EventsPage,
MediaPage,
GeneralPage,
UserOptionsPage
}
// Event arguments for the navigation events
public class NavigationEventArgs : EventArgs
{
private NavigationScreenTarget _target;
private bool _instantaneous = false;
public NavigationEventArgs(NavigationScreenTarget target, bool instantaneous)
{
_target = target;
_instantaneous = instantaneous;
}
public NavigationScreenTarget Target
{
get { return _target; }
}
public bool InstantaneousNavigation
{
get { return _instantaneous; }
}
}
// Navigation controller class
public static class NavigationController
{
public static event EventHandler<NavigationEventArgs> NavigateToScreen;
public static event EventHandler ShowMediaPlayer;
public static event EventHandler HideMediaPlayer;
// This tell the target to navigate to the screen using
// the default behavior (animated, typically)
public static void NotifyNavigateToScreen(NavigationScreenTarget target)
{
NotifyNavigateToScreen(target, false);
}
// this tells the target to navigate to the screen with no
// transition animation
public static void NotifyNavigateToScreen(
NavigationScreenTarget target, bool instaneous)
{
if (NavigateToScreen != null)
NavigateToScreen(null,
new NavigationEventArgs(target, instaneous));
}
// specialized navigation events
public static void NotifyShowMediaPlayer()
{
if (ShowMediaPlayer != null)
ShowMediaPlayer(null, new EventArgs());
}
public static void NotifyHideMediaPlayer()
{
if (HideMediaPlayer != null)
HideMediaPlayer(null, new EventArgs());
}
}
In this case, I called the class NavigationController. Other names like ScreenPresenter would certainly seem to fit. I haven’t come up with a reasonable name that doesn’t imply MVC/MVP, so if you come up with something, be sure to post it in the comments here.
I used the word “Notify” instead of “On” for the method names as this was an instructional event, as opposed to informational. I could have gone all old windows and called it “PostXEvent” or something, but that just didn’t feel right either.
I also included an example of a specialized event (NotifyShowMediaPlayer). I did that one differently from the normal navigation because it is an overlay “window”, not a real navigation target in the scrolling screen container.
When this project is complete, I’ll extract the interesting bits, like this, and post an end-to-end example that uses the Navigation, EventBus, Scroller, and other bits. In the mean time, I hope these three posts have given you some ideas for how to handle navigation and async communications in your own applications.