AvaloniaUI - 使用基于组合根的 DI 系统将 ViewModel 注入视图的正确方法是什么?

tyh*_*dev 9 c# dependency-injection mvvm avaloniaui avalonia

我对 Avalonia/WPF、Xaml 和桌面开发总体来说是新手,所以请原谅并澄清我所展示的任何相关误解。我将继续研究可用的文档,但我很难找到能够解决我所困扰的问题的材料。

我正在尝试使用推荐的 MVVM 模式和关联的 Avalonia 项目模板,在我的 Avalonia 应用程序中实现基于组合根、构造函数注入的依赖注入系统。我对 Microsoft.Extensions.DependencyInjection 包有一定的熟悉,因此一直在尝试使用该系统。

在基于此 DI 框架以及其他框架的 WPF 和 Avalonia 教程之间,我尝试拼凑出一个可行的解决方案。我认为我已经在概念上弄清楚了注册服务和视图模型并适当地为这些类设置构造函数,以便框架在实例化时将依赖项注入到这些类中。然而,我遇到的问题是如何实现 View 类的构造函数注入。

我尝试将 MainWindow 和 MainWindowViewModel 注册为服务:

// App.axaml.cs
public partial class App : Application
    {
        private IServiceProvider _services;
        
        public override void Initialize()
        {
            AvaloniaXamlLoader.Load(this);
        }

        
        public override void OnFrameworkInitializationCompleted()
        {
            ConfigureServiceProvider();

            if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
            {
                desktop.MainWindow = _services.GetService<MainWindow>();
            }
            
            base.OnFrameworkInitializationCompleted();
        }
        
        
        private void ConfigureServiceProvider()
        {
            var services = ConfigureServices();
            _services = services.BuildServiceProvider();
        }
        
        private static IServiceCollection ConfigureServices()
        {
            var services = new ServiceCollection();
            
            services.AddTransient<MainWindow>();
            services.AddTransient<MainWindowViewModel>();

            return services;
        }
    }
Run Code Online (Sandbox Code Playgroud)

然后,目标是能够通过构造函数将 MainWindowViewModel 类注入到 MainWindow 类中,然后将该参数分配给 MainWindow 视图类的 DataContext 属性:

// MainWindow.axaml.cs
public partial class MainWindow : Window
    {
        public MainWindow(MainWindowViewModel viewModel)
        {
            DataContext = viewModel;
            InitializeComponent();
#if DEBUG
            this.AttachDevTools();
#endif
        }

        private void InitializeComponent()
        {
            AvaloniaXamlLoader.Load(this);
        }
    }

Run Code Online (Sandbox Code Playgroud)

但是,这会导致引发以下错误:

  MainWindow.axaml(1, 2): [XAMLIL] Unable to find public constructor for type MyApp.Client:MyApp.Client.Views.MainWindow() Line 1, position 2.
Run Code Online (Sandbox Code Playgroud)

如果没有无参数构造函数的存在,视图似乎无法实例化,但是,这似乎会阻止构造函数注入。

我很可能对 ViewModel 和 View 之间的预期关系存在一些根本性的误解。我遇到过许多示例,其中 ViewModel 未注册到服务容器,而是直接在 View 构造函数中实例化并分配给 DataContext 属性。我宁愿避免这种方法。

同时,我遇到的每个教程都演示了将 ViewModel 注入到相应的 View 类中,都是使用服务定位器模式来实现的,其中 DI 服务容器是显式传递的(或作为全局对象调用),并且 ViewModel 是从容器中显式解析的。

任何人都可以指导我任何示例源代码或教程来演示如何通过构造函数将 ViewModel 正确注入到视图中吗?这有可能实现吗?我可以在 MainWindow.axaml 文件中修改某些内容以启用所需的行为吗?感谢您一次又一次的关注,我非常感谢您澄清我可能存在的任何误解。

仅供参考,这是 MainWindow 标记:

  MainWindow.axaml(1, 2): [XAMLIL] Unable to find public constructor for type MyApp.Client:MyApp.Client.Views.MainWindow() Line 1, position 2.
Run Code Online (Sandbox Code Playgroud)

kek*_*eks 8

视图模型通过 DataContext 而不是构造函数注入与视图关联。请注意,单个视图可以重复使用(特别是如果您正在处理虚拟化列表)。

一般来说,您的 DI 根本不应该了解大部分视图部分,它应该只关心 ViewModel 和较低层。

通常不是通过 DI 创建视图,而是通过将特定属性绑定到 ContentControl 的其他视图通过视图定位器进行定位,例如

<ContentControl Content="{Binding MySubViewModel}" />
Run Code Online (Sandbox Code Playgroud)

(您可以在 avalonia.mvvm 模板中找到一个简单的视图定位器,您可以根据您的需要对其进行调整)。当需要从视图模型代码中显示新的顶级视图时,他们通常会实现某种窗口管理器来管理顶级视图,并且可以通过 DI 从视图模型访问,例如

    public class ViewManager : IViewManager
    {
        private Window CreateWindowForModel(object model)
        {
            foreach (var template in Application.Current.DataTemplates)
            {
                if (template.Match(model))
                {
                    var control = template.Build(model);
                    if (control is Window w)
                        return w;
                    return new Window { Content = control };
                }
            }

            throw new KeyNotFoundException("Unable to find view for model: " + model);
        }

        public void ShowWindow(object model) => CreateWindowForModel(model).Show();
    }
Run Code Online (Sandbox Code Playgroud)

然后将实现添加IViewManager到您的 DI 中。

请注意,这种方法对于所有 XAML 框架都是可重用的,并且可以在不同平台之间完全重用视图模型(例如,如果您想使用 Xamarin 实现移动 UI,使用 Avalonia 实现桌面),只需少量 UI 工具包特定服务。