IoC в MVVM для чайников

Решил наконец-то научиться делать интерфейс приложений расширяемый на несколько View/ViewModel в рамках MVVM. Наткнулся на шаблон Inversion of Control + Depency Injection, информации по нему много, многие его советуют для огранизации логической архитектуры приложения. К тому же, он дает возможность организовать удобства типа хранилища синглтонов, которые мне как раз и нужны.

Проблема была в том, что в статьях авторы рекомендуют вот это вот всё, и практически никто не показывает реализацию IoC вручную в WPF+MVVM "для чайников".

А я не могу не понимая, как оно работает, использовать "черный ящик" в своих проектах, вообще не люблю большое, срашное и непонятное, которое непонятно как работает, и всегда пытаюсь раскопать эту самую суть. Разбор решил выполнить на практической задаче - сделать многостраничный интерфейс с ListBox для выбора страниц.

Начал с двух интефейсов

public interface IView
{
    string Title { get; set; }
    object DataContext { get; set; }
}

public interface IViewModel
{
}

Затем прикрутил их все

public sealed partial class MainWindow : Window, IView {...}
public partial class Page1 : Page, IView {...}
public partial class Page2 : Page, IView {...}

public class MainViewModel : NotifyPropertyChanged, IViewModel {...}
public class Page1ViewModel : NotifyPropertyChanged, IViewModel {...}
public class Page2ViewModel : NotifyPropertyChanged, IViewModel {...}

И реализовал вот такие 2 контейнера с синглтонами

public static class ViewService
{
    private static readonly ConcurrentDictionary<Type, IView> instances = new ConcurrentDictionary<Type, IView>();

    public static T GetView<T>(IViewModel viewModel = null) where T : IView
    {
        if (!instances.TryGetValue(typeof(T), out IView view))
        {
            view = (T)Activator.CreateInstance(typeof(T));
            instances[typeof(T)] = view;
        }
        if (viewModel != null)
            view.DataContext = viewModel;
        return (T)view;
    }
}

public static class ViewModelService
{
    private static readonly ConcurrentDictionary<Type, IViewModel> instances = new ConcurrentDictionary<Type, IViewModel>();

    public static T GetViewModel<T>() where T : IViewModel
    {
        if (!instances.TryGetValue(typeof(T), out IViewModel viewModel))
        {
            viewModel = (T)Activator.CreateInstance(typeof(T));
            instances[typeof(T)] = viewModel;
        }
        return (T)viewModel;
    }
}

Главное окно создаю вот так

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        MainViewModel vm = ViewModelService.GetViewModel<MainViewModel>();
        MainWindow window = ViewService.GetView<MainWindow>(vm);
        window.Show();
    }
}

А странички - в конструкторе главной вью-модели

public class MainViewModel : NotifyPropertyChanged, IViewModel
{
    private IView _currentPage;

    public List<IView> Pages { get; }

    public IView CurrentPage
    {
        get => _currentPage;
        set
        {
            _currentPage = value;
            OnPropertyChanged();
            OnPropertyChanged(nameof(Title));
        }
    }

    public string Title => $"IoC Demo - {CurrentPage.Title}";

    public MainViewModel()
    {
        Pages = new List<IView>
        {
            ViewService.GetView<Page1>(ViewModelService.GetViewModel<Page1ViewModel>()),
            ViewService.GetView<Page2>(ViewModelService.GetViewModel<Page2ViewModel>())
        };
        CurrentPage = Pages[0];
    }
}

Главное окно MainWindow

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <ListBox BorderThickness="0" Background="AliceBlue" ItemsSource="{Binding Pages}" SelectedItem="{Binding CurrentPage}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Title}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5"/>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    <Frame Content="{Binding CurrentPage}" Grid.Column="1" NavigationUIVisibility="Hidden"/>
</Grid>

Странички одинаковые, просто у одной тип Page1, у второй Page2

<Grid>
    <TextBlock Margin="5" Text="{Binding Text}"/>
</Grid>

Вью-модель странички (для видимости)

public class Page1ViewModel : NotifyPropertyChanged, IViewModel
{
    public string Text { get; } = "Page one";
}

И это все вместе работает.

Вопрос: Нарушаю ли я MVVM, и правильно ли реализовал IoC контейнеры с синглтонами?

IoC WPF

P.S.


Ответы (1 шт):

Автор решения: aepot

Кажется, осилил.

  1. Конструкторы изменил так, чтобы они принимали конкретный тип View Model и закидывали его себе в DataContext.
public MainWindow(MainViewModel viewModel) {...}
public Page1(Page1ViewModel viewModel) {...}
public Page2(Page2ViewModel viewModel) {...}
  1. Интерфейс IViewModel выкинул за ненадобностью

  2. Реализовал IoC контейнер, на этот раз надеюсь, что настоящий

public static class IocContainer
{
    private static readonly IDictionary<Type, object> instances = new ConcurrentDictionary<Type, object>();

    public static void Register<T>() where T : class
        => instances[typeof(T)] = null;

    public static T Resolve<T>() where T : class
        => (T)GetInstance(typeof(T));

    private static object GetInstance(Type type)
    {
        bool registered = instances.TryGetValue(type, out object instance);
        if (instance == null)
        {
            object[] args = type.GetConstructors().First().GetParameters().Select(x => GetInstance(x.ParameterType)).ToArray();
            instance = Activator.CreateInstance(type, args);
            if (registered)
                instances[type] = instance;
        }
        return instance;
    }
}
  1. Код OnStartup теперь выглядит так:
protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);
    IocContainer.Register<MainViewModel>();
    IocContainer.Register<Page1ViewModel>();
    IocContainer.Register<Page2ViewModel>();

    IocContainer.Resolve<MainWindow>().Show();
}
  1. А конструктор MainViewModel так:
public MainViewModel()
{
    Pages = new List<IView>
    {
        IocContainer.Resolve<Page1>(),
        IocContainer.Resolve<Page2>()
    };
    CurrentPage = Pages[0];
}

Спасибо всем комментаторам, накачали меня действительно нужной информацией, особенно SimpleIoC помог.

P.S. Архив с этим решением на Яндекс.Диске - ссылка.

→ Ссылка