Tuesday, 5 November 2013

Writing a custom Blend interaction to perform page navigation in Windows Store apps

Previously I wrote about replacing an attached behaviour that was used to execute a view model action, with a Blend behaviour that executes a view model command. The advantage of this approach is that you use what’s provided by the Blend SDK, rather than having to write your own attached behaviour, which may introduce errors, and associated unit tests.

I then updated the code as it was not necessary to pass a CommandParameter to the view model command. This is because the EventTriggerBehavior passes the associated EventArgs (in this case the ItemClickEventArgs) to the embedded action through the Execute method of the IAction. An action method signature takes the form shown in the following code example.

object IAction.Execute(object sender, object parameter)
{
    ...
}

Here, the sender will be the control to which the parent behaviour is attached (in my case a GridView), with the parameter receiving the ItemClickEventArgs from the EventTriggerBehavior. Because in my sample app a generic DelegateCommand is used for the view model command, the ItemClickEventArgs are passed from the action to the generic DelegateCommand. The problem then is that the view model has a dependency on the ItemClickEventArgs type, which is a UI specific concept. From a purist point of view for MVVM, this is unacceptable. In this blog post I’ll address this issue in order to remove the UI dependency from the view model.

Implementation

My previous post used the InvokeCommandAction interaction to execute a DelegateCommand in the MainPageViewModel class. In response the DelegateCommand executes a method which navigates to the PhotoPage. An alternative approach is to use the NavigateToPageAction interaction to perform the navigation directly. Not only does this remove the dependency on the ItemClickEventArgs type from the MainPageViewModel class, it also removes the DelegateCommand, the NavigateToPhoto method, and the need for the view model to have the NavigationService injected. The declaration of the interaction on MainPage then becomes as shown in the following code example.

<GridView Grid.Row="1"
          IsItemClickEnabled="True"
          ItemsSource="{Binding Photos}"
          Margin="140,0,0,0"
          SelectionMode="None">
    <interactivity:Interaction.Behaviors>
        <core:EventTriggerBehavior EventName="ItemClick">
            <core:NavigateToPageAction TargetPage="PhotoViewer.Views.PhotoPage" />
        </core:EventTriggerBehavior>
    </interactivity:Interaction.Behaviors>
    ...
</GridView>

An EventTriggerBehavior is used which handles the ItemClick event. The NavigateToPageAction specifies the page that will be navigated to through the TargetPage property. The overall effect is that when the ItemClick event fires on the GridView, the PhotoPage will be navigated to. In the PhotoPageViewModel class the OnNavigatedTo method can receive a navigation parameter from the NavigateToPageAction interaction, through the Parameter property.

The problem then becomes passing the clicked item from the GridView as the navigation parameter, as the GridView doesn’t have a property that represents the clicked item. The most suitable property is the SelectedItem property of the GridView. However, this is incorrect as the GridView doesn’t have selection turned on, and even if it did item selection occurs with the mouse by right clicking. Since there isn’t a SelectedItem, it can’t be bound to.

In addition, unlike the InvokeCommandAction interaction, the NavigateToPageAction interaction does not pass the EventArgs from the EventTriggerBehavior to its embedded action through the Execute method of the IAction. Therefore, it’s not currently possible to use the NavigateToPageAction interaction to perform navigation in this scenario and pass the required parameter type through to the OnNavigatedTo method of a view model class. Therefore, the solution then becomes to write a new interaction to do this.

Such an interaction has been developed as part of the Prism for the Windows Runtime project. The interaction is used by the AdventureWorks Shopper reference implementation, and is named NavigateWithEventArgsToPageAction. The latest drop of this project, which contains the interaction, can be downloaded here. To create a new action you must create a class that derives from the DependencyObject class, and implements the IAction interface. The IAction interface has only one method that needs to be implemented, named Execute.

public class NavigateWithEventArgsToPageAction : DependencyObject, IAction
{
    public string TargetPage { get; set; }
    public string EventArgsParameterPath { get; set; }
    object IAction.Execute(object sender, object parameter)
    {
        var propertyPathParts = EventArgsParameterPath.Split('.');
        object propertyValue = parameter;
        foreach (var propertyPathPart in propertyPathParts)
        {
            var propInfo = propertyValue.GetType().GetTypeInfo().GetDeclaredProperty(propertyPathPart);
            propertyValue = propInfo.GetValue(propertyValue);
        }
 
        var pageType = Type.GetType(TargetPage);
        var frame = GetFrame(sender as DependencyObject);
 
        return frame.Navigate(pageType, propertyValue);
    }
 
    private Frame GetFrame(DependencyObject dependencyObject)
    {
        var parent = VisualTreeHelper.GetParent(dependencyObject);
        var parentFrame = parent as Frame;
        if (parentFrame != null) return parentFrame;
        return GetFrame(parent);
    }
}

Here, the Execute method traverses the visual tree to obtain the Frame control used by the current page, and then calls its Navigate method to navigate to the target page, passing in the specified parameter. The overall effect of this interaction is to invoke navigation to a specified page, while allowing the event arguments to be passed as a parameter to the page being navigated to.

The NavigateWithEventArgsToPageAction interaction can then be invoked like any other Blend behaviour.

<GridView Grid.Row="1"
          IsItemClickEnabled="True"
          ItemsSource="{Binding Photos}"
          Margin="140,0,0,0"
          SelectionMode="None">
    <interactivity:Interaction.Behaviors>
        <core:EventTriggerBehavior EventName="ItemClick">
            <behaviours:NavigateWithEventArgsToPageAction TargetPage="PhotoViewer.Views.PhotoPage"
                                                          EventArgsParameterPath="ClickedItem.Path" />
        </core:EventTriggerBehavior>
    </interactivity:Interaction.Behaviors>
    ...
</GridView>

An EventTriggerBehavior is used which handles the ItemClick event. The custom NavigateWithEventArgsToPageAction interaction specifies the page that will be navigated to through the TargetPage property, with the navigation parameter being specified by the EventArgsParameterPath property. The overall effect is that when the ItemClick event fires on the GridView, the PhotoPage will be navigated to. In the PhotoPageViewModel class the OnNavigatedTo method will receive the value of the EventArgsParameterPath from the NavigateWithEventArgsToPageAction interaction.

The EventArgs associated with the ItemClick event on the GridView are of type ItemClickEventArgs. The ClickedItem property, specified in the EventArgsParameterPath property of the NavigateWithEventArgsToPageAction interaction, will be of the underlying type, which in this case is Photo (the Photo type is simply a wrapper around the FileInformation object). The Path property of the Photo object simply returns the Path property of the wrapped FileInformation object, which is a string containing the path of the associated file. It is this string that will be received by the OnNavigatedTo method in the PhotoPageViewModel class.

public async override void OnNavigatedTo(object navigationParameter, NavigationMode navigationMode, Dictionary<string, object> viewModelState)
{
    base.OnNavigatedTo(navigationParameter, navigationMode, viewModelState);
    _filePath = navigationParameter as string;
   ...
}

Once the OnNavigatedTo method receives the path of the file, it can be loaded and displayed by the page. The advantage of using the custom NavigateWithEventArgsToPageAction interaction is that it removes any UI dependencies on EventArgs from view models.

Summary

In this blog post I’ve shown the custom NavigateWithEventArgsToPageAction interaction that can be used to invoke page navigation, while passing the EventArgs of the parent behaviour to the embedded action through the Execute method of the IAction. In a perfect world this solution would have been possible by using the NavigateToPageAction interaction. However, this is currently not the case. The advantage of the approach shown here is that it removes any EventArgs UI dependencies from view models.

The sample app can be downloaded here.

2 comments:

  1. Hi, and thanks for your post. This is very informative. I would like to extend the NavigateWithEventArgsToPageAction feature a little further so that the ItemClicked provides the TargetPage as well. With the introduction of the "new rule" that AppBar can't be added to the top of a AppStore page (or even more importantly, wouldn't translate to a universal app well) I am changing the Hub Page to include a HubSection containing the navigation options in a grid view. Each item in the Grid contains the "NavigationPageToken" and "NavigationParameter" allowing the item to go to different pages within the app. This also translates to 8.1 Phone navigation so think it's a viable alternative to the legacy "Navigation Menu".
    My issue, is that I don't seem to be able to bind to the TargetPage, is that because it's not a Dependency Property? Or do you see a better way to do this?

    ReplyDelete
  2. I'd imagine that you don't need to bind to the TargetPage property. Instead you could use reflection to get at the object behind the ClickedItem property, allowing you to then access the NavigationPageToken and NavigationParameter properties, before invoking navigation as in the code above. Hope this helps.

    ReplyDelete