Instead of using a smelly locator or some other hack to intercept the frame's navigation, I recommend to avoid the Frame
as content host and also avoid Page
as content.
In WPF, the recommended view management should be based on the view-model-first pattern. Also in WPF it is not recommended to use the heavy Frame
as content host. Both recommendations also apply to UWP.
We should view a UWP application as single-page application (SPA) in terms of pages based on the Page
class and the hosting root frame.
This means MainPage.xaml
is only used as host for our custom page system, which is based on a ContentControl
, a set of page view models and a set of DataTemplate
definition for each page view model and a PageNavigationViewModel
class that controls the page navigation. MainPage
(or Page
) is the equivalent to the WPF Window
: the element tree root or visual host.
The pattern
The following example shows the pattern by seetting up a basic two page application (a landing page and a settings page) using dependency injection with Autofac (which of course can be replaced with any other IoC framework).
The important detail is not the IoC framework or the IoC container configuration, but the way the application is structured to allow page navigation combined with dependency injection.
The Goal
The goal is to display a LandingPage
view (UserControl
) based on a LandingPageViewModel
and a SettingsPage
view based on a SettingsPageViewModel
.
All view model instances are created by the IoC container, while all associated views are instantiated implicitly by the UWP framework using DataTemplate
.
The example is structured into three sections:
- Setting up the navigation infrastructure
- Creating the pages/views
- Bootstrapping the application
There is some extra complexity like factories introduced due to depndency injection. In a real world example, we would depend on interfaces instead of concrete implementations (Dependencsy Inversion principle). For simplicity there are no interfaces used except those that are relevant to the infrastructure.
1 Setting up the navigation infrastructure
PageId.cs
Each view is identified by an enum. This makes selecting a page model e.g., via command parameter and refactoring easier. It also eliminates magic strings.
public enum PageId
{
Undefined = 0,
LandingPage,
SettingsPage
}
Factory delegates (Autofac specific details)
The delegates are required to allow Autofac to create factories. Other IoC frameworks may h ave a different requirement to generate factories. MEF for example uses the ExportFactory<T>
type as constructor depndency. The framework would then automatically generate the appropriate factory.
IoC generated factories allow dynamic type creation, where the instances are wired up according to the IoC container configuration. Factories are used to avoid passing around a reference to the IoC container (or even worse making the container a Singleton). Such practice is an anti-pattern, which contradicts the use of the IoC container.
The delagates should be added to the common namespace of the PageIndexFactory
and Bootstrapper
classes (see below) and marked as internal
.
private LandingPageModelFactory LandingPageModelFactory { get; }
private SettingsPageModelFactory SettingsPageModelFactory { get; }
PageIndexFactory.cs
The individual page view models are initialized by a PageIndexFactory
, which is injected into the PageNavigationViewModel
. The purpose is to create the navigation index. PageIndexFactory
makes use of delegate factories.
Every serious IoC framework supports auto-generation of factories. This way the IoC container is still able to wire up the dependencies (note that passing around the original IoC container instance is an anti-pattern).
public class PageIndexFactory
{
private LandingPageModelFactory LandingPageModelFactory { get; }
private SettingsPageModelFactory SettingsPageModelFactory { get; }
public PageIndexFactory(LandingPageModelFactory landingPageModelFactory,
SettingsPageModelFactory settingsPageModelFactory)
{
this.LandingPageModelFactory = landingPageModelFactory;
this.SettingsPageModelFactory = settingsPageModelFactory;
}
public Dictionary<PageId, IPageModel> CreateIndex()
{
var index = new Dictionary<PageId, IPageModel>()
{
{PageId.LandingPage, this.LandingPageModelFactory.Invoke()},
{PageId.SettingsPage, this.SettingsPageModelFactory.Invoke()}
};
return index;
}
}
PageNavigationViewModel.cs
This is the view model that handles the navigation. It exposes a SelectViewCommand
, which can be assigned to an ICommandSource
like a Button
. The CommandParameter
must be the PageId
, which actually selects the IPageModel
from the page index.
The PageNavigationViewModel
is assigned to the application's original MainPage
, which is the host of the custom navigation infrastructure.
The PageNavigationViewModel
exposes a SelectedView
property which holds a view model e.g., IPageModel
. This property is bound to the hosting ContentControl.Content
property.
public class PageNavigationViewModel : INotifyPropertyChanged
{
public PageNavigationViewModel(PageIndexFactory pageIndexFactory)
{
this.PageIndex = pageIndexFactory.CreateIndex();
if (this.PageIndex.TryGetValue(PageId.LandingPage, out IPageModel welcomePageModel))
{
this.SelectedView = welcomePageModel;
}
}
private void ExecuteSelectPage(object commandParameter)
{
var pageId = (PageId) commandParameter;
if (this.PageIndex.TryGetValue(pageId, out IPageModel selectedPageModel))
{
this.SelectedView = selectedPageModel;
}
}
public ICommand SelectViewCommand => new RelayCommand(ExecuteSelectPage);
private Dictionary<PageId, IPageModel> PageIndex { get; }
private IPageModel selectedView;
public IPageModel SelectedView
{
get => this.selectedView;
set
{
this.selectedView = value;
OnPropertyChanged();
}
}
}
IPageModel.cs
The interface, which must be implemented by the individual page view models.
public interface IPageModel : INotifyPropertyChanged
{
string PageTitle { get; }
}
INavigationHost.cs
This interface is implemented by the application Page
host e.g., MainPage
. It allows to assign the PageNavigationViewModel
anonymously.
interface INavigationHost
{
PageNavigationViewModel NavigationViewModel { get; set; }
}
MainPage.xaml.cs
The host of the custom navigation infrastructure. This instance is created via reflection by the hosting Frame
. We use the implementation of INavigationHost
to initialze this class with an instance of PageNavigationviewModel
(see App.xaml.cs* below).
MainPage
is the only class that is not instantiated by the IoC container. As this class has no reponsibilities, except exposing the PageNavigationViewModel
and hosting the ContentControl
(to host the real pages/views), it will have no relevant dependencies.
public sealed partial class MainPage : Page, INavigationHost
{
private PageNavigationViewModel navigationViewModel;
public PageNavigationViewModel NavigationViewModel
{
get => this.navigationViewModel;
set
{
this.navigationViewModel = value;
this.DataContext = this.NavigationViewModel;
}
}
public MainPage()
{
this.InitializeComponent();
}
}
MainPage.xaml
Hosts the real view and the DataTemplateSelector
for the ContentControl
. host and optionally the navigation elements like navigation buttons.
The ContentControl
loads the DataTemplate
, that is associated with the IPageModel
instance. This DataTemplate
contains e.g., a UserControl
, which hosts the actual page content.
The DataContext
of each view is set by the ContentControl
and is the current Content
(which is the PageNavigationViewModel.SelectedView
).
<Page>
<Page.Resources>
<local:PageTemplateSelector x:Key="PageTemplateSelector">
<local:PageTemplateSelector.DataTemplateCollection>
<DataTemplate x:DataType="local:LandingPageViewModel"
local:Element.DataType="local:LandingPageViewModel">
<local:LandingPage />
</DataTemplate>
<DataTemplate x:DataType="local:SettingsPageViewModel"
local:Element.DataType="local:SettingsPageViewModel">
<local:SettingsPage />
</DataTemplate>
</local:PageTemplateSelector.DataTemplateCollection>
</local:PageTemplateSelector>
</Page.Resources>
<StackPanel>
<!-- Optional navigation section -->
<Button Content="Show Settings Page"
Command="{x:Bind NavigationViewModel.SelectViewCommand}">
<Button.CommandParameter>
<local:PageId>SettingsPage</local:PageId>
</Button.CommandParameter>
</Button>
<Button Content="Show Welcome Page"
Command="{x:Bind NavigationViewModel.SelectViewCommand}">
<Button.CommandParameter>
<local:PageId>LandingPage</local:PageId>
</Button.CommandParameter>
</Button>
<!-- The host of the views -->
<ContentControl Content="{x:Bind NavigationViewModel.SelectedView, Mode=OneWay}"
ContentTemplateSelector="{StaticResource PageTemplateSelector}" />
</StackPanel>
</Page>
PageTemplateSelector.cs
UWP does not support implicit data templates like WPF. Therefore we have to use a template selector that is assigned to the hosting ContentControl
. ContentControl.Content
will hold the PageNavigationViewModel.SelectedViw
view model and the PageTemplateSelector
will select the matching DataTemplate
. Since DataTemplate
has not DataType
property (opposed to the WPF version), we have to introduce an attached property to hoöd this value. Note, that x:DataType
is a compiler directive and not accessible by code.
public class PageTemplateSelector : DataTemplateSelector
{
public DataTemplateCollection DataTemplateCollection { get; set; }
#region Overrides of DataTemplateSelector
public PageTemplateSelector()
{
this.DataTemplateCollection = new DataTemplateCollection();
}
/// <inheritdoc />
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
if (item != null
&& this.DataTemplateCollection.First(template => Element.GetDataType(template) == item.GetType()) is DataTemplate dataTemplate)
{
return dataTemplate;
}
return base.SelectTemplateCore(item, container);
}
/// <inheritdoc />
protected override DataTemplate SelectTemplateCore(object item)
{
if (item != null
&& this.DataTemplateCollection.First(template => Element.GetDataType(template) == item.GetType()) is
DataTemplate dataTemplate)
{
return dataTemplate;
}
return base.SelectTemplateCore(item);
}
#endregion
}
DataTemplateCollection.cs
The XAML collection to hold the page view DataTemples
definitions. It is used by the PageTemplateSelector
.
public class DataTemplateCollection : List<DataTemplate>
{}
Element.cs
Class that defines the attached DataType
property, which is used by the PageTemplateSelector
to filter the DataTemplate
definitions. This attached property should be therefore set on every DataTemplate
that is associated with a page view model.
public class Element : DependencyObject
{
#region Type attached property
public static readonly DependencyProperty DataTypeProperty = DependencyProperty.RegisterAttached(
"DataType",
typeof(Type),
typeof(Element),
new PropertyMetadata(default(Type)));
public static void SetDataType([NotNull] DependencyObject attachingElement, Type value) => attachingElement.SetValue(Element.DataTypeProperty, value);
public static Type GetDataType([NotNull] DependencyObject attachingElement) =>
(Type) attachingElement.GetValue(Element.DataTypeProperty);
#endregion
}
2 Creating the pages/views
All dependencies are resolved by the IoC container. The PageIndexfactory
controls the instantiation via auto-generated factories.
To keep it short, the following example implementation only shows the LandingPage
related UserControl
and LandingPageViewModel
. The same pattern applies to the SettingsPage
and SettingsPageViewModel
.
LandingPageViewModel.cs
The view model for the welcome view.
public class LandingPageViewModel : IPageModel, INotifyPropertyChanged
{
public LandingPageViewModel(IFacade someExampleDependency)
{
this.Facade = someExampleDependency;
this.PageTitle = "Welcome Page";
}
public string PageTitle { get; }
private string IFacade Facade { get; }
}
LandingPage.xaml.cs
The DataContext
is set implicitly by the hosting ContentControl
.
public sealed partial class LandingPage : UserControl
{
// Enable x:Bind
public LandingPageViewModel ViewModel { get; private set; }
public LandingPage()
{
this.InitializeComponent();
// Delegate the DataContext to the ViewModel property to enable x:Bind
this.DataContextChanged += OnDataContextChanged;
}
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
this.ViewModel = args.NewValue as LandingPageViewModel;
}
}
LandingPage.xaml
The view that is displayed via DataTemplate
by the ContentControl
.
<UserControl>
<Grid>
<TextBlock Text="{x:Bind ViewModel.PageTitle}" />
</Grid>
</UserControl>
3 Bootstrapping the application
The following section shows how to set up the IoC container and bootstrap the UWP application.
Bootstrapper.cs
Encapsulates the IoC container configuration and bootstrapping of the application.
internal sealed class Bootstrapper
{
internal static void InitializeApplication(INavigationHost navigationHost)
{
// Don't use the dependency container outside this class
using (IContainer services = Bootstrapper.BuildDependencies())
{
PageNavigationViewModel navigationViewModel = services.Resolve<PageNavigationViewModel>();
navigationHost.NavigationViewModel = navigationViewModel;
}
}
internal static IContainer BuildDependencies()
{
var builder = new ContainerBuilder();
builder.RegisterType<PageIndexFactory>();
builder.RegisterType<PageNavigationViewModel>();
builder.RegisterType<LandingPageViewModel>();
builder.RegisterType<SettingsPageViewModel>();
// Dependency to address your question
builder.RegisterType<Facade>().As<IFacade>();
// Don't forget to dispose the IContainer instance (caller's responsibility)
return builder.Build();
}
}
App.xaml.cs
Bootstarpping the UWP application.
sealed partial class App : Application
{
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
this.InitializeComponent();
this.Suspending += OnSuspending;
}
/// <summary>
/// Invoked when the application is launched normally by the end user. Other entry points
/// will be used such as when the application is launched to open a specific file.
/// </summary>
/// <param name="e">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
Frame rootFrame = Window.Current.Content as Frame;
// Do not repeat app initialization when the Window already has content,
// just ensure that the window is active
if (rootFrame == null)
{
// Create a Frame to act as the navigation context and navigate to the first page
rootFrame = new Frame();
rootFrame.NavigationFailed += OnNavigationFailed;
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
{
//TODO: Load state from previously suspended application
}
// Place the frame in the current Window
Window.Current.Content = rootFrame;
}
if (e.PrelaunchActivated == false)
{
if (rootFrame.Content == null)
{
// When the navigation stack isn't restored navigate to the first page,
// configuring the new page by passing required information as a navigation
// parameter
rootFrame.Navigate(typeof(MainPage), e.Arguments);
/****************** Build depndencies and initialize navigation *************/
if (rootFrame.Content is INavigationHost navigationHost)
{
Bootstrapper.InitializeApplication(navigationHost);
}
}
// Ensure the current window is active
Window.Current.Activate();
}
}
...
}