如何从 ServiceLocator 转向依赖注入?具体例子
How to move from ServiceLocator to Dependency Injection? Specific example
问题是从 ServiceLocator
反模式转移到 Dependency Injection
。鉴于本人经验不足,无法将DI原理移植到现在实现的代码中。
总结
摘要部分可选阅读。您可能想对某事发表评论,建议。
该程序的主要目的是合并特定信息的占位符字段的过程。大量的信息使得需要有基础设施。如表单、服务、数据库。我对这项任务有一些经验。我设法基于 WinForms
创建了一个类似的程序。它甚至有效!但是在模式、维护、可扩展性和性能方面,代码很糟糕。了解编程是一种爱好很重要。这不是主要的教育或工作。
在 WinForms
上执行所描述任务的体验非常糟糕。我开始研究模式和新平台。起点是UWP
和MVVM
。需要注意的是绑定机制很神奇
途中第一个问题独立解决。它与通过 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
并导航到默认页面 - MainPage
。 Microsoft documentation.
ViewModelLocator
是单例。对其 属性 的第一次调用将调用构造函数。
private static ViewModelLocator _current;
public static ViewModelLocator Current => _current ?? (_current = new ViewModelLocator());
// Constructor
private ViewModelLocator(){...}
使用ViewModelLocator
和View
(Page
)是这样的,ShellPage
:
private ShellViewModel ViewModel => ViewModelLocator.Current.ShellViewModel;
使用ViewModelLocator
与ViewModel
类似,ShellViewModel
:
private static NavigationService NavigationService => ViewModelLocator.Current.NavigationService;
移动到 DI
如上所示,ShellViewModel
从 ViewModelLocator
得到 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
我对 ServiceLocator
和 ViewModelLocator
之间的相等性有误吗?
Called ServiceLocator
(or ViewModelLocator
)
本质上,当前任务是连接View
和ViewModel
。 NavigationService
超出了本任务的范围。那么它不应该在 ViewModelLocator
中吗?
正如@Maess 指出的那样,您(现在)面临的最大挑战是将静态依赖项重构为构造函数注入。例如,你的 ShellViewModel 应该有一个像这样的构造函数:
public ShellViewModel(INavigationService navigation)
完成后,您可以设置一个 DI 框架(如 NInject),其中包含所有依赖项(有点像 SimpleIoC 的东西),理想情况下,从容器(构建其他一切)。通常这是应用程序的主视图模型。
我已经在多个项目(WPF 和 UWP)上成功地完成了这项工作,而且效果很好。唯一需要注意的是在运行时创建视图模型时(就像您经常做的那样),通过注入工厂来完成。
问题是从 ServiceLocator
反模式转移到 Dependency Injection
。鉴于本人经验不足,无法将DI原理移植到现在实现的代码中。
总结
摘要部分可选阅读。您可能想对某事发表评论,建议。
该程序的主要目的是合并特定信息的占位符字段的过程。大量的信息使得需要有基础设施。如表单、服务、数据库。我对这项任务有一些经验。我设法基于 WinForms
创建了一个类似的程序。它甚至有效!但是在模式、维护、可扩展性和性能方面,代码很糟糕。了解编程是一种爱好很重要。这不是主要的教育或工作。
在 WinForms
上执行所描述任务的体验非常糟糕。我开始研究模式和新平台。起点是UWP
和MVVM
。需要注意的是绑定机制很神奇
途中第一个问题独立解决。它与通过 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
并导航到默认页面 - MainPage
。 Microsoft documentation.
ViewModelLocator
是单例。对其 属性 的第一次调用将调用构造函数。
private static ViewModelLocator _current;
public static ViewModelLocator Current => _current ?? (_current = new ViewModelLocator());
// Constructor
private ViewModelLocator(){...}
使用ViewModelLocator
和View
(Page
)是这样的,ShellPage
:
private ShellViewModel ViewModel => ViewModelLocator.Current.ShellViewModel;
使用ViewModelLocator
与ViewModel
类似,ShellViewModel
:
private static NavigationService NavigationService => ViewModelLocator.Current.NavigationService;
移动到 DI
如上所示,ShellViewModel
从 ViewModelLocator
得到 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
我对 ServiceLocator
和 ViewModelLocator
之间的相等性有误吗?
Called
ServiceLocator
(orViewModelLocator
)
本质上,当前任务是连接View
和ViewModel
。 NavigationService
超出了本任务的范围。那么它不应该在 ViewModelLocator
中吗?
正如@Maess 指出的那样,您(现在)面临的最大挑战是将静态依赖项重构为构造函数注入。例如,你的 ShellViewModel 应该有一个像这样的构造函数:
public ShellViewModel(INavigationService navigation)
完成后,您可以设置一个 DI 框架(如 NInject),其中包含所有依赖项(有点像 SimpleIoC 的东西),理想情况下,从容器(构建其他一切)。通常这是应用程序的主视图模型。
我已经在多个项目(WPF 和 UWP)上成功地完成了这项工作,而且效果很好。唯一需要注意的是在运行时创建视图模型时(就像您经常做的那样),通过注入工厂来完成。