Search code examples
c#dependency-injectioninversion-of-controlwinui-3windows-template-studio

Odd dependency injection behavior with Views & ViewModels using Template Studio for WinUI


Using Template Studio for WinUI, I created an app with 2 pages, Main and Foo. If the view model is passed into the page's constructor the application generates an exception, but if the view model is grabbed using App.GetService<xViewModel>(); where x is the name of the view model for the page, everything works great. Passing the same view model into other non-page class constructors works great as well.

Can someone explain why passing the view model into the page's constructor fails?

Code

Generated Foo.xaml.cs file (works great):

using Ioc.ViewModels;
using Microsoft.UI.Xaml.Controls;

namespace Ioc.Views;

public sealed partial class FooPage : Page
{
    public FooViewModel ViewModel
    {
        get;
    }

    public FooPage()
    {
        ViewModel = App.GetService<FooViewModel>();
        InitializeComponent();
    }
}

Foo.xaml.cs - passing view model into the constructor (throws exception):

    public FooPage(FooViewModel viewModel)
    {
        ViewModel = viewModel;
        InitializeComponent();
    }

the following exception occurs when trying to navigate to the Foo page when view model is passed into the constructor:

System.NullReferenceException: 'Object reference not set to an instance of an object.'

at Ioc.Ioc_XamlTypeInfo.XamlUserType.ActivateInstance() in C:\PathToProject\Ioc\obj\x64\Debug\net7.0-windows10.0.19041.0\win10-x64\XamlTypeInfo.g.cs:line 2602

App.xaml.cs constructor where the FooViewModel service is registered:

    public App()
    {
        InitializeComponent();

        Host = Microsoft.Extensions.Hosting.Host.
        CreateDefaultBuilder().
        UseContentRoot(AppContext.BaseDirectory).
        ConfigureServices((context, services) =>
        {
            // Default Activation Handler
            services.AddTransient<ActivationHandler<LaunchActivatedEventArgs>, DefaultActivationHandler>();

            // Other Activation Handlers

            // Services
            services.AddTransient<INavigationViewService, NavigationViewService>();

            services.AddSingleton<IActivationService, ActivationService>();
            services.AddSingleton<IPageService, PageService>();
            services.AddSingleton<INavigationService, NavigationService>();

            // Core Services
            services.AddSingleton<IFileService, FileService>();

            // Views and ViewModels
            services.AddTransient<FooViewModel>();
            services.AddTransient<FooPage>();
            services.AddTransient<MainViewModel>();
            services.AddTransient<MainPage>();
            services.AddTransient<ShellPage>();
            services.AddTransient<ShellViewModel>();

            // Configuration
        }).
        Build();

        UnhandledException += App_UnhandledException;
    }

Edit

Navigation using a frame and this Navigation method:

private Frame? _frame; // Type Microsoft.UI.Xaml.Controls.Frame

    public bool NavigateTo(string pageKey, object? parameter = null, bool clearNavigation = false)
    {
        var pageType = _pageService.GetPageType(pageKey);

        if (_frame != null && (_frame.Content?.GetType() != pageType || (parameter != null && !parameter.Equals(_lastParameterUsed))))
        {
            _frame.Tag = clearNavigation;
            var vmBeforeNavigation = _frame.GetPageViewModel();
            var navigated = _frame.Navigate(pageType, parameter);
            if (navigated)
            {
                _logger.LogDebug("Navigated to {PageType}", pageType.Name);
                _lastParameterUsed = parameter;
                if (vmBeforeNavigation is INavigationAware navigationAware)
                {
                    navigationAware.OnNavigatedFrom();
                }
            }

            return navigated;
        }

        return false;
    }

The code throws the exception on this line:

var navigated = _frame.Navigate(pageType, parameter);

Solution

  • By navigate I guess you mean navigation of a Frame control but the navigation feature of Frames doesn't resolve your type like App.GetService<T>() does.

    What you can do is something like this:

    In App.xaml.cs

    public static bool TryGetService(Type serviceType, out object? service)
    {
        service = (App.Current as App)?.Host?.Services.GetService(serviceType);
        return service is not null;
    }
    

    and in the navigation event handler:

    private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
    {
        if (args.SelectedItem is NavigationViewItem navigationViewItem &&
            navigationViewItem.Tag is string pageName &&
            Type.GetType(pageName) &&
            App.TryGetService(pageType, out object? page) is true)
        {
            this.ContentFrame.Content = page;
        }
    }
    ``