Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Prism for Silverlight/MEF in Easy Samples. Part 2 - Prism Navigation

0.00/5 (No votes)
21 Mar 2011 1  
Prism for Silverlight/MEF in Easy Samples tutorial. Part 2 - Prism Navigation

Download PrismTutorialSamples_PART2.zip - 285.46 KB

Introduction

This is a Part 2 of Prism for Silverlight/MEF in Easy Samples Tutorial. Part 1 can be accessed at Prism for Silverlight/MEF in Easy Samples. Part 1 - Prism Modules and Part 3 - at Prism for Silverlight/MEF in Easy Samples. Part 3 - Communication between the Modules

In this part, I cover Prism Region Navigation, which allows changing a view displayed or active within a Prism Region. (For region definition and samples look at the first part of this tutorial).

Region navigation is essential for properly using the "real estate" of the application by changing the views displayed at different locations within the application screen depending on the current state of the application.

Region Navigation functionality within Prism allows loading a view within a region based on the view's class name, passing navigation parameters, making decisions whether the view should be displayed or not, cancelling the navigation if needed and recording the navigation operations.

On top of the prerequisites listed for "Part 1" (C#, MEF and Silverlight), one also needs to understand the MVVM pattern for some of the samples in "Part 2".

Region Navigation Overview and Samples

Simple Region Navigation Sample

This sample's code is located under SimpleRegionNavigation.sln solution. It demonstrates basic navigation functionality. Module1 of the sample application has two views: Module1View1 and Module1View2. Both views are registered with "MyRegion1" region within the Module1Impl.Initialize method (view discovery is used to associate these views with the region):

        public void Initialize()
        {
            TheRegionManager.RegisterViewWithRegion
            (
                "MyRegion1", 
                typeof(Module1View1)
            );
            
            TheRegionManager.RegisterViewWithRegion
            (
                 "MyRegion1", 
                 typeof(Module1View2)
            );
        }

Both views display a message (a TextBlock specifying the name of the view) and a button to switch to the other view. The foreground of Module1View1 is red, while the foreground of Module1View2 is green.

There are two ways to navigate to a view within a region:

  1. By using RegionManager's RequestNavigate method.
  2. By using Region's RequestNavigate method.
These two methods are very similar, except that RegionManager.RequestNavigate function requires region name as its first parameter, while Region's method does not require it.

To demonstrate both methods, Module1View1 navigates to Module1View2 using RegionManager.RequestNavigate function:

            TheRegionManager.RequestNavigate
            (
                "MyRegion1",
                new Uri("Module1View2", UriKind.Relative),
                PostNavigationCallback
            );

while Module1View2 navigates to Module1View1 using Region.RequestNavigate:

            _region1.RequestNavigate
            (
                new Uri("Module1View1", UriKind.Relative),
                PostNavigationCallback
            );

One can see that both methods require a relative Uri argument, whose string matches the name of the view we want to navigate to.

Both methods also need a post-navigation callback as their last argument. This is because some navigation implementations might be asynchronous so, if you want some code for sure to be executed after the end of the navigation you put it within the post-navigation callback. In our case we want to display a modular MessageBox stating whether the navigation was successful or not:

   
        void PostNavigationCallback(NavigationResult navigationResult)
        {
            if (navigationResult.Result == true)
                MessageBox.Show("Navigation Successful");
            else
                MessageBox.Show("Navigation Failed");
        }

Many times, however, the post-navigation callback is not needed, and then the following simple lambda can be placed in its stead:

a => { }

Up to now we've covered the code behind for Module1View1 view located within Module1View1.xaml.cs file. Module1View2 functionality, however, is more complex. We can see, that Module1View2 class implements IConfirmNavigationRequest interface. IConfirmNavigationRequest extends INavigationAware interface.

INavigationAware interface has three methods:

        bool IsNavigationTarget(NavigationContext navigationContext);
        void OnNavigatedFrom(NavigationContext navigationContext);
        void OnNavigatedTo(NavigationContext navigationContext);

IsNavigationTarget method allows the navigation target to declare that it does not want to be navigated to, by returning "false". This method is most useful with injected views as will be shown below.

OnNavigatedFrom method is called before one navigates away from a view. It allows doing any pre-navigation processing within the view that has been navigated from. In case of Module1View2 we show a message box:

        public void OnNavigatedFrom(NavigationContext navigationContext)
        {
            MessageBox.Show("We are within Module1View2.OnNavigatedFrom");
        }

OnNavigatedTo method is called after one navigates to the view. It allows doing any post-navigation processing within the view that has been navigated to. In case of Module1View2 a message box is shown:

        public void OnNavigatedTo(NavigationContext navigationContext)
        {
            MessageBox.Show("We are within Module1View2.OnNavigatedTo");
        }

The above methods IsNavigationTarget, OnNavigatedFrom and OnNavigatedTo are part of INavigationAware interface (which IConfirmNavigationRequest extends) and can be used if the view class implements INavigationAware only. The method ConfirmNavigationRequest, however, is only part of IConfirmNavigationRequest and in order to use it one needs to implement IConfirmNavigationRequest interface.

ConfirmNavigationRequest method is called before one navigates from the current view. It gives you a last chance to cancel the navigation. Its second argument is a delegate called "continuationCallback" that takes a Boolean value as its argument. Calling continuationCallback(true) will allow the navigation to continue, while calling continuationCallback(false) will cancel it.

Within Module1View2 we show a message box, asking whether the user wants to continue navigation or cancel it and the argument to "continuationCallback" is determined by whether the user pressed OK or Cancel button:

        
        public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
        {
            MessageBoxResult messageBoxResult =
                MessageBox.Show("Should Navigate from Current View?", "Navigate", MessageBoxButton.OKCancel);

            bool shouldNavigateFromCurrentView = messageBoxResult.HasFlag(MessageBoxResult.OK);

            continuationCallback(shouldNavigateFromCurrentView);
        }

Even though we use a modular window that blocks the control flow until the user clicks a button, non-modular control can also be used to continue or cancel the navigation - we simply need to pass the "continuationCallback" parameter to it and based on the user action it should either call continuationCallback(true) or continuationCallback(false).

This is how the application looks when it is started:

Once "Navigate to Next View" button is clicked, we get a popup indicating that we are inside Module1View2.OnNavigatedTo function. After OK is clicked, we are getting the post-navigate callback message indicating that the navigation has been successful. Once OK is clicked, the new view is shown on the screen:

If we click "Navigate to Next View" button now, we'll hit the Module1View2.ConfirmNavigationRequest method first, which will show us a popup window asking whether to continue navigation or cancel it. If we press ok, navigation to Module1View1 continues successfully, otherwise it fails and we stay at Module1View2. (In case of successful navigation we get two more modular popups - one indicating that we are inside Module1View2.OnNavigatedFrom function and one informing us that the navigation was successful, while in case of a navigation failure we are informed of it by a message popup)

Exercise: create a similar demo (look at "Prism for Silverlight/MEF in Easy Samples Tutorial Part1" for instructions on how to create projects for Silverlight Prism application and modules and how to load the module into the application using the bootstrapper).

Exercise: create a similar demo with the two views located in different modules instead of them being within the same module. (Since several people had difficulty with this exercise, I added another sample illustrating navigation between two views located in different modules: NavigationBetweenTwoDifferentModules.zip).

Simple Navigation via the View Model

In the above example, Module1View2 view implements IConfirmNavigationRequest interface and because of that its functions IsNavigationTarget, OnNavigatedFrom, OnNavigatedTo and ConfirmNavigationRequest participate in the navigation process. A better way, however, is to make all the navigation decisions within the view model (if MVVM pattern is being followed). So, Prism provides a way for the view model to implement INavigationAware or IConfirmNavigationRequest interfaces (instead of the view).

The rule is the following: if the view implements one of INavigationAware or IConfirmNavigationRequest interfaces than the view's corresponding methods will be called during the navigation. If, however, the view does not implement it, but the view's DataContext does, then the corresponding functions of the DataContext will be involved as demonstrated by our sample under NavigationViaViewModel.sln solution.

Module1View1 is almost the same as in the previous sample (aside from the fact that it does not popup message windows).

Module1View2 does not implement IConfirmNavigationRequest. Instead, within the constructor, it sets its DataContext property to be a new object of View2Model type:

            // we are setting the DataContext property of
            // Module1View2 view
            View2Model view2Model = new View2Model();
            DataContext = view2Model;

View2Model is a non-visual (as a view model should be) class that implements IConfirmNavigationRequest:

    
    public class View2Model : IConfirmNavigationRequest
    {
        // we use this event to communicate to the view
        // that we are starting to navigate from it
        public event Func<bool> ShouldNavigateFromCurrentViewEvent;

        #region INavigationAware Members

        public bool IsNavigationTarget(NavigationContext navigationContext)
        {
            return true;
        }

        public void OnNavigatedTo(NavigationContext navigationContext)
        {
           
        }

        public void OnNavigatedFrom(NavigationContext navigationContext)
        {
            
        }

        #endregion

        #region IConfirmNavigationRequest Members

        public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
        {
            bool shouldNavigateFromCurrentViewFlag = false;

            if (ShouldNavigateFromCurrentViewEvent != null)
                shouldNavigateFromCurrentViewFlag = ShouldNavigateFromCurrentViewEvent();

            continuationCallback(shouldNavigateFromCurrentViewFlag);
        }

        #endregion
    }

We use the view model's ShouldNavigateFromCurrentViewEvent event to inform the view that navigation has been started away from it. The view connects a handler to this event within the view's constructor:

     view2Model.ShouldNavigateFromCurrentViewEvent += 
                new Func<bool>(view2Model_ShouldNavigateFromCurrentViewEvent);

Just like in the previous sample, View2Model.view2Model_ShouldNavigateFromCurrentViewEvent displays a modular popup window asking whether the user wants to continue or cancel the navigation. Then, depending on the user choice it returns a Boolean to the view model and the View2Model.ConfirmNavigationRequest's "continuationCallback" argument is called with the corresponding value specifying whether to continue or cancel the navigation.

Exercise: create a similar demo.

Navigation Parameters Sample

If a view or a view model implements INavigationAware or IConfirmNavigationRequest, interface, the navigation can pass parameters to these functions as part of the navigation Url. As I mentioned above, the Url should contain the name of the view class to which one wants to navigate. This name of the class can be followed by '?' character and key-value parameter pairs where the keys are separated from the values by '=' character and the pairs are separated from each other by '&' character.

NavigationParameters.sln solution contains a sample where the view Module1View1 navigates to itself but with different parameters.

Here is how the sample's window looks:

One can enter any text into the text box above, then press the button "Copy via Navigation" and the text will be copied into the text block below.

The "Copy via Navigation" has the following callback:

        void NextViewButton_Click(object sender, RoutedEventArgs e)
        {
            if (TheTextToCopyTextBox.Text == null)
                TheTextToCopyTextBox.Text = "";

            // navigate passing the paramter "TheText" 
            TheRegionManager.RequestNavigate
            (
                "MyRegion1",
                new Uri("Module1View1?TheText=" + TheTextToCopyTextBox.Text, UriKind.Relative),
                a => { }
            );
        }

Note that the full Url will look like: "Module1View1?TheText=<text-to-copy>".

Module1View1 view implements INavigationAware interface, so, its own OnNavigatedTo function will be called at the end of the navigation process. Within this function we get the UriQuery dictionary from the navigationContext argument and from the UriQuery dictionary we get the value corresponging to "TheText" key as below:

        public void OnNavigatedTo(NavigationContext navigationContext)
        {
            TheCopiedTextTextBlock.Text = navigationContext.UriQuery["TheText"];
        }

Of course, in reality, RequestNavigate and OnNavigatedTo functions can be in different views and even in different modules, and instead of a trivial task of copying text, other much more complex tasks requiring navigation parameters might be employed.

Exercise: Create a similar demo.

Using IsNavigationTarget Method for Navigating to Injected Views

In case of the view injection there can be multiple views of the same view type within an application. Navigating based on the Url containing the View class name only, will not work in that case, since all those views have the same view class. Prism employs bool INavigationAware.IsNavigationTarget(NavigationContext) method to specify which view to navigate to in such case.

When navigating to injected views, each of the region's views of the specified type has its INavigationAware.IsNavigationTarget method called until one of them returns "true" (or until all of them return "false"). The view whose INavigationAware.IsNavigationTarget method returns "true" will be navigated to.

The corresponding sample is located under InjectedViewNavigation.sln solution. Unlike in previous examples, the region control is represented by a ListBox in the application's Shell.xaml file:

        <ListBox HorizontalAlignment="Center"
                 VerticalAlignment="Center"
                 prism:RegionManager.RegionName="MyRegion1">
        </ListBox>

so that it displays every view connected to the region and navigating to a view makes this view selected within the list box:

The view is defined by Module1View1 class. It has ViewID property that uniquely specifies the view object. This class also has a static method CreateViews() which creates 5 views with different ViewIDs ranging from 1 to 5 and associates the created views with "MyRegion1" region:

        public static void CreateViews(IRegionManager regionManager)
        {
            IRegion region = regionManager.Regions["MyRegion1"];

            for (int i = 1; i <= MAX_VIEW_ID; i++)
            {
                Module1View1 view = new Module1View1 { ViewID = i };
                view.TheRegionManager = regionManager;
                region.Add(view);
            }
        }

Each view has a button to navigate to the next view. The button click has the following event handler:

        void NextViewButton_Click(object sender, RoutedEventArgs e)
        {
            NextViewButton.IsEnabled = false;
            TheRegionManager.RequestNavigate
            (
                "MyRegion1",
                new Uri("Module1View1?ViewID=" + NextViewID, UriKind.Relative),
                a => { }
            );
        }

As you can see, we navigate to the "Module1View1" view passing "ViewID" parameter set to point to a value returned by NextViewID property.

NextViewID returns the ViewID of the current view plus one unless it is the last view in the set - then it goes back to ViewID=1:

        int NextViewID
        {
            get
            {
                if (ViewID < Module1View1.MAX_VIEW_ID)
                    return ViewID + 1;

                return 1;
            }
        }

IsNavigationTarget method compares the "ViewID" parameter received as part of the Navigation Url to the ViewID property of the view. If they are the same, it returns true (this means we navigate to this view), if they are different, it returns false (the view is not navigated to):

        public bool IsNavigationTarget(NavigationContext navigationContext)
        {
            string viewIDString = navigationContext.UriQuery["ViewID"];

            int viewID = Int32.Parse(viewIDString);

            bool canNavigateToThisView = (viewID == ViewID);

            return canNavigateToThisView;
        }

One can see that by pushing the view's "Navigate to Next View" button, we indeed switch to the next view. After changing NextViewID property to return every second view, we can see that every second view in the list gets selected. If we change the region control within Shell.xaml file of the application project to be a ContentControl instead of a ListBox the navigation will be displaying the navigated view instead of selecting it.

Exercise: Create a similar demo.

Undo-Redo Functionality Using Navigation Journal

Navigation functionality also includes undo-redo capabilities as shown in this sample. The sample is located under NavigationJournal.sln solution.

This sample is very similar to the previous one, only each injected view has two more buttons: "Back" button and "Forward" button. "Back" button allows undoing the last operation, while "Forward" - redoing the last undone operation:

"Back" and "Forward" buttons are enabled only when the corresponding operations are possible.

Undo-redo functionality is available from RegionNavigationJournal, which in turn can be accessed via RegionNavigationService. We can get a reference to the navigation service from the navigationContext argument within OnNavigatedTo method:

        public void OnNavigatedTo(NavigationContext navigationContext)
        {
            // getting a reference to navigation service:
            if (_navigationService == null)
                _navigationService = navigationContext.NavigationService;

            ...
        }

Here is how "Back" button hooks to the corresponding Undo functionality:

        void BackButton_Click(object sender, RoutedEventArgs e)
        {
            _navigationService.Journal.GoBack();
         
            ...
        }

"Forward" button's handler is calling _navigationService.Journal.GoForward() method instead.

Button enabling/disabling functionality is based on _navigationService.Journal.CanGoBack and _navigationService.Journal.CanGoForward properties for "Back" and "Forward" buttons correspondingly. It seems that it is logical to add button enabling/disabling functionality to the body of OnNavigatedTo method (which is called on Undo-Redo), but unfortunately it is called before the navigation Journal state is updated, so that the CanGoBack and CanGoForward properties do not have the correct values yet. In order to get around this problem, I had to figure out which view is current based on the Uri property of the current entry within the Journal. We get the ViewID from the Uri and set IsEnabled properties on the "Back" and "Forward" buttons of the corresponding view:

       void UpdateIfCurrentView()
        {
            if (_navigationService == null)
                return;

            string uriStr = _navigationService.Journal.CurrentEntry.Uri.OriginalString;

            int lastEqualIdx = uriStr.LastIndexOf('=');

            string viewIDStr = uriStr.Substring(lastEqualIdx + 1);

            int viewID = Int32.Parse(viewIDStr);

            if (viewID != this.ViewID)
                return;

            BackButton.IsEnabled = _navigationService.Journal.CanGoBack;
            ForwardButton.IsEnabled = _navigationService.Journal.CanGoForward;
        }

Method UpdateIfCurrentView is set as the static JournalUpdatedEvent event handler for each of the created views:

        public Module1View1()
        {
            InitializeComponent();

            ...

            JournalUpdatedEvent += UpdateIfCurrentView;

            ...

        }

"Back" and "Forward" button click handlers call JournalUpdatedEvent after calling _navigationService.Journal.GoBack() or _navigationService.Journal.GoForward() methods:

        void BackButton_Click(object sender, RoutedEventArgs e)
        {
            _navigationService.Journal.GoBack();
            DisableButtons();

            JournalUpdatedEvent();
        }

        void ForwardButton_Click(object sender, RoutedEventArgs e)
        {
            _navigationService.Journal.GoForward();

            DisableButtons();

            JournalUpdatedEvent();
        }

Important Note: the journal undo/redo functionality works only within one region. If there are multiple regions each one of them will have their own undo/redo sequence.

Exercise: create a similar demo.

Conclusion

In part 2 of this tutorial I gave a detailed description of Prism's Navigation functionality in small and simple samples. I'd appreciate very much if you could vote for this article and leave a comment. I would love to hear your suggestions for improving and expanding the content of this tutorial.

History

Feb 15, 2011 - changed the code to use latest Prism dlls (which lead to significant changes in API: INavigationAwareWithVeto interface was replaced by IConfirmNavigationRequest, CanNavigateTo() was replaced by IsNavigationTarget() and method OnNavigatedFrom was added to INavigationAware interface. Hat tip to jh1111 for noticing that my API was out of date.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here