wpf 如何修复循环依赖关系

fnatzsnv  于 2023-01-10  发布在  其他
关注(0)|答案(3)|浏览(208)

我最近才开始在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

w46czmvw

w46czmvw1#

你不应该把视图模型的具体版本传递给mainviewmodel,如果它需要知道它们,当你有50个视图的时候你该怎么做?

public MainViewModel(NavService navService)
{

导航服务应该按需解决具体的示例。更像。

NavService.CurrentView = 
 
 (IInitiatedViewModel)serviceProvider.GetService(typeofregisteredinterface);

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需要传入。
您通常需要视图模型接口:

interface IInitiatedViewModel
    {
        Task Initiate();
    }

因此,在视图模型示例化之后,您可以为它获取任何数据。

public async Task Initiate()
    {
        Suppliers = await _repository.GetAddressesByTypeAsync((int)AddressType.Supplier);
        if(Suppliers.Count == 1)
        {
            ChosenSupplier = Suppliers[0];
        }
    }

我建议你也应该有一个列表,其中有你的视图模型和视图的类型和描述。
然后你可以从特定的视图模型类型中抽象出导航,他们选择的视图是什么,也就是描述中的什么,它的视图模型接口是Type中的什么。
如果需要,您可以扩展这个原则,在其中包含一个可选的工厂方法。
和逻辑可以位于导航服务或其他注入类中。

ar5n3qh5

ar5n3qh52#

不要在构造函数中配置IoC容器!将相关代码移到OnStartup重写或Application.Startup事件处理程序中。构造函数仅用于初始化/配置示例。构造函数必须始终快速返回。
你的依赖注入实现错误。因为你现在已经实现了它,它违背了它的目的。第一步总是遵循依赖反转原则(SOLID中的 * D *):不依赖于具体的类型。而是依赖于它们的抽象。
这意味着你必须引入抽象类和接口,然后只把这些抽象注入到你的类型中。
循环依赖通常是由于设计错误的职责而引入的。IoC揭示了这个设计缺陷,因为依赖是公开的,通常是在构造函数中。
从类级(模块)设计的Angular 来看,
当类型A依赖于B并且B依赖于A时(AB
那么您可以使用以下选项来修复它:

    • a)**AB应合并为一个类(A
    • b)**BA或其他类的职责过多或了解过多。将相关职责移回A。现在B必须使用A来履行其职责
A ⟷ B ➽ A ⟶ B
    • c)**共享逻辑必须被移动/提取到第三类型C
A ⟷ B ➽ A ⟶ C ⟵ B
    • d)**引入接口以反转依赖关系(依赖关系反转原理):
IA    
          ⬀ ⬉        
A ⟷ B ➽ A   B  
          ⬊ ⬃  
           IB

这意味着:

    • a)**A知道如何导航。这可能导致A承担过多责任。证据:需要导航的每个类型也必须实现完整的逻辑(重复代码)
    • b)**每个类型都知道它可以/被允许导航到哪里。而导航逻辑由专用类型封装(NavigationService),则只有客户端知道实际有效的目的地。这增加了代码的健壮性。这将意味着A将必须向B提供自变量以允许B履行其职责。B现在不知道A的存在(AB)。
    • c)因为依赖项 * 没有 * 被引入以使特定的类成员(API)可用, c) 不能应用于您的情况。在您的情况下,您的B依赖于A作为一个单独的类型(示例而不是示例成员)。
    • d)**因为依赖关系在构造函数中表现出来,所以引入接口不会解决循环依赖关系

要修复原始问题,您有三种选择:
1.隐藏工厂后面的依赖项(不推荐)
1.修改你的设计。NavigationService知道的太多了。按照你的模式,NavigationService必须显式地知道每个视图模型类(或者每个导航目的地)。
1.使用属性注入代替构造函数注入(不推荐)
以下示例将使用Func<TProduct>而不是抽象工厂来简化示例。
这些示例还使用enum作为目标标识符,以避免使用魔术字符串:

    • 导航ID. cs**
public enum NavigationId
{
  None = 0,
  HomeViewModel
}

1)隐藏依赖项

通过实现抽象工厂模式,让类依赖于(抽象)工厂,而不是依赖于显式类型。
注意,仍然存在一个隐式循环依赖,它只是隐藏在工厂后面,依赖只是从构造函数中移除(构造NavigationService不再需要构造HomeViewModel,反之亦然)。
您必须引入接口(例如IHomeViewModel)来完全消除循环依赖。
你也会看到,为了添加更多的目的地,你必须修改NavigationService。这是一个很好的指标,你实现了一个坏的设计。事实上,你已经违反了开放-封闭原则(* O * 在固体)。

    • 导航服务. cs**
class NavigationService
{
  private HomeViewModel HomeViewModel { get; }

  // Constructor.
  // Because of the factory the circular dependency of the constructor
  // is broken. On class level the dependency still exists,
  // but could be removed by introducing a 'IHomeViewModel'  interface.
  public NavigationService(Func<HomeViewModel> homeViewModelFactory)
    => this.HomeViewModel = homeViewModelFactory.Invoke();
}
    • 家庭视图模型. cs**
class HomeViewModel
{
  private NavigationService NavigationService { get; }

  // Constructor
  public HomeViewModel(Func<NavigationService> navigationServiceFactory)
    => this.NavigationService = navigationServiceFactory.Invoke();
}
    • 应用程序xaml. cs**

配置IoC容器以注入工厂。在本例中,工厂是简单的Func<T>委托。对于更复杂的场景,您可能需要实现抽象工厂。

protected override void OnStartup(StartupEventArgs e)
{  
  IServiceCollection _services = new ServiceCollection();
 
  // Because ServiceCollection registration members return the current ServiceCollection instance
  // you can chain registrations       
  _services.AddSingleton<HomeViewModel>()
    .AddSingleton<NavigationService>();
    .AddSingleton<Func<HomeViewModel>>(serviceProvider => serviceProvider.GetRequiredService<HomeViewModel>)
    .AddSingleton<Func<NavigationService>>(serviceProvider => serviceProvider.GetRequiredService<NavigationService>);
}

2)修复类设计(职责)

每个类都应该知道它被允许导航到的导航目的地。没有一个类应该知道它可以导航到的其他类,或者它是否可以导航。
与解**1)**相反,循环依赖被完全解除。

public class Navigator : INavigator
{
  // The critical knowledge of particular types is now removed
  public Navigator()
  {}

  // Every class that wants to navigate to a destination 
  // must know/provide this destination
  public void Navigate(object navigationDestination) 
    => this.CurrentSource = navigationDestination;

  public object CurrentSource { get; set; }
}

3)属性注入

. NET依赖项注入框架不支持属性注入。但是,通常不建议使用属性注入。除了隐藏依赖项之外,它还带来了使错误的类设计工作的危险(如本示例中的情况)。
虽然**2)是推荐的解决方案,但您可以将两种解决方案1)2)**结合起来,并决定特定导航源需要了解多少目的地信息。

public class Navigator : INavigator
{
  public Navigator(Func<HomeViewModel> homeViewModelFactory)
  {
    this.HomeViewModelFactory = homeViewModelFactory;

    // This reveals that the class knows too much.
    // To introduce more destinations, 
    // you will always have to modify this code.
    // Same applies to your switch-statement.
    // A switch-statement is another good indicator 
    // for breaking the Open-Closed principle
    this.NavigationDestinationTable = new Dictionary<NavigationId, Func<object>>()
    {
      { NavigationId.HomeViewModel, homeViewModelFactory }
    };
  }

  public void Navigate(NavigationId navigationId)
    => this.CurrentSource = this.NavigationDestinationTable.TryGetValue(navigationId, out Func<object> factory) ? factory.Invoke() : default;

  public void Navigate(object navigationDestination)
    => this.CurrentSource = navigationDestination;

  public object CurrentSource { get; set; }
  public Func<HomeViewModel> HomeViewModelFactory { get; }
  internal Dictionary<NavigationId, Func<object>> NavigationDestinationTable { get; }
}
nhn9ugyo

nhn9ugyo3#

这是一个设计问题,主视图模型紧密耦合,作为一个通道,违反了SRP(Single Responsibility Principle,单一责任原则),导航服务和其他视图模型都显式依赖,这是循环依赖问题的直接原因。
为简单起见,请注意NavService的以下重构

public abstract class ViewModel : ObservableObject {

}

public interface INavigationService {
    object CurrentView { get; }
    void NavigateTo<T>() where T : ViewModel;
}

public class NavService : INavigationService, ObservableObject {
    private readonly Func<Type, object> factory;
    private object _currentView;
    
    public NavService(Func<Type, object> factory) {
        this.factory = factory;
    }
    
    public object CurrentView {
        get => _currentView;
        private set {
            _currentView = value;
            OnPropertyChanged();
        }
    }
    
    public void NavigateTo<T>() where T: ViewModel {
        object viewModel = factory.Invoke(typeof(T)) 
            ?? throw new InvalidOperationException("Error message here");
        CurrentView = viewModel;
    }
}

这个服务在注册时应该配置用于获取视图模型的工厂。

public partial class App : Application {
    private readonly IServiceProvider _serviceProvider;
    public App() {
        IServiceCollection _services = new ServiceCollection();
        
        _services.AddSingleton<MainViewModel>();
        _services.AddSingleton<HomeViewModel>();
        _services.AddSingleton<SettingsViewModel>();
        
        _services.AddSingleton<DataService>();
        _services.AddSingleton<INavigationService, NavService>()(sp => {
            return new NavService(type => sp.GetRequiredService(type));
        });
        
        _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);
    }
}

这完全解耦了主视图模型和其他视图模型,但允许强类型导航并消除了循环依赖,因为在这个特定场景中,模型只需要知道导航服务

public class MainViewModel : ViewModel {
    private INavigationService _navService;
    
    /* Ctor */
    public MainViewModel(INavigationService navService) {
        NavService = navService;
        HomeViewCommand = new RelayCommand(o => true, o => { NavService.NavigateTo<HomeViewModel>(); });
        SettingsViewCommand = new RelayCommand(o => true, o => { NavService.NavigateTo<SettingsViewModel(); });
    }
    
    public INavigationService NavService {
        get => _navService;
        set {
            _navService = value;
            OnPropertyChanged();
        }
    }
    /* Commands */
    public RelayCommand SettingsViewCommand { get; set; }
    public RelayCommand HomeViewCommand { get; set; }

}

请注意,视图中不需要任何更改,导航服务现在也足够灵活,允许将任意数量的视图模型引入到系统中,而不需要对其进行任何更改。

相关问题