Search code examples
c#mvvmdependency-injectionuwpservice-locator

How to move from ServiceLocator to Dependency Injection? Specific example


The problem is moving from the ServiceLocator anti-pattern to Dependency Injection. In view of my inexperience, I can't shift the DI principle to the code implemented now.

Summary

The Summary section is optional for read. You may want to comment on something, advise.

The main purpose of the program is the process of merging placeholder fields of specific information. Number of information makes need to have infrastructure around. Such as forms, services, database. I have some experience with this task. I managed to create a similar program based on WinForms. And it even works! But in terms of patterns, maintenance, extensibility, and performance, the code is terrible. It is important to understand that programming is a hobby. It is not the main education or job.

The experience of implementing the described task on WinForms is terrible. I started studying patterns and new platform. The starting point is UWP and MVVM. It should be noted that the binding mechanism is amazing.

The first problem on the way was solved independently. It is related to navigation in the UWP via the NavigationView located in the ShellPage connected with ShellViewModel. Along with creating a NavigationService. It is based on templates from Windows Template Studio.

Since there is a working WinForms program and its anti-pattern orientation, there is time and a desire to do everything correctly.

Now I'm facing an architecture problem. Called ServiceLocator (or ViewModelLocator). I find it in examples from Microsoft, including templates from Windows Template Studio. And in doing so, I fall back into the anti-pattern trap. As stated above, I don't want this again.

And the first thing that comes as a solution is dependency injection. In view of my inexperience, I can't shift the DI principle to the code implemented now.

Current implementation

The start point of app UWP is app.xaml.cs. The whole point is to transfer control to ActivationService. Its task is adding Frame to Window.Current.Content and navigated to default page - MainPage. Microsoft documentation.

The ViewModelLocator is a singleton. The first call to its property will call constructor.

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

Using ViewModelLocator with View (Page) is like this, ShellPage:

private ShellViewModel ViewModel => ViewModelLocator.Current.ShellViewModel;

Using ViewModelLocator with ViewModel is similar, ShellViewModel:

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

Moving to DI

ShellViewModel has NavigationService from ViewModelLocator as shown above. How can I go to DI at this point? In fact, the program is small. And now is a good time to get away from anti-patterns.

Code

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());

Update 1

Am I wrong about the equality between ServiceLocator and ViewModelLocator?

Called ServiceLocator (or ViewModelLocator)

Essentially, the current task is to connected View and ViewModel. NavigationService is beyond the scope of this task. So shouldn't it be in ViewModelLocator?


Solution

  • As @Maess notes, the biggest challenge you face (right now) is refactoring the static dependencies into constructor injection. For example, your ShellViewModel should have a constructor like:

    public ShellViewModel(INavigationService navigation)
    

    Once you have done that, you can then set up a DI framework (like NInject) with all your dependencies (kind of like your SimpleIoC thing), and, ideally, pull one root object from the container (which constructs everything else). Usually that's the main view model of the application.

    I've done this successfully on multiple projects, both WPF and UWP, and it works great. Only thing you have to be careful of is when creating view models at runtime (as you often do), do it by injecting a factory.