AvaloniaUI - 使用基于组合根的 DI 系统将 ViewModel 注入视图的正确方法是什么?
AvaloniaUI - What is the proper way to inject ViewModels into Views using composition-root based DI system?
我是 Avalonia/WPF、Xaml 和一般桌面开发的新手,所以请原谅并澄清我所展示的任何相关误解。我将继续研究可用的文档,但我很难找到 material 来解决我遇到的问题。
我正在尝试在我的 Avalonia 应用程序中实现基于组合根、构造函数注入的依赖注入系统,使用推荐的 MVVM 模式和关联的 Avalonia 项目模板。我对 Microsoft.Extensions.DependencyInjection 包有些熟悉,所以一直在尝试使用这个系统。
在基于此 DI 框架以及其他框架的 WPF 和 Avalonia 教程之间,我试图拼凑出一个可行的解决方案。我想我已经在概念上弄清楚了注册服务和 ViewModel 以及为这些 classes 适当地设置构造函数,这样框架将在实例化时将依赖项注入这些 classes。但是,我遇到困难的地方是如何为 View classes.
实现构造函数注入
我尝试将 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;
}
}
目标是能够通过构造函数将 MainWindowViewModel class 注入 MainWindow class,然后将该参数分配给 MainWindow 视图的 DataContext 属性-class:
// 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);
}
}
但是,这会导致出现以下错误:
MainWindow.axaml(1, 2): [XAMLIL] Unable to find public constructor for type MyApp.Client:MyApp.Client.Views.MainWindow() Line 1, position 2.
如果没有无参数构造函数,似乎无法实例化视图,但是,这似乎可以防止构造函数注入。
我很可能对 ViewModel 和 View 之间的预期关系有一些根本性的误解。我遇到过许多示例,其中 ViewModel 未注册到服务容器,而是直接在 View 构造函数中实例化并分配给 DataContext 属性。我宁愿避免这种方法。
与此同时,我遇到的每个教程都演示了将 ViewModel 注入相应的视图 classes,使用服务定位器模式这样做,其中显式传递 DI 服务容器(或作为全局对象调用) ) 并且 ViewModel 是从容器中显式解析的。
任何人都可以告诉我任何示例源代码或教程,它们演示了如何通过构造函数将 ViewModels 正确地注入到 Views 中吗?这有可能实现吗?我可以在 MainWindow.axaml 文件中修改某些内容以启用所需的行为吗?谢谢你的一次又一次,如果我有任何误解,我将不胜感激。
仅供参考,这是 MainWindow 标记:
// MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MyApp.Client.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="MyApp.Client.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
x:CompileBindings="True"
Icon="/Assets/avalonia-logo.ico"
Title="MyApp">
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Window>
视图模型通过 DataContext 而不是构造函数注入与视图相关联。请注意,单个视图可以重复使用(尤其是在处理虚拟化列表时)。
一般来说,您的 DI 根本不应该知道大部分 视图部分,它应该只关心 ViewModel 和较低层。
视图通常不是通过 DI 创建,而是通过视图定位器由将特定属性绑定到 ContentControl 的其他视图定位,例如。 g.
<ContentControl Content="{Binding MySubViewModel} />
(您可以在 avalonia.mvvm 模板中找到一个简单的视图定位器,您可以根据需要对其进行调整)。
当需要从您的视图模型代码中显示一个新的 top-level 视图时,他们通常会实现某种 window 管理器来管理 top-level 视图并且可以通过 DI 从视图模型访问, e. g.
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();
}
然后您将 IViewManager
实现添加到您的 DI。
请注意,此方法可重用于所有 XAML 框架,并且可以在各种平台之间完全重用视图模型(例如,如果您想使用 Xamarin 实现移动设备 UI 并使用 Xamarin 实现桌面设备Avalonia)只有少数 UI-toolkit 特定服务。
我是 Avalonia/WPF、Xaml 和一般桌面开发的新手,所以请原谅并澄清我所展示的任何相关误解。我将继续研究可用的文档,但我很难找到 material 来解决我遇到的问题。
我正在尝试在我的 Avalonia 应用程序中实现基于组合根、构造函数注入的依赖注入系统,使用推荐的 MVVM 模式和关联的 Avalonia 项目模板。我对 Microsoft.Extensions.DependencyInjection 包有些熟悉,所以一直在尝试使用这个系统。
在基于此 DI 框架以及其他框架的 WPF 和 Avalonia 教程之间,我试图拼凑出一个可行的解决方案。我想我已经在概念上弄清楚了注册服务和 ViewModel 以及为这些 classes 适当地设置构造函数,这样框架将在实例化时将依赖项注入这些 classes。但是,我遇到困难的地方是如何为 View classes.
实现构造函数注入我尝试将 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;
}
}
目标是能够通过构造函数将 MainWindowViewModel class 注入 MainWindow class,然后将该参数分配给 MainWindow 视图的 DataContext 属性-class:
// 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);
}
}
但是,这会导致出现以下错误:
MainWindow.axaml(1, 2): [XAMLIL] Unable to find public constructor for type MyApp.Client:MyApp.Client.Views.MainWindow() Line 1, position 2.
如果没有无参数构造函数,似乎无法实例化视图,但是,这似乎可以防止构造函数注入。
我很可能对 ViewModel 和 View 之间的预期关系有一些根本性的误解。我遇到过许多示例,其中 ViewModel 未注册到服务容器,而是直接在 View 构造函数中实例化并分配给 DataContext 属性。我宁愿避免这种方法。
与此同时,我遇到的每个教程都演示了将 ViewModel 注入相应的视图 classes,使用服务定位器模式这样做,其中显式传递 DI 服务容器(或作为全局对象调用) ) 并且 ViewModel 是从容器中显式解析的。
任何人都可以告诉我任何示例源代码或教程,它们演示了如何通过构造函数将 ViewModels 正确地注入到 Views 中吗?这有可能实现吗?我可以在 MainWindow.axaml 文件中修改某些内容以启用所需的行为吗?谢谢你的一次又一次,如果我有任何误解,我将不胜感激。
仅供参考,这是 MainWindow 标记:
// MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MyApp.Client.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="MyApp.Client.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
x:CompileBindings="True"
Icon="/Assets/avalonia-logo.ico"
Title="MyApp">
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Window>
视图模型通过 DataContext 而不是构造函数注入与视图相关联。请注意,单个视图可以重复使用(尤其是在处理虚拟化列表时)。
一般来说,您的 DI 根本不应该知道大部分 视图部分,它应该只关心 ViewModel 和较低层。
视图通常不是通过 DI 创建,而是通过视图定位器由将特定属性绑定到 ContentControl 的其他视图定位,例如。 g.
<ContentControl Content="{Binding MySubViewModel} />
(您可以在 avalonia.mvvm 模板中找到一个简单的视图定位器,您可以根据需要对其进行调整)。 当需要从您的视图模型代码中显示一个新的 top-level 视图时,他们通常会实现某种 window 管理器来管理 top-level 视图并且可以通过 DI 从视图模型访问, e. g.
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();
}
然后您将 IViewManager
实现添加到您的 DI。
请注意,此方法可重用于所有 XAML 框架,并且可以在各种平台之间完全重用视图模型(例如,如果您想使用 Xamarin 实现移动设备 UI 并使用 Xamarin 实现桌面设备Avalonia)只有少数 UI-toolkit 特定服务。