Testable Navigation with MVVM through Behaviors on Windows Phone 8

Testable Navigation with MVVM through Behaviors on Windows Phone 8

Navigating is a concern of the View and should be executed there. Never the less the business logic leading to that execution shall be done in the ViewModel, because it needs to be tested properly.


We'll use a thing Gregor Biswanger (MVP Client App Dev) (http://webenliven-space.de/dotnetblog/) once told me about his solution to the problem - Behaviors.

A behavior is a  ViewModel Event that is being subscribed in the View's CodeBehind. The business logic in the ViewModel composes the URI e.g. "/Views/DetailsView.xaml?id=123" that shall be navigated to by the NavigationService in the CodeBehind. As soon as the Method in the ViewModel is called, the Event fires and the CodeBehind recieves the URI, navigates to the DetailsView and passes the parameter "id=123" via the URI string.

Step 1



Create a new Windows Phone 8 App





Step 2



Create a new folder "ViewModels" and a new class "MainViewModel.cs" in that folder.





Make that class public

using System;
using System.Linq;
 
namespace NavigationWithBehaviorsAndMVVM.ViewModels
{
    public class MainViewModel
    {
    }
}

Step 3





Implement the "INofityPropertyChanged" Interface, implement the required members and the event invocator.

using System;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
 
namespace NavigationWithBehaviorsAndMVVM.ViewModels
{
    /// <summary>
    /// ViewModel for the MainPage.xaml
    /// </summary>
    public class MainViewModel : INotifyPropertyChanged
    {
        private string _title;
 
        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
 
        /// <summary>
        /// Gets or sets the title.
        /// </summary>
        /// <value>The title.</value>
        public string Title
        {
            get
            {
                return _title;
            }
            set
            {
                _title = value;
                NotifyPropertyChanged("Title");
            }
        }
 
        /// <summary>
        /// Notifies the property changed.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        private void NotifyPropertyChanged([CallerMemberName]
                                           String propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

Step 4



Create the "DetailsPage.xaml" and the "DetailsViewModel.cs"









Step 5



Repeat Step 4 for the "DetailsViewModel.cs"

using System;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
 
namespace NavigationWithBehaviorsAndMVVM.ViewModels
{
    public class DetailsViewModel : INotifyPropertyChanged
    {
        private string _title;
 
        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
 
        /// <summary>
        /// Gets or sets the title.
        /// </summary>
        /// <value>The title.</value>
        public string Title
        {
            get
            {
                return _title;
            }
            set
            {
                _title = value;
                NotifyPropertyChanged("Title");
            }
        }
 
        /// <summary>
        /// Notifies the property changed.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        private void NotifyPropertyChanged([CallerMemberName]
                                           String propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

Step 6



Create an "event Action<string>" an invocator for it and a method that calls the event in the "MainViewModel.cs".

using System;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
 
namespace NavigationWithBehaviorsAndMVVM.ViewModels
{
    /// <summary>
    /// ViewModel for the MainPage.xaml
    /// </summary>
    public class MainViewModel : INotifyPropertyChanged
    {
        private string _title;
 
        /// <summary>
        /// Occurs when [go to details page].
        /// </summary>
        public event Action<string> GoToDetailsPage;
 
        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
 
        /// <summary>
        /// Gets or sets the title.
        /// </summary>
        /// <value>The title.</value>
        public string Title
        {
            get
            {
                return _title;
            }
            set
            {
                _title = value;
                NotifyPropertyChanged("Title");
            }
        }
 
        /// <summary>
        /// Nexts the page.
        /// </summary>
        public void DetailsPage()
        {
            OnGoToDetailsPage("/DetailsPage.xaml?id=123");
        }
 
        /// <summary>
        /// Called when [go to details page].
        /// </summary>
        /// <param name="message">The message.</param>
        protected virtual void OnGoToDetailsPage(string message)
        {
            Action<string> handler = GoToDetailsPage;
            if (handler != null)
                handler(message);
        }
 
        /// <summary>
        /// Notifies the property changed.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        private void NotifyPropertyChanged([CallerMemberName]
                                           String propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

Step 7



Set the DataContext in the MainPage.xaml and bind the applicationtitle to the Title property.

<phone:PhoneApplicationPage
    x:Class="NavigationWithBehaviorsAndMVVM.MainPage"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:viewModels="clr-namespace:NavigationWithBehaviorsAndMVVM.ViewModels"
    mc:Ignorable="d"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    shell:SystemTray.IsVisible="True">
 
    <phone:PhoneApplicationPage.DataContext>
        <viewModels:MainViewModel />
    </phone:PhoneApplicationPage.DataContext>
     
    <!--LayoutRoot is the root grid where all page content is placed-->
    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
 
 
        <!--TitlePanel contains the name of the application and page title-->
        <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
            <TextBlock Text="{Binding Title}" Style="{StaticResource PhoneTextNormalStyle}" Margin="12,0"/>
            <TextBlock Text="MainPage" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
        </StackPanel>
 
        <!--ContentPanel - place additional content here-->
        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
             
        </Grid>
    </Grid>
 
</phone:PhoneApplicationPage>

Step 8



Repeat Step 7 for the "DetailsView.xaml"

<phone:PhoneApplicationPage
    x:Class="NavigationWithBehaviorsAndMVVM.DetailsPage"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:viewModels="clr-namespace:NavigationWithBehaviorsAndMVVM.ViewModels"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    mc:Ignorable="d"
    shell:SystemTray.IsVisible="True">
 
    <phone:PhoneApplicationPage.DataContext>
        <viewModels:DetailsViewModel />
    </phone:PhoneApplicationPage.DataContext>
     
    <!--LayoutRoot is the root grid where all page content is placed-->
    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
 
        <!--TitlePanel contains the name of the application and page title-->
        <StackPanel Grid.Row="0" Margin="12,17,0,28">
            <TextBlock Text="{Binding Title}" Style="{StaticResource PhoneTextNormalStyle}"/>
            <TextBlock Text="page name" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
        </StackPanel>
 
        <!--ContentPanel - place additional content here-->
        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
 
        </Grid>
    </Grid>
 
</phone:PhoneApplicationPage>

Step 9



Get the "MainViewModel" and subscribe to the event in the "MainPage.xaml.cs" CodeBehind.

using System;
using System.Linq;
using Microsoft.Phone.Controls;
using NavigationWithBehaviorsAndMVVM.ViewModels;
 
namespace NavigationWithBehaviorsAndMVVM
{
    public partial class MainPage : PhoneApplicationPage
    {
        private MainViewModel _viewModel;
 
        /// <summary>
        /// Initializes a new instance of the <see cref="MainPage" /> class.
        /// </summary>
        public MainPage()
        {
            InitializeComponent();
 
            _viewModel = (MainViewModel)DataContext;
            _viewModel.GoToDetailsPage += ViewModelGoToDetailsPage;
        }
 
        /// <summary>
        /// Views the model go to details page.
        /// </summary>
        /// <param name="obj">The obj.</param>
        void ViewModelGoToDetailsPage(string obj)
        {
            NavigationService.Navigate(new Uri(obj, UriKind.Relative));
        }
    }
}

Step 10



In the "DetailsPage.xaml.cs" Codebehind, just override the OnNavigatedTo Method

using System;
using System.Linq;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using NavigationWithBehaviorsAndMVVM.ViewModels;
 
namespace NavigationWithBehaviorsAndMVVM
{
    public partial class DetailsPage : PhoneApplicationPage
    {
        private DetailsViewModel _viewModel;
 
        /// <summary>
        /// Initializes a new instance of the <see cref="DetailsPage" /> class.
        /// </summary>
        public DetailsPage()
        {
            InitializeComponent();
 
            _viewModel = (DetailsViewModel)DataContext;
        }
 
        /// <summary>
        /// Called when a page becomes the active page in a frame.
        /// </summary>
        /// <param name="e">An object that contains the event data.</param>
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
 
            string id;
            NavigationContext.QueryString.TryGetValue("id", out id);
           
            _viewModel.Title = id;
        }
    }
}




Step 11



Add the References to your Project and add the EventTrigger to invoke the "NavigateToDetailsPage()" method in the MainViewModel to your MainPage.xaml











<phone:PhoneApplicationPage
    x:Class="NavigationWithBehaviorsAndMVVM.MainPage"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:viewModels="clr-namespace:NavigationWithBehaviorsAndMVVM.ViewModels"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:ec="clr-namespace:Microsoft.Expression.Interactivity.Core;assembly=Microsoft.Expression.Interactions"
    mc:Ignorable="d"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    shell:SystemTray.IsVisible="True">
 
    <phone:PhoneApplicationPage.DataContext>
        <viewModels:MainViewModel />
    </phone:PhoneApplicationPage.DataContext>
     
    <!--LayoutRoot is the root grid where all page content is placed-->
    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
 
 
        <!--TitlePanel contains the name of the application and page title-->
        <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
            <TextBlock Text="{Binding Title}" Style="{StaticResource PhoneTextNormalStyle}" Margin="12,0"/>
            <TextBlock Text="MainPage" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
        </StackPanel>
 
        <!--ContentPanel - place additional content here-->
        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <Button Content="Navigate to details page">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <ec:CallMethodAction TargetObject="{Binding Mode=OneWay}"
                                             MethodName="NavigateToDetailsPage"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Button>
        </Grid>
    </Grid>
 
</phone:PhoneApplicationPage>


Last Step



Try it out...













  

Sort by: Published Date | Most Recent | Most Useful
Comments
Page 1 of 1 (4 items)