如何从 ServiceLocator 转向依赖注入?具体例子

How to move from ServiceLocator to Dependency Injection? Specific example

问题是从 ServiceLocator 反模式转移到 Dependency Injection。鉴于本人经验不足,无法将DI原理移植到现在实现的代码中。

总结

摘要部分可选阅读。您可能想对某事发表评论,建议。

该程序的主要目的是合并特定信息的占位符字段的过程。大量的信息使得需要有基础设施。如表单、服务、数据库。我对这项任务有一些经验。我设法基于 WinForms 创建了一个类似的程序。它甚至有效!但是在模式、维护、可扩展性和性能方面,代码很糟糕。了解编程是一种爱好很重要。这不是主要的教育或工作。

WinForms 上执行所描述任务的体验非常糟糕。我开始研究模式和新平台。起点是UWPMVVM。需要注意的是绑定机制很神奇

途中第一个问题独立解决。它与通过 ShellPage 中的 ShellViewModel 中的 NavigationView 中的导航有关。随着创建 NavigationService。它基于 Windows Template Studio.

中的模板

既然有一个有效的 WinForms 程序及其反模式方向,就有时间和愿望正确地做所有事情

现在我面临架构问题。称为 ServiceLocator(或 ViewModelLocator)。我在 Microsoft 的示例中找到它,包括 Windows Template Studio 的模板。在这样做的过程中,我又落入了反模式陷阱。如上所述,我不想再这样了。

作为解决方案出现的第一件事是 dependency injection。鉴于本人经验不足,无法将DI原理移植到现在实现的代码中。

当前实施

应用 UWP 的起点是 app.xaml.cs。重点是将控制权转移到 ActivationService。它的任务是将 Frame 添加到 Window.Current.Content 并导航到默认页面 - MainPageMicrosoft documentation.

ViewModelLocator是单例。对其 属性 的第一次调用将调用构造函数。

private static ViewModelLocator _current;
public static ViewModelLocator Current => _current ?? (_current = new ViewModelLocator());
// Constructor
private ViewModelLocator(){...}

使用ViewModelLocatorViewPage)是这样的,ShellPage

private ShellViewModel ViewModel => ViewModelLocator.Current.ShellViewModel;

使用ViewModelLocatorViewModel类似,ShellViewModel:

 private static NavigationService NavigationService => ViewModelLocator.Current.NavigationService;

移动到 DI

如上所示,

ShellViewModelViewModelLocator 得到 NavigationService。这个时候怎么去DI?其实程序很小。现在是远离反模式的好时机。

代码

ViewModelLocator

private static ViewModelLocator _current;
public static ViewModelLocator Current => _current ?? (_current = new ViewModelLocator());

private ViewModelLocator()
{
    // Services
    SimpleIoc.Default.Register<NavigationService>();

    // ViewModels and NavigationService items
    Register<ShellViewModel, ShellPage>();
    Register<MainViewModel, MainPage>();
    Register<SettingsViewModel, SettingsPage>();
}

private void Register<TViewModel, TView>()
    where TViewModel : class
    where TView : Page
{
    SimpleIoc.Default.Register<TViewModel>();
    NavigationService.Register<TViewModel, TView>();
}

public ShellViewModel ShellViewModel => SimpleIoc.Default.GetInstance<ShellViewModel>();
public MainViewModel MainViewModel => SimpleIoc.Default.GetInstance<MainViewModel>();
public SettingsViewModel SettingsViewModel => SimpleIoc.Default.GetInstance<SettingsViewModel>();

public NavigationService NavigationService => SimpleIoc.Default.GetInstance<NavigationService>();

ShellPage:页面

private ShellViewModel ViewModel => ViewModelLocator.Current.ShellViewModel;

public ShellPage()
{
    InitializeComponent();

    // shellFrame and navigationView from XAML
    ViewModel.Initialize(shellFrame, navigationView);
}

ShellViewModel : ViewModelBase

private bool _isBackEnabled;
private NavigationView _navigationView;
private NavigationViewItem _selected;

private ICommand _itemInvokedCommand;
public ICommand ItemInvokedCommand => _itemInvokedCommand ?? (_itemInvokedCommand = new RelayCommand<NavigationViewItemInvokedEventArgs>(OnItemInvoked));

private static NavigationService NavigationService => ViewModelLocator.Current.NavigationService;

public bool IsBackEnabled
{
    get => _isBackEnabled;
    set => Set(ref _isBackEnabled, value);
}

public NavigationViewItem Selected
{
    get => _selected;
    set => Set(ref _selected, value);
}

public void Initialize(Frame frame, NavigationView navigationView)
{
    _navigationView = navigationView;
    _navigationView.BackRequested += OnBackRequested;

    NavigationService.Frame = frame;
    NavigationService.Navigated += Frame_Navigated;
    NavigationService.NavigationFailed += Frame_NavigationFailed;
}

private void OnItemInvoked(NavigationViewItemInvokedEventArgs args)
{
    if (args.IsSettingsInvoked)
    {
        NavigationService.Navigate(typeof(SettingsViewModel));
        return;
    }

    var item = _navigationView.MenuItems.OfType<NavigationViewItem>().First(menuItem => (string)menuItem.Content == (string)args.InvokedItem);
    var pageKey = GetPageKey(item);
    NavigationService.Navigate(pageKey);
}
private void OnBackRequested(NavigationView sender, NavigationViewBackRequestedEventArgs args)
{
    NavigationService.GoBack();
}

private void Frame_Navigated(object sender, NavigationEventArgs e)
{
    IsBackEnabled = NavigationService.CanGoBack;
    if (e.SourcePageType == typeof(SettingsPage))
    {
        Selected = _navigationView.SettingsItem as NavigationViewItem;
        return;
    }

    Selected = _navigationView.MenuItems
        .OfType<NavigationViewItem>()
        .FirstOrDefault(menuItem => IsMenuItemForPageType(menuItem, e.SourcePageType));
}
private void Frame_NavigationFailed(object sender, NavigationFailedEventArgs e)
{
    throw e.Exception;
}

private bool IsMenuItemForPageType(NavigationViewItem item, Type sourcePageType)
{
    var pageKey = GetPageKey(item);
    var navigatedPageKey = NavigationService.GetNameOfRegisteredPage(sourcePageType);
    return pageKey == navigatedPageKey;
}

private Type GetPageKey(NavigationViewItem item) => Type.GetType(item.Tag.ToString());

更新 1

我对 ServiceLocatorViewModelLocator 之间的相等性有误吗?

Called ServiceLocator (or ViewModelLocator)

本质上,当前任务是连接ViewViewModelNavigationService 超出了本任务的范围。那么它不应该在 ViewModelLocator 中吗?

正如@Maess 指出的那样,您(现在)面临的最大挑战是将静态依赖项重构为构造函数注入。例如,你的 ShellViewModel 应该有一个像这样的构造函数:

public ShellViewModel(INavigationService navigation)

完成后,您可以设置一个 DI 框架(如 NInject),其中包含所有依赖项(有点像 SimpleIoC 的东西),理想情况下,从容器(构建其他一切)。通常这是应用程序的主视图模型。

我已经在多个项目(WPF 和 UWP)上成功地完成了这项工作,而且效果很好。唯一需要注意的是在运行时创建视图模型时(就像您经常做的那样),通过注入工厂来完成。