IoC в WPF по правилам MVVM

154
08 апреля 2022, 15:00

Постепенно начал изучать IoC и всю эту кухню и вот не как не могу понять, как работать с ними в WPF приложение по правилам MVVM.

Допустим я делаю некий класс настроек контейнера (использую Autofac):

class ContainerConfig
{
    public static IContainer Configure()
    {
        ISettings settings = new ConfigurationBuilder<ISettings>().UseJsonFile("Settings.json").Build();
        var builder = new ContainerBuilder();
        builder.RegisterType<MainViewModel>().SingleInstance();
        builder.RegisterInstance(settings).SingleInstance();
        return builder.Build();
    }
}

В нем я регистрирую пока 2 объекта:

  1. MainViewModel - главная VM приложения, она я как понял должна быть в едином экземпляре.
  2. Некий объект настроек приложения.

Далее переопределяю OnStartup, для того, что бы создать окно, задать ему DataContext и все это вывести:

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        var container = ContainerConfig.Configure();
        using var scope = container.BeginLifetimeScope();
        var mainViewModel = scope.Resolve<MainViewModel>();
        new MainWindow() { DataContext = mainViewModel }.Show();
    }
}

Вроде пока я двигаюсь в правильном направление, или нет?
Ок, дальше для проверки работы я просто привяжу свойство из настроек, написав в MainViewModel следующее:

class MainViewModel
{
    public ISettings Settings { get; }
    public MainViewModel(ISettings settingsModel)
    {
        Settings = settingsModel;
    }
}

И сама привязка:

<TextBlock Text="{Binding Settings.SomeValue}"/>

Вроде все работает, все хорошо.
Теперь допустим мне надо сделать еще одну VM, которой например нужна главная VM, я делаю:

class SecondViewModel
{
    private MainViewModel main;
    public SecondViewModel(MainViewModel mainViewModel)
    {
        main = mainViewModel;
    }
    public int Test { get; set; } = 33;
    private void SomeMethod()
    {
        main.SomeProperty = false;
    }
}

Регистрирую его:

builder.RegisterType<SecondViewModel>().SingleInstance();

Ну и дописываю в MainViewModel новую VM:

class MainViewModel
{
    public ISettings Settings { get; }
    public SecondViewModel Second { get; }
    public MainViewModel(ISettings settingsModel, SecondViewModel second)
    {
        Settings = settingsModel;
        Second = second;
    }
}

В итоге получаю ошибку зацикленности и тут явно понимаю, что делаю что-то не так.
Немного поискав информацию, нашел способ обхода.

Короче как видите, я не совсем до конца понимаю как все это должно работать и возникает куча вопросов, например:

  1. Правильно я сделал выше?
  2. Что должно регистрироваться в контейнере?
  3. Необходимы-ли для VM слоев интерфейсы?
  4. Как не нарушить MVVM?

В общем, помогите разобраться, как все-же правильно реализовывать IoC в WPF приложение, да еще и с MVVM?

Answer 1

DI контейнер - это инструмент, который делает за вас работу, которую вы и сами можете сделать, не нарушая никаких принципов.

Как вы знаете, контейнер - это такая вещь, которая создает обекты и следит за их временем жизни за вас.

Вы в своем коде создали 2 класса с цикличной зависимостью в конструкторе, что делает невозможным создание таких классов (если только в конструкторы NULL не передавать). Попробуйте создать ваши классы вручную и вы увидите, что у вас ничего не получится, так как, чтобы создать класс А, вам надо предоставить ему класс Б, а чтобы создать класс Б - вам надо предоставить класс А. Этот цикл и называется циклической зависимостью и его не так и просто побороть.

Но, как я сказал, побороть его не просто, но можно, например если определить фабрику для классов и передавать уже фабрику в конструктор. И то, это сработает, только если вы не попытаетесь обратиться к фабрике прямо внутри конструктора.

Например

class A
{
    IBFactory _bfactory;
    public A(IBFactory bfactory)
    {
        _bfactory = bfactory;
    }
    void Foo()
    {
        var b = _bfactory.GetB();
        // do stuff
    }
}
class B
{
    A _a;
    public B(A a)
    {
        _a = a;
    }
}
interface IBFactory{
    B GetB();
}

Реализация IBFactory может быть самой простой

class BFactory : IBFactory
{
    IContainer _container;
    public BFactory(IContainer container)
    {
        _container = container;
    }
    public B GetB(){
        return _container.Resolve<B>();
    }
}

Некоторые контейнеры, если мне память не изменяет, сами по себе поддерживают создание подобных фабрик. Ленивые же программисты в таком случае, вместо фабрики, пробрасывают просто весь контейнер в А класс, что черевато и является анти паттерном - service loсator.

Однако, также хочется сказать, что такие ситуации, когда подчиненная модель хочет знать о главной, в идеале, не должны существовать. Старайтесть проектировать такие VM, которые были бы максимально самодостаточные. В случае, если должна происходить комуникация между VM, то в общем случае для этого есть шина сообщений, или вы можете пробрасывать какие то свои интерфейсы, но вообще такая коммуникация должна быть ограничена. Чем меньше VM общаются друг с другом, тем меньше у вас головной боли при отладке.

Теперь давайте пойдем по вопросам:

Правильно я сделал выше?

Нет ничего правильного в нашей профессии, но я бы решил вашу задачу иначе.

class SecondViewModel
{    
    public SecondViewModel()
    {        
    }
    event EventHandler<MyEventArgs> SomethingHappened;
    private void OnSomethingHappened(...) =>  SomethingHappened?.Invoke(....);
    ///......    
    private void SomeMethod()
    {
        OnSomethingHappened(...);      
    }
}
class MainViewModel
{
    public ISettings Settings { get; }
    public SecondViewModel Second { get; }
    public MainViewModel(ISettings settingsModel, SecondViewModel second)
    {
        Settings = settingsModel;
        Second = second;
        Second.SomethingHappened += Second_SomethingHappened;
    }
    private void Second_SomethingHappened(object sender, MyEventArgs args)
    {
        this.SomeProperty = false;
    }
}

Таким образом, вторая модель знать не знает ни о какой главной модели, а логика реакции главной модели на подчиненные модели хранится в одном месте - в главной модели.

Что должно регистрироваться в контейнере?

Всё, что вы планируете использовать. Ваша проблема не в контейнере, а в орагнизации ваших классов - в циклической зависимости.

Необходимы-ли для VM слоев интерфейсы?

Зависит от вашей органищации классов. Когда то это имеет смысл, когда то - не имеет (чаще не имеет). В вашей ситуации интерфейсы вам не помогут.

Как не нарушить MVVM?

Вы оперируете здесь только с VM слоем, потому как бы вы этот слой не сделали, сам по себе он не нарушает MVVM.

READ ALSO
Положение окна WPF

Положение окна WPF

Окно WPF запускается с WindowState="Maximized"Если сразу после запуска посмотреть Left или Top окна то они будут равны -8

102
c# linq группировка по диапазону с условием

c# linq группировка по диапазону с условием

Имеется список со значениями координат:

100
Нужно ли корректировать верстку так, чтобы при масштабировании нигде ничего не выпирало?

Нужно ли корректировать верстку так, чтобы при масштабировании нигде ничего не выпирало?

Нужно ли вообще корректировать верстку так, чтоб при масштабировании нигде ничего не выпирало и тд? Вот как выглядит отверстанная часть...

77
Производительность тегов HTML

Производительность тегов HTML

Есть ли разница в производительности между тегами? Например у меня (образно) 10000 слов данных, каждое слово нужно обернуть в тег Влияет ли выбор...

139