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:
- By using
RegionManager's
RequestNavigate method.
- 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:
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
{
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 = "";
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)
{
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.