Introduction
Recently, I have been working on a lot of Silverlight projects. The common thread between all of them is that they are all made up of a bunch of UserControls which navigate amongst themselves. For example, the first control would be Login.xaml, and once authenticated, it will need to navigate to the main application. Then the main application has a navigation section (like a menu etc.) and on the side of that will be the current control, which may need to navigate to another one. And so on.
So what has worked brilliantly for me is to create a UserControl which acts as a page controller and does all the transitioning and loading that needs to be done. You just pass it a UIElement
(which will be a UserControl in almost every circumstance), it loads it, and then replaces what is currently on screen.
Using the code
- Create a new Silverlight Application project or go to an existing one. All the navigation will be controlled by one
UserControl
. So add a new UserControl
called NavController.xaml to the project.
- NavController.xaml will have a main grid (
LayoutRoot
), then inside that will be a border (ControlContainer
), above that will be an image (ImageOverlay
) of the original control (more on that later), and then a gray overlay (GrayOverlay
) over that, and finally a swirly loader on top of all of this (Loader
).
The XAML for that - which you should put into NavController.xaml - looks like this:
<Grid x:Name="LayoutRoot">
<Border x:Name="ContentContainer"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
</Border>
<Image Opacity="0" x:Name="ImageOverlay"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" />
<Rectangle Opacity="0" x:Name="GrayOverlay"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch">
<Rectangle.Fill>
<RadialGradientBrush>
<GradientStop Color="#55000000" Offset="0" />
<GradientStop Color="#C5000000" Offset="1" />
</RadialGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Border Opacity="0" VerticalAlignment="Center"
HorizontalAlignment="Center" x:Name="Loader"></Border>
</Grid>
- Now go into NavController.cs. We need to make a method that accepts a
UIElement
and does all the work involved to get it on screen. Here's a breakdown of what will happen:
- The method gets passed a
UIElement
.
- We take a screenshot of what is currently on-screen and display that in the
ImageOverlay
control.
- Display the
ImageOverlay
, GrayOverlay
, and Loader
controls.
- Replace
ContentContainer.Child
with the new UIElement
.
- In the
Loaded
event of the new control, call another method in NavController
that hides the controls from step 3.
Now a quick explanation about steps 2 and 5: we could just replace the child of ContentContainer
with the new control without doing the image, but then the user would see the control changing (like a flicker) which won't look seamless at all. So we take a screenshot of it and display it, then the new control can do all its loading in the background. For example, the new control may call a Web Service and take a few seconds to load. That's fine, because once all the initial loading of that control is done, it will call the loading to stop. Or if there are no service calls, it can just set the loading to stop on the Loaded
event.
So first, we need to create the storyboards to display and hide those controls from step 3. I am doing a simple fade effect, but you can really do anything.
This will go just below the end of the grid above:
<UserControl.Resources>
<Storyboard x:Name="DisplayOverlays"
Completed="DisplayOverlays_Completed">
<DoubleAnimation To="1"
Storyboard.TargetName="ImageOverlay"
Storyboard.TargetProperty="Opacity"
Duration="00:00:01">
<DoubleAnimation.EasingFunction>
<SineEase />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation To="1"
Storyboard.TargetName="GrayOverlay"
Storyboard.TargetProperty="Opacity"
Duration="00:00:01">
<DoubleAnimation.EasingFunction>
<SineEase />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation To="1"
Storyboard.TargetName="Loader"
Storyboard.TargetProperty="Opacity"
Duration="00:00:01">
<DoubleAnimation.EasingFunction>
<SineEase />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<Storyboard x:Name="HideOverlays"
Completed="HideOverlays_Completed">
<DoubleAnimation To="0"
Storyboard.TargetName="ImageOverlay"
Storyboard.TargetProperty="Opacity"
Duration="00:00:01">
<DoubleAnimation.EasingFunction>
<SineEase />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation To="0"
Storyboard.TargetName="GrayOverlay"
Storyboard.TargetProperty="Opacity"
Duration="00:00:01">
<DoubleAnimation.EasingFunction>
<SineEase />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation To="0" Storyboard.TargetName="Loader"
Storyboard.TargetProperty="Opacity"
Duration="00:00:01">
<DoubleAnimation.EasingFunction>
<SineEase />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</UserControl.Resources>
The first thing we do in the method is to set a class variable to the control passed in so that we can use it once the display animation is complete. Next, we take a screenshot (that's not technically correct, but you get what I mean) of the ContentContainer
element using a WritableBitmap
. Then we set the source of the ImageOverlay
control to that image, then display all overlays (although their opacity is still 0 so they won't be seen), and finally start the animation to fade the overlays in.
private UIElement _waitingControl = null;
public void NavigateToControl(UIElement newControl)
{
_waitingControl = newControl;
WriteableBitmap bitmap = new WriteableBitmap(ContentContainer, null);
ImageOverlay.Source = bitmap; bitmap.Invalidate(); ImageOverlay.Visibility = System.Windows.Visibility.Visible;
GrayOverlay.Visibility = System.Windows.Visibility.Visible;
Loader.Visibility = System.Windows.Visibility.Visible;
DisplayOverlays.Begin();
}
And the method to hide the overlays (which will get called by the new control once finished doing what it needs to is pretty self-explanatory):
public void Hide()
{
HideOverlays.Begin();
}
When the fading in the animation of the overlays is complete and we have hidden the underlying control, then we can change it to the new control (the user will not see this because of the overlays):
private void DisplayOverlays_Completed(object sender, EventArgs e)
{
ContentContainer.Child = _waitingControl;
}
And once the overlays have been hidden (in terms of their opacity), we need to collapse them. This is because even though they are transparent, you will not be able to click through them.
private void HideOverlays_Completed(object sender, EventArgs e)
{
ImageOverlay.Visibility = System.Windows.Visibility.Collapsed;
GrayOverlay.Visibility = System.Windows.Visibility.Collapsed;
Loader.Visibility = System.Windows.Visibility.Collapsed;
}
- Okay, now that the actual control is basically done, we need a way for any control to be able to call its parent
NavController
. For that, we will have a global static class that has some clever methods in it. Add a new class called Global.cs. In that, put this:
public static class Global
{
public static T FindParent<T>(UIElement control) where T : UIElement
{
UIElement p = VisualTreeHelper.GetParent(control) as UIElement;
if (p != null)
{
if (p is T)
return p as T;
else
return FindParent<T>(p);
}
return null;
}
public static void ChangeControl(this UIElement uc, UIElement newControl)
{
NavController controller = FindParent<NavController>(uc);
if (controller != null)
controller.NavigateToControl(newControl);
}
public static void HideLoading(this UIElement uc)
{
NavController controller = FindParent<NavController>(uc);
if (controller != null)
controller.Hide();
}
}
The first part is a very useful helper method which will navigate up the visual tree and return the first instance of the type. So we will use it so that any control can find the first NavController
that it is inside. The two methods after that are to make it easier to stop the loading or navigate to a new control from any UIElement
. Notice the this
in the argument list? That means that on any UIElement
, we can now do: myElement.HideLoading();
. Cool, huh? Keep in mind that these two methods call their counterpart in the NavController
, but must not be the same name, else you will cause an endless loop and possibly destroy the universe.
- We are getting closer to having it working! But first, we need to set our new
NavController
to be the first control that gets loaded (and then things will load inside of that). So open App.cs and find the Application_Startup
method. Replace the current line in there with this:
this.RootVisual = new NavController();
(this.RootVisual as NavController).NavigateToControl(new Login());
The first line just makes the first control that gets loaded, the NavController
. Then the next line runs the method that tells it to change to a new control. In this case, I have just created a UserControl
called Login
which would be the login.
OK, now as mentioned earlier, every control that gets loaded this way must tell the NavController
to stop the loading when it is done. In the case of Login
, that would be when the control has finished loading like this:
public Login()
{
InitializeComponent();
this.Loaded += (se, ev) =>
{
this.HideLoading();
};
}
And then when the user clicks the button, it can switch to a new control (in this case, MainPage
) like this:
private void Button_Click(object sender, RoutedEventArgs e)
{
this.ChangeControl(new MainPage());
}
In Closing
The download contains what is explained above, plus a few controls to explain it better. It also shows how you can have a NavController
inside another. I will do another post soon about making a swirly loader - but you can put anything you want in the Loader
control. You can try out the solution below.
Note
I would appreciate comments - but keep in mind that this is my first article, so don't be too mean :P
You can view the original post and view a demo over at my blog here: RogueCode.
History
- 2/18/2011 - Article written.