Как перейти от ServiceLocator к внедрению зависимостей? Specifi c пример - PullRequest
0 голосов
/ 20 февраля 2020

Проблема движется от анти-паттерна ServiceLocator к Dependency Injection. Ввиду моей неопытности я не могу перенести принцип DI на код, реализованный сейчас.

Summary

Раздел Summary не является обязательным для чтения. Вы можете что-то прокомментировать, посоветовать.

Основная цель программы - процесс объединения полей-заполнителей с указанными c данными. Количество информации делает необходимым иметь инфраструктуру вокруг. Такие как формы, сервисы, базы данных. У меня есть некоторый опыт с этой задачей. Мне удалось создать аналогичную программу на основе WinForms. И это даже работает! Но с точки зрения шаблонов, обслуживания, расширяемости и производительности код ужасен. Важно понимать, что программирование - это хобби. Это не основное образование или работа.

Опыт выполнения описанного задания на WinForms ужасен. Я начал изучать шаблоны и новую платформу. Начальная точка - UWP и MVVM. Следует отметить, что механизм связывания удивителен.

Первая проблема на пути была решена самостоятельно. Он связан с навигацией в UWP через NavigationView, расположенный в ShellPage, связанном с ShellViewModel. Вместе с созданием 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 .

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 имеет NavigationService с ViewModelLocator, как показано выше. Как я могу go к 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 : Page

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?

Вызывается ServiceLocator (или ViewModelLocator)

По сути, текущая задача заключается в подключении View и ViewModel. NavigationService выходит за рамки этой задачи. Так не должно ли быть в ViewModelLocator?

1 Ответ

1 голос
/ 21 февраля 2020

Как отмечает @Maess, самая большая проблема, с которой вы сталкиваетесь (сейчас), - это рефакторинг зависимостей stati c в конструктор. Например, ваш ShellViewModel должен иметь конструктор, подобный следующему:

public ShellViewModel(INavigationService navigation)

После того, как вы это сделаете, вы можете затем настроить DI-фреймворк (например, NInject) со всеми вашими зависимостями (вроде как SimpleIo * 1008). * вещь), и, в идеале, вытащите один root объект из контейнера (который создает все остальное). Обычно это модель основного вида приложения.

Я сделал это успешно на нескольких проектах, как WPF, так и UWP, и это прекрасно работает. Единственное, о чем вы должны быть осторожны, это когда вы создаете модели представлений во время выполнения (как вы часто это делаете), вводя фабрику.

...