Click here to Skip to main content
Click here to Skip to main content

Robust UserControl Navigation in Silverlight

, 18 Feb 2011
Rate this:
Please Sign up or sign in to vote.
A simple and very effective way to manage navigation between UserControls in a Silverlight project.

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

  1. 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.
  2. 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).
  3. structure.png

    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>
  4. 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:
    1. The method gets passed a UIElement.
    2. We take a screenshot of what is currently on-screen and display that in the ImageOverlay control.
    3. Display the ImageOverlay, GrayOverlay, and Loader controls.
    4. Replace ContentContainer.Child with the new UIElement.
    5. 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;
        //take screenshot
        WriteableBitmap bitmap = new WriteableBitmap(ContentContainer, null);
        ImageOverlay.Source = bitmap;//set the source of the image control
        bitmap.Invalidate();//kill the bitmap
        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();
        //start hiding all the overlays to display the new control
    }

    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;
        // change the control to the new one
    }

    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;
    }
  5. 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:
  6. 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.

  7. 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:
  8. 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)
    {
        //check login etc
        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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Matt Cavanagh
Software Developer self employed
South Africa South Africa
I develop awesome Windows Phone/Windows 8 stuff, am a Nokia Developer Champion, I do Netduino electronics stuff, and blog a lot. I also occasionally do talks about development at Universities and conferences like TechEd. I run a small indie Windows Phone studio, currently working on an AppCampus-funded game.
 
Checkout my just-for-fun apps here: http://www.windowsphone.com/en-US/store/publishers?publisherId=RogueCode&appId=23d742d2-5b14-48a7-8e5f-b3b779537338
I also do Windows Phone (and Windows) development for clients, for example: http://www.windowsphone.com/en-za/store/app/dstv/a87feeed-a8dd-4bcb-8d47-15908340fdab
 
I am currently on hiatus from writing development articles for WPCentral.com.
 
My first book has just been published on home automation with a Netduino: http://www.amazon.co.uk/Netduino-Home-Automation-Projects-Cavanagh/dp/1849697825
Follow on   Twitter

Comments and Discussions

 
GeneralMy vote of 5 PinmemberClodetta del Mar13-Mar-12 2:33 
Generalthanks for sharing - have 5 PinmemberPranay Rana21-Feb-11 2:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140821.2 | Last Updated 18 Feb 2011
Article Copyright 2011 by Matt Cavanagh
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid