我最近才开始在WPF项目中使用Microsoft.Extensions.DependencyInjection
nuget包,因为我想开始学习更多关于DI的知识。
- 问题**
每当我尝试访问除MainViewModel
之外的任何其他ViewModel
的依赖项时,总是得到Circular Dependency Exception
- 这就是我目前所做的**
我已将这两个Nuget包安装到我的项目中Microsoft.Extensions.Hosting --version 7.0.0
Microsoft.Extensions.DependencyInjection --version 7.0.0
然后我继续在App.xaml.cs
中创建了容器
public partial class App : Application
{
private readonly ServiceProvider _serviceProvider;
public App()
{
IServiceCollection _services = new ServiceCollection();
_services.AddSingleton<MainViewModel>();
_services.AddSingleton<HomeViewModel>();
_services.AddSingleton<SettingsViewModel>();
_services.AddSingleton<DataService>();
_services.AddSingleton<NavService>();
_services.AddSingleton<MainWindow>(o => new MainWindow
{
DataContext = o.GetRequiredService<MainViewModel>()
});
_serviceProvider = _services.BuildServiceProvider();
}
protected override void OnStartup(StartupEventArgs e)
{
var MainWindow = _serviceProvider.GetRequiredService<MainWindow>();
MainWindow.Show();
base.OnStartup(e);
}
}
在我的App.xaml
中,我还定义了一些DataTemplates
,它们允许我基于它们的DataType
显示不同的视图
<Application.Resources>
<DataTemplate DataType="{x:Type viewModel:HomeViewModel}">
<view:HomeView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModel:SettingsViewModel}">
<view:SettingsView/>
</DataTemplate>
</Application.Resources>
然后我继续创建了我的MainWindow. xaml
<Window x:Class="Navs.MainWindow"
...>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Border>
<StackPanel>
<Button Height="25" Content="Home" Command="{Binding HomeViewCommand}"/>
<Button Height="25" Content="Settings" Command="{Binding SettingsViewCommand}"/>
</StackPanel>
</Border>
<ContentControl Grid.Column="1" Content="{Binding NavService.CurrentView}">
</ContentControl>
</Grid>
</Window>
以及对应的ViewModel
public class MainViewModel : ObservableObject
{
private NavService _navService;
public NavService NavService
{
get => _navService;
set
{
_navService = value;
OnPropertyChanged();
}
}
/* Commands */
public RelayCommand SettingsViewCommand { get; set; }
public RelayCommand HomeViewCommand { get; set; }
public MainViewModel(NavService navService, HomeViewModel homeViewModel, SettingsViewModel settingsViewModel)
{
NavService = navService;
HomeViewCommand = new RelayCommand(o => true, o => { NavService.CurrentView = homeViewModel; });
SettingsViewCommand = new RelayCommand(o => true, o => { NavService.CurrentView = settingsViewModel; });
}
}
正如您所看到的,在依赖注入的帮助下,我现在可以通过构造函数访问在容器中注册的对象。
我还创建了两个UserControls
UserControl1
<Grid>
<StackPanel VerticalAlignment="Center">
<Button Height="25" Content="Click me" Command="{Binding OpenWindowCommand}" />
<Button Content="Settings View" Command="{Binding SettingsViewCommand}" Height="25" />
</StackPanel>
</Grid>
它对应的是ViewModel
public class HomeViewModel
{
public RelayCommand SettingsViewCommand { get; set; }
public HomeViewModel()
{
}
}
然后是UserControl2
<Grid>
<StackPanel VerticalAlignment="Center">
<TextBox Text="{Binding Message}"
Height="25"/>
<Button Height="25" Content="Home View" Command="{Binding HomeViewCommand}"/>
<Button Height="25" Content="Fetch" Command="{Binding FetchDataCommand}"/>
</StackPanel>
</Grid>
与之对应的ViewModel
public class SettingsViewModel : ObservableObject
{
public string Message { get; set; }
public RelayCommand HomeViewCommand { get; set; }
public RelayCommand FetchDataCommand { get; set; }
public SettingsViewModel()
{
}
}
NavService.cs
public class NavService : ObservableObject
{
private object _currentView;
public object CurrentView
{
get => _currentView;
set
{
_currentView = value;
OnPropertyChanged();
}
}
private HomeViewModel HomeViewModel { get; set; }
private SettingsViewModel SettingsViewModel { get; set; }
public NavService(HomeViewModel homeViewModel, SettingsViewModel settingsViewModel)
{
HomeViewModel = homeViewModel;
SettingsViewModel = settingsViewModel;
CurrentView = HomeViewModel;
}
public void NavigateTo(string viewName)
{
switch (viewName)
{
case "Settings":
CurrentView = SettingsViewModel;
break;
case "Home":
CurrentView = HomeViewModel;
break;
}
}
}
这一切都工作得很好,当我接受HomeViewModel
并试图将NavService
作为构造函数传入时,问题就出现了。
public HomeViewModel(NavService navService)
{
}
此时,它将引发异常。
我希望能够从各种Views
访问NavService
,以便可以从多个位置更改NavService.CurrentView
。
3条答案
按热度按时间w46czmvw1#
你不应该把视图模型的具体版本传递给mainviewmodel,如果它需要知道它们,当你有50个视图的时候你该怎么做?
导航服务应该按需解决具体的示例。更像。
serviceprovider是一个示例类,除非你在启动时示例化app.xaml.cs中的所有内容,解析所有内容,否则你需要对它进行某种引用。
我建议不要这样做,因为在任何真实的的应用程序中,你可能会有比两个视图模型更复杂的东西。
请注意,服务提供者是:
https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceprovider?view=dotnet-plat-ext-7.0
它已经定义了一个接口IServiceProvider,因此您可以轻松地注入一个mock用于测试目的。
不过,您在app.xaml中使用的serviceprovider需要传入。
您通常需要视图模型接口:
因此,在视图模型示例化之后,您可以为它获取任何数据。
我建议你也应该有一个列表,其中有你的视图模型和视图的类型和描述。
然后你可以从特定的视图模型类型中抽象出导航,他们选择的视图是什么,也就是描述中的什么,它的视图模型接口是Type中的什么。
如果需要,您可以扩展这个原则,在其中包含一个可选的工厂方法。
和逻辑可以位于导航服务或其他注入类中。
ar5n3qh52#
不要在构造函数中配置IoC容器!将相关代码移到
OnStartup
重写或Application.Startup
事件处理程序中。构造函数仅用于初始化/配置示例。构造函数必须始终快速返回。你的依赖注入实现错误。因为你现在已经实现了它,它违背了它的目的。第一步总是遵循依赖反转原则(SOLID中的 * D *):不依赖于具体的类型。而是依赖于它们的抽象。
这意味着你必须引入抽象类和接口,然后只把这些抽象注入到你的类型中。
循环依赖通常是由于设计错误的职责而引入的。IoC揭示了这个设计缺陷,因为依赖是公开的,通常是在构造函数中。
从类级(模块)设计的Angular 来看,
当类型
A
依赖于B
并且B
依赖于A
时(A
B
)那么您可以使用以下选项来修复它:
A
和B
应合并为一个类(A
)B
对A
或其他类的职责过多或了解过多。将相关职责移回A
。现在B
必须使用A
来履行其职责C
:这意味着:
A
知道如何导航。这可能导致A
承担过多责任。证据:需要导航的每个类型也必须实现完整的逻辑(重复代码)NavigationService
),则只有客户端知道实际有效的目的地。这增加了代码的健壮性。这将意味着A
将必须向B
提供自变量以允许B
履行其职责。B
现在不知道A
的存在(A
B
)。B
依赖于A
作为一个单独的类型(示例而不是示例成员)。要修复原始问题,您有三种选择:
1.隐藏工厂后面的依赖项(不推荐)
1.修改你的设计。
NavigationService
知道的太多了。按照你的模式,NavigationService
必须显式地知道每个视图模型类(或者每个导航目的地)。1.使用属性注入代替构造函数注入(不推荐)
以下示例将使用
Func<TProduct>
而不是抽象工厂来简化示例。这些示例还使用
enum
作为目标标识符,以避免使用魔术字符串:1)隐藏依赖项
通过实现抽象工厂模式,让类依赖于(抽象)工厂,而不是依赖于显式类型。
注意,仍然存在一个隐式循环依赖,它只是隐藏在工厂后面,依赖只是从构造函数中移除(构造
NavigationService
不再需要构造HomeViewModel
,反之亦然)。您必须引入接口(例如
IHomeViewModel
)来完全消除循环依赖。你也会看到,为了添加更多的目的地,你必须修改
NavigationService
。这是一个很好的指标,你实现了一个坏的设计。事实上,你已经违反了开放-封闭原则(* O * 在固体)。配置IoC容器以注入工厂。在本例中,工厂是简单的
Func<T>
委托。对于更复杂的场景,您可能需要实现抽象工厂。2)修复类设计(职责)
每个类都应该知道它被允许导航到的导航目的地。没有一个类应该知道它可以导航到的其他类,或者它是否可以导航。
与解**1)**相反,循环依赖被完全解除。
3)属性注入
. NET依赖项注入框架不支持属性注入。但是,通常不建议使用属性注入。除了隐藏依赖项之外,它还带来了使错误的类设计工作的危险(如本示例中的情况)。
虽然**2)是推荐的解决方案,但您可以将两种解决方案1)和2)**结合起来,并决定特定导航源需要了解多少目的地信息。
nhn9ugyo3#
这是一个设计问题,主视图模型紧密耦合,作为一个通道,违反了SRP(Single Responsibility Principle,单一责任原则),导航服务和其他视图模型都显式依赖,这是循环依赖问题的直接原因。
为简单起见,请注意
NavService
的以下重构这个服务在注册时应该配置用于获取视图模型的工厂。
这完全解耦了主视图模型和其他视图模型,但允许强类型导航并消除了循环依赖,因为在这个特定场景中,模型只需要知道导航服务
请注意,视图中不需要任何更改,导航服务现在也足够灵活,允许将任意数量的视图模型引入到系统中,而不需要对其进行任何更改。