Как подружить NavigationView+Page из WinUI 3 SDK и IoC контейнер?

Например у меня в главном окне есть NavigationView, и есть страницы Page, и есть MS DI IoC контейнер.

public partial class App : Application
{
    public static IServiceProvider Container { get; private set; }

    public App()
    {
        InitializeComponent();
    }

    protected override void OnLaunched(LaunchActivatedEventArgs args)
    {
        ServiceCollection services = [];

        // ... регистрация вьюмоделей страниц ...

        services.AddSingleton<SettingsPageViewModel>();
        services.AddSingleton<MainViewModel>();
        services.AddTransient<MainWindow>();
        Container = services.BuildServiceProvider();

        Window window = Container.GetService<MainWindow>();
        window.Activate();
        base.OnLaunched(args);
    }
}

NavigationModel.cs

public class NavigationModel(string header, Type viewType, IconElement icon)
{
    public string Header { get; } = header;
    public Type ViewType { get; } = viewType;
    public IconElement Icon { get; } = icon;
}

MainWindow.xaml

<NavigationView x:Name="Nav"
                IsSettingsVisible="True"
                IsBackButtonVisible="Collapsed"
                SelectionChanged="NavigationView_SelectionChanged"
                OpenPaneLength="160"
                IsBackEnabled="False"
                MenuItemsSource="{x:Bind MenuItems}"
                SelectedItem="{x:Bind MenuItems[0], Mode=OneTime}">
    <NavigationView.MenuItemTemplate>
        <DataTemplate x:DataType="models:NavigationModel">
            <NavigationViewItem Content="{x:Bind Header, Mode=OneTime}"
                                Icon="{x:Bind Icon, Mode=OneTime}"/>
        </DataTemplate>
    </NavigationView.MenuItemTemplate>
    <Frame x:Name="navigationFrame" CacheSize="0"/>
</NavigationView>

Есть метод Frame.Navigate(Type), который предлагает передать тип страницы для перехода.

MainWindow.xaml.cs

public NavigationModel[] MenuItems { get; }

private async void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
    if (args.SelectedItemContainer is null)
        return;

    if (!args.IsSettingsSelected)
    {
        try
        {
            if (args.SelectedItem is NavigationModel nav)
            {
                navigationFrame.Navigate(nav.ViewType);
            }
        }
        catch (Exception ex)
        {
            await ShowContentDialogAsync(ex.ToString(), ex.GetType().Name);
        }
    }
    else
    {
        navigationFrame.Navigate(typeof(SettingsPage));
    }
}

Код урезан для примера только до вызова настроек.

При этом в метод Navigate можно пробросить какие-то аргументы и получить их внутри Page. Здесь возникает вопрос: а что, если я хочу, чтобы экземпляр Page создавался через IoC контейнер, и в конструктор этой страницы я там бы пробросил в неё экземпляр вьюмодели? В свою очередь VM тоже тянет свои зависимости из контейнера.

Сейчас мне приходится шарить контейнер через статическое поле в App и обращаться к нему прямо из кода этой страницы.

var vm = App.Container.GetService<MyPageViewModel>(); // singleton

Считаю, что это грязный приём и ищу более правильное решение. Может я что-то упустил?

В идеале бы вообще от статики с контейнером избавиться.


Ответы (1 шт):

Автор решения: aepot

Спасибо @EvgeniyZ за наводку на нужные примеры.

Итого, чтобы перехватить создание экземпляра, нужно переопределить метод IXamlType.ActivateInstance() через реализацию интерфейса и подсовывание этого типа в метод IXamlMetadataProvider.GetXamlType().

На деле это выглядит так (лишний код опущу)

App.xaml.cs

public partial class App : Application
{
    private readonly IServiceProvider _services;

    public static T GetService<T>() => ((App)Current)._services.GetRequiredService<T>();
    public static object GetService(Type type) => ((App)Current)._services.GetRequiredService(type);


    public App()
    {
        InitializeComponent();
        _services = ConfigureServices().BuildServiceProvider();
    }

    private IServiceCollection ConfigureServices()
    {
        ServiceCollection services = [];

        // ... регистрация View и ViewModel страниц ...

        services.AddSingleton<MainViewModel>();
        services.AddTransient<MainWindow>();
        return services;
    }

    protected override void OnLaunched(LaunchActivatedEventArgs args)
    {
        Window window = GetService<MainWindow>();
        window.Activate();
        base.OnLaunched(args);
    }
}

Далее ещё одна partial часть класса App

partial class App : IXamlMetadataProvider
{
    private IXamlMetadataProvider _appProvider;
    private IXamlMetadataProvider AppProvider => _appProvider ??= (IXamlMetadataProvider)GetType().GetProperty("_AppProvider", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(this);

    IXamlType IXamlMetadataProvider.GetXamlType(Type type)
    {
        IXamlType xamlType = AppProvider.GetXamlType(type);
        if (xamlType?.UnderlyingType.IsSubclassOf(typeof(Microsoft.UI.Xaml.Controls.Page)) is null or false)
            return xamlType;
        return new XamlUserType(xamlType);
    }

    IXamlType IXamlMetadataProvider.GetXamlType(string fullName)
    {
        IXamlType xamlType = AppProvider.GetXamlType(fullName);
        if (xamlType?.UnderlyingType.IsSubclassOf(typeof(Microsoft.UI.Xaml.Controls.Page)) is null or false)
            return xamlType;
        return new XamlUserType(xamlType);
    }

    XmlnsDefinition[] IXamlMetadataProvider.GetXmlnsDefinitions() => AppProvider.GetXmlnsDefinitions();
}

И собственно реализация IXamlType с использованием контейнера в ActivateInstance().

public sealed partial class XamlUserType(IXamlType xamlUserType) : IXamlType
{
    public object ActivateInstance() => App.GetService(UnderlyingType);

    public object CreateFromString(string value) => xamlUserType.CreateFromString(value);

    public IXamlMember GetMember(string name) => xamlUserType.GetMember(name);

    public void AddToVector(object instance, object value) => xamlUserType.AddToVector(instance, value);

    public void AddToMap(object instance, object key, object value) => xamlUserType.AddToMap(instance, key, value);

    public void RunInitializer() => xamlUserType.RunInitializer();

    public IXamlType BaseType => xamlUserType.BaseType;
    public IXamlType BoxedType => xamlUserType.BoxedType;
    public IXamlMember ContentProperty => xamlUserType.ContentProperty;
    public string FullName => xamlUserType.FullName;
    public bool IsArray => xamlUserType.IsArray;
    public bool IsBindable => xamlUserType.IsBindable;
    public bool IsCollection => xamlUserType.IsCollection;
    public bool IsConstructible => xamlUserType.IsConstructible;
    public bool IsDictionary => xamlUserType.IsDictionary;
    public bool IsMarkupExtension => xamlUserType.IsMarkupExtension;
    public IXamlType ItemType => xamlUserType.ItemType;
    public IXamlType KeyType => xamlUserType.KeyType;
    public Type UnderlyingType => xamlUserType.UnderlyingType;
}

В итоге теперь можно зарегать в контейнере вью и вьюмодель страницы

services.AddSingleton<MyPageViewModel>();
services.AddTransient<MyPage>();

И получить вьюмодель в конструктор

public sealed partial class MyPage : Page
{
    public MyPageViewModel ViewModel { get; }

    public MyPage(MyPageViewModel vm)
    {
        InitializeComponent();
        ViewModel = vm;
    }
}

Готово.

Решение сделано на основе проекта WinUI.DependencyInjection (Документация).

→ Ссылка