How to use dependency injection in an UWP app?
Asked Answered
A

4

9

I am using autofac in an UWP application. In my App instance, I am setting up the dependency, like this:

public sealed partial class App
{
   private readonly IFacade m_facade;

   public App()
   {
       InitializeComponent();

       m_facade = InitializeDependencies();

       Suspending += OnSuspending;
   }

   private IFacade InitializeDependencies()
   {
       var containerBuilder = new ContainerBuilder();

       //  Registers all the platform-specific implementations of services.
       containerBuilder.RegisterType<LoggingService>()
                       .As<ILoggingService>()
                       .SingleInstance();

       containerBuilder.RegisterType<SQLitePlatformService>()
                       .As<ISQLitePlatformService>()
                       .SingleInstance();

       containerBuilder.RegisterType<DiskStorageService>()
                       .As<IDiskStorageService>()
                       .SingleInstance();

       ...

       containerBuilder.RegisterType<Facade>()
                       .As<IFacade>();

       //  Auto-magically resolves the IFacade implementation.
       var facadeContainer = containerBuilder.Build();
       var facadeLifetimeScope = m_facadeContainer.BeginLifetimeScope();

       return facadeLifetimeScope.Resolve<IFacade>();
   }
}

I need to pass my IFacade instance around to the different Pages to reach my view-models. Here is an example of one of my pages:

internal sealed partial class SomePage
{
    public SomePageViewModel ViewModel { get; }

    public SomePage()
    {
        ViewModel = new SomePageViewModel(/* need an IFacade implementation here!! */);

        InitializeComponent();
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        ViewModel.LoadAsync();

        base.OnNavigatedTo(e);
    }
}

UWP is responsible for the Pages instantiation so it limits my options. Here is how the navigation from one page to another is done in UWP. From the App instance:

rootFrame.Navigate(typeof(MainPage), e.Arguments);

Or from a Page instance:

ContentFrame.Navigate(typeof(SomeOtherPage));

Question

What would be the proper way to pass my IFacade instance around to the view-models (without doing anything hacky obviously)?

Arboretum answered 15/4, 2018 at 14:46 Comment(11)
I have been searching for something like this in the past. I ended up with creating an overridden navigation service. Hopefully a better way is available these days.Filide
The hacky thing that had in mind was to make the IFacade instance publicly available from the App instance through a static property and then have the Page instances get it and pass it to the view-models. This is from from being ideal from the unit tests perspective.Arboretum
Well, that is hacky... beware that you don't end up with tons of statics.Filide
@Filide When doing view first approach you would need to use more of a service locator pattern. register the view models as well and then resolve them from the container. The container will resolve and inject the necessary dependencies. Other wise you could probably use a pre built framework that already has the functionality built in.Cecilius
@Nkosi: an example of such a pattern in combination with the navigation service would be nice.Filide
@Filide checkout caliburn.micro for the time being. They have a frame adapter that should suit your needsCecilius
@Stefan, I provided examples as requested.Cecilius
I see, loking good, you're up for the bounty ;-)Filide
@Kzrystof: assuming you used a dirty hack; you might want to check the newly arrived answer.Filide
@Nkosi: please give me some day's to work through your answer XDFilide
@Filide I did :) The simplest approach is the dirty hack and uses a static service locator in the App class (which is what I actually did for now).Arboretum
C
22

Because UWP is responsible for the Page's instantiation it removes the ability to explicitly inject dependencies into views.

The simplest approach would be to have an accessible service locator and register your dependencies with it.

public sealed partial class App {

    public App() {
        InitializeComponent();

        Container = ConfigureServices();

        Suspending += OnSuspending;
    }

    public static IContainer Container { get; set; }

    private IContainer ConfigureServices() {
        var containerBuilder = new ContainerBuilder();

        //  Registers all the platform-specific implementations of services.
        containerBuilder.RegisterType<LoggingService>()
                       .As<ILoggingService>()
                       .SingleInstance();

        containerBuilder.RegisterType<SQLitePlatformService>()
                       .As<ISQLitePlatformService>()
                       .SingleInstance();

        containerBuilder.RegisterType<DiskStorageService>()
                       .As<IDiskStorageService>()
                       .SingleInstance();

        containerBuilder.RegisterType<Facade>()
                       .As<IFacade>();

        //...Register ViewModels as well

        containerBuilder.RegisterType<SomePageViewModel>()
            .AsSelf();

        //...

        var container = containerBuilder.Build();
        return container;
   }

   //...
}

And then resolve dependencies as needed in the views.

internal sealed partial class SomePage {

    public SomePage() {
        InitializeComponent();
        ViewModel = App.Container.Resolve<SomePageViewModel>();
        this.DataContext = ViewModel;
    }

    public SomePageViewModel ViewModel { get; private set; }

    protected override void OnNavigatedTo(NavigationEventArgs e) {
        ViewModel.LoadAsync();
        base.OnNavigatedTo(e);
    }
}

Another more complicated way would be to use a convention base approach and tapping into the application's Frame navigation.

The plan would be to subscribe to the NavigatedTo event

public interface INavigationService {
    bool Navigate<TView>() where TView : Page;
    bool Navigate<TView, TViewModel>(object parameter = null) where TView : Page;
}

public class NavigationService : INavigationService {
    private readonly Frame frame;
    private readonly IViewModelBinder viewModelBinder;

    public NavigationService(IFrameProvider frameProvider, IViewModelBinder viewModelBinder) {
        frame = frameProvider.CurrentFrame;
        frame.Navigating += OnNavigating;
        frame.Navigated += OnNavigated;
        this.viewModelBinder = viewModelBinder;
    }

    protected virtual void OnNavigating(object sender, NavigatingCancelEventArgs e) { }

    protected virtual void OnNavigated(object sender, NavigationEventArgs e) {
        if (e.Content == null)
            return;

        var view = e.Content as Page;
        if (view == null)
            throw new ArgumentException("View '" + e.Content.GetType().FullName +
                "' should inherit from Page or one of its descendents.");

        viewModelBinder.Bind(view, e.Parameter);
    }

    public bool Navigate<TView>() where TView : Page {
        return frame.Navigate(typeof(TView));
    }

    public bool Navigate<TView, TViewModel>(object parameter = null) where TView : Page {
        var context = new NavigationContext(typeof(TViewModel), parameter);
        return frame.Navigate(typeof(TView), context);
    }
}

and once there using the navigation argument to pass the view model type to be resolved and data bind to the view.

public interface IViewModelBinder {
    void Bind(FrameworkElement view, object viewModel);
}

public class ViewModelBinder : IViewModelBinder {
    private readonly IServiceProvider serviceProvider;

    public ViewModelBinder(IServiceProvider serviceProvider) {
        this.serviceProvider = serviceProvider;
    }

    public void Bind(FrameworkElement view, object viewModel) {
        InitializeComponent(view);

        if (view.DataContext != null)
            return;

        var context = viewModel as NavigationContext;
        if (context != null) {
            var viewModelType = context.ViewModelType;
            if (viewModelType != null) {
                viewModel = serviceProvider.GetService(viewModelType);
            }

            var parameter = context.Parameter;
            //TODO: figure out what to do with parameter
        }

        view.DataContext = viewModel;
    }

    static void InitializeComponent(object element) {
        var method = element.GetType().GetTypeInfo()
            .GetDeclaredMethod("InitializeComponent");

        method?.Invoke(element, null);
    }
}

The Frame is accessed via a wrapper service that extracts it from the current window

public interface IFrameProvider {
    Frame CurrentFrame { get; }
}

public class DefaultFrameProvider : IFrameProvider {
    public Frame CurrentFrame {
        get {
            return (Window.Current.Content as Frame);
        }
    }
}

And the following extension classes provide utility support

public static class ServiceProviderExtension {
    /// <summary>
    /// Get service of type <typeparamref name="TService"/> from the <see cref="IServiceProvider"/>.
    /// </summary>
    public static TService GetService<TService>(this IServiceProvider provider) {
        return (TService)provider.GetService(typeof(TService));
    }
    /// <summary>
    /// Get an enumeration of services of type <paramref name="serviceType"/> from the <see cref="IServiceProvider"/>
    /// </summary>
    public static IEnumerable<object> GetServices(this IServiceProvider provider, Type serviceType) {
        var genericEnumerable = typeof(IEnumerable<>).MakeGenericType(serviceType);
        return (IEnumerable<object>)provider.GetService(genericEnumerable);
    }
    /// <summary>
    /// Get an enumeration of services of type <typeparamref name="TService"/> from the <see cref="IServiceProvider"/>.
    /// </summary>
    public static IEnumerable<TService> GetServices<TService>(this IServiceProvider provider) {
        return provider.GetServices(typeof(TService)).Cast<TService>();
    }
    /// <summary>
    /// Get service of type <paramref name="serviceType"/> from the <see cref="IServiceProvider"/>.
    /// </summary>
    public static object GetRequiredService(this IServiceProvider provider, Type serviceType) {
        if (provider == null) {
            throw new ArgumentNullException("provider");
        }

        if (serviceType == null) {
            throw new ArgumentNullException("serviceType");
        }

        var service = provider.GetService(serviceType);
        if (service == null) {
            throw new InvalidOperationException(string.Format("There is no service of type {0}", serviceType));
        }
        return service;
    }
    /// <summary>
    /// Get service of type <typeparamref name="T"/> from the <see cref="IServiceProvider"/>.
    /// </summary>
    public static T GetRequiredService<T>(this IServiceProvider provider) {
        if (provider == null) {
            throw new ArgumentNullException("provider");
        }
        return (T)provider.GetRequiredService(typeof(T));
    }
}

public class NavigationContext {
    public NavigationContext(Type viewModelType, object parameter = null) {
        ViewModelType = viewModelType;
        Parameter = parameter;
    }
    public Type ViewModelType { get; private set; }
    public object Parameter { get; private set; }
}

public static class NavigationExtensions {
    public static bool Navigate<TView>(this Frame frame) where TView : Page {
        return frame.Navigate(typeof(TView));
    }

    public static bool Navigate<TView, TViewModel>(this Frame frame, object parameter = null) where TView : Page {
        var context = new NavigationContext(typeof(TViewModel), parameter);
        return frame.Navigate(typeof(TView), context);
    }
}

You could configure the application like you would before at start up but the container will be used to initialize the navigation service and it will handle the rest.

public sealed partial class App {

    public App() {
        InitializeComponent();

        Container = ConfigureServices();

        Suspending += OnSuspending;
    }

    public static IContainer Container { get; set; }

    private IContainer ConfigureServices() {
        //... code removed for brevity

        containerBuilder
            .RegisterType<DefaultFrameProvider>()
            .As<IFrameProvider>()
            .SingleInstance();

        containerBuilder.RegisterType<ViewModelBinder>()
            .As<IViewModelBinder>()
            .SingleInstance();

        containerBuilder.RegisterType<AutofacServiceProvider>()
            .As<IServiceProvider>()

        containerBuilder.RegisterType<NavigationService>()
            .AsSelf()
            .As<INavigationService>();


        var container = containerBuilder.Build();
        return container;
    }

    protected override void OnLaunched(LaunchActivatedEventArgs e) {
        Frame rootFrame = Window.Current.Content as Frame;
        if (rootFrame == null) {
            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;
        }

        //Activating navigation service
        var service = Container.Resolve<INavigationService>();

        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<SomePage, SomePageViewModel>();
            }
            // Ensure the current window is active
            Window.Current.Activate();
        }
    }

    public class AutofacServiceProvider : IServiceProvider
        public object GetService(Type serviceType) {
            return App.Container.Resolve(serviceType);
        }
    }

   //...
}

By using the above convention the Frame extensions allow some generic navigation.

ContentFrame.Navigate<SomeOtherPage, SomeOtherPageViewModel>();

Views can be as simple as

internal sealed partial class SomePage {
    public SomePage() {
        InitializeComponent();
    }

    public SomePageViewModel ViewModel { get { return (SomePageViewModel)DataContext;} }

    protected override void OnNavigatedTo(NavigationEventArgs e) {
        ViewModel.LoadAsync();
        base.OnNavigatedTo(e);
    }
}

As the navigation service would also set the data context of the view after navigation.

Navigation can also be initiated by view models that have the INavigationService injected

public class SomePageViewModel : ViewModel {
    private readonly INavigationService navigation;
    private readonly IFacade facade;

    public SomePageViewModel(IFacade facade, INavigationService navigation) {
        this.navigation = navigation;
        this.facade = facade;
    }

    //...

    public void GoToSomeOtherPage() {
        navigation.Navigate<SomeOtherPage, SomeOtherPageViewModel>();
    }

    //...
}
Cecilius answered 23/4, 2018 at 21:47 Comment(3)
Since UWP does not seem to provide any dependency injection framework, this answer is the most useful thing I have for now :)Arboretum
@Kzrystof The abstract approach I took with using IServiceProvide allows you to swap in your dependency container or choice. I have seen a few frameworks do something similar.Cecilius
@Nkosi, excellent and many many thanks. I want to improve the solution to change the frame provided by DefaultFrameProvider to be a frame within SomePage.xaml. But there is a catch22 in the ConfigureServices() setup, as you must first resolve the NavigationService before Navigate(SomePage, SomepageViewModel), but you need SomePage.xaml active to be the window.Current.Content in DefaultFrameProvider so that it returns the frame in SomePage.xaml for use in navigation between other pages. Any idea how you would do it?Orna
A
2

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:

  1. Setting up the navigation infrastructure
  2. Creating the pages/views
  3. 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();
      }
    }

    ...
}
Abuttal answered 28/11, 2020 at 21:9 Comment(1)
This is a really comprehensive answer. I'm going to try it out to get a better understanding of it. Thanks for taking the time to supply this detail.Convent
A
1

Just to add to Nkosi's fantastic and deep question, when I create pages in UWP I use the following pattern:

private IDependency _dep1;

public Page()
{
    _dep1 = ServiceLocator.Current.Resolve<IDependency>();
    init();
}

public Page(IDependency dep1, ...)
{
    _dep1 = dep1;
    init();
}

private void init()
{
    /* Do initialization here, i.e. InitializeComponent() */
}

The benefit that this gives is that it allows you to still write testable code because you inject your dependencies in your unit tests. The service locator only runs at run time.

As Nkosi points out, the Frame is responsible ultimately for instantiation of the Page via the Navigate method. At the point that Microsoft exposes the ability to intercept or override the instantiation, it'll be possible to have a DI container do the instantiation. Until then, we're stuck with using a Service Locator pattern at runtime.

Astrophysics answered 5/3, 2019 at 20:27 Comment(1)
You should never pass around the IoC container or make it globally accessible. Instead use/inject factories. Most IoC frameworks support auto-generation of factories or factory delegates.Abuttal
S
0

I'm very new to UWP (but a very old hand at .NET/WPF etc etc), so this might be hacky, but I by-passed the Frame.Navigate method altogether by putting the following at the top of my OnLaunched method:

        if (Window.Current.Content == null)
        {
            var windowFrame = new Frame()
            {
                Content = new MainPage()
            };

            Window.Current.Content = windowFrame;
            Window.Current.Activate();

            return;
        }

You then control the creation of your mainpage - if you want to support DI, then you can use something like:

            var windowFrame = new Frame()
            {
                Content = container.GetInstance<MainPage>()
            };

This then means that MainPage becomes your CompositionRoot and your DI flows from there.

My guess is that you then don't get any navigate back/forward functionality, although I may be wrong...my app is only single-page so I've not seen if that is a problem or not...

Start answered 29/4, 2020 at 14:57 Comment(1)
How do you set the dataContext for your MainPage()? if that can be set manually, then within mainPage(compositionRoot) one could use @Nkosi's answer above to do navigation within it's frame... I am stuck trying to use his answer to use Autofac DI, launching a main page into the "main" frame, and then navigation to other pages within that main page's frame..Orna

© 2022 - 2024 — McMap. All rights reserved.