Как подружить 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 шт):
Спасибо @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 (Документация).