3

Решил наконец-то научиться делать интерфейс приложений расширяемый на несколько 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&lt;T&gt;(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&lt;T&gt;() 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&lt;IView&gt; Pages { get; }

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

public string Title =&gt; $&quot;IoC Demo - {CurrentPage.Title}&quot;;

public MainViewModel()
{
    Pages = new List&lt;IView&gt;
    {
        ViewService.GetView&lt;Page1&gt;(ViewModelService.GetViewModel&lt;Page1ViewModel&gt;()),
        ViewService.GetView&lt;Page2&gt;(ViewModelService.GetViewModel&lt;Page2ViewModel&gt;())
    };
    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.

aepot
  • 49,560
  • 1
    А почему статика? Контейнер должен создаваться один раз при старте приложения, забиваться нужными объектами/типами и дальше через него уже получается все что нужно. Также не пойму для чего вам два контейнера, почему не один? Ну и также я не вижу у вас тут DI, к примеру, у вас есть HttpClient отдельно, в виде сервиса, вам его надо достать в Page1, вы пишете private HttpClientService client; public Page1(HttpClientService service) {this.service = service;} и все, ваше приложение падает, когда контейнер должен сам выдать этот HttpClientService. – EvgeniyZ Oct 01 '20 at 12:20
  • 1
    Вот это, Pages = new List<IView>, по сути тоже должен делать контейнер за вас. По поводу нарушений - да по сути сама Frame сильно ограничивает MVVM и не редко его нарушает, а так, кроме того, что у вас за DataContext отвечает контейнер, мне лично придраться не к чему. Могу посоветовать вам этот материал, который мне однажды очень хорошо помог в понимание как устроены IoC. – EvgeniyZ Oct 01 '20 at 12:24
  • @EvgeniyZ Спасибо! 1) Почему статика, потому что я не вижу причин для синглтона здесь, я никому и никуда сам контейнер не передаю, он просто статичный, и он просто есть. 2) view.DataContext = viewModel; - Property DI, разве нет? 3) Почему два контейнера: можно свалить это в один класс, можно даже сотворить один метод, но я не хотел усложнять синтаксис передачей двух типов в метод, так сказать, двойной дженерик. Поэтому разделил. – aepot Oct 01 '20 at 13:09
  • @EvgeniyZ 4) По поводу new List<IView> - просто посадить в getter что-то типа get => ViewService.GetAllViews<Page>();? Но там еще View главного окна болтается, не получится ли лишних плясок вокруг выфильтровывания Page из контейнера? В общем, пока не придумал, что можно изменить. И не понял, куда и зачем унести из контейнера DataContext. Спасибо за ссылку. – aepot Oct 01 '20 at 13:09
  • а вообще в вашем примере я вижу фабрики, но не вижу DI – tym32167 Oct 01 '20 at 13:16
  • Возьмем, например, вашу главную модель. Почему она решает какие страницы, какого типа и сколько надо отрендерить? Почему нельзя эти страницы создать заранее и закинуть в конструктор? Или делегировать это создание другому классу? – tym32167 Oct 01 '20 at 13:18
  • 1
    Ваши сервисы для создания являются обычными фабриками. Вы, конечно, можете попробовать натянуть на них название "DI контейнер", но выполнять роль контейнера они не начнут от этого. Также пример не очень удачный, так как хранить представления в памяти я бы не стал - другое окно может захотеть предсталение забрать себе и можно получить непонятную кашу. Обычно можно хранить VM как синглтон, а представление рендерить каждый раз, когда неадо показать VM – tym32167 Oct 01 '20 at 13:22
  • Ну статика, если вам так удобней, и вы так видите, то дерзайте, но, если вы возьмете, например AutoFac или любой другой контейнер, то вы не увидите там статику вовсе, ибо у них другой жизненный цикл. Судить плохо это или нет, не буду, лично для меня статика зло) И не путайте жизненный цикл контейнера и объекта, внутри него.2) Вы, по сути, просто взяли объект и установили ему свойство. Это больше и правда похоже на фабрику. 3) Сама суть IoC в том, что появляется некий механизм, который отвечает за инициализацию объектов и внедрение в них необходимых зависимостей.
  • – EvgeniyZ Oct 01 '20 at 13:40
  • Как это сделано у AutoFac, но как реализовано, увы, подсказать не смогу.
  • По поводу лишних плясок - IoC заменяет var first = new First(); var second = new Second(first);, он делает это за вас, вам необходимо лишь зарезолвить один раз главный объект, который в последующем, при помощи DI будет получать все необходимое для него. Саму структуру приложения это, по сути, не меняет, меняет лишь подход в некоторых местах.

    – EvgeniyZ Oct 01 '20 at 13:41
  • @tym32167 справедливое замечание, одну и ту же вьюху действительно нельзя показывать одновременно больше, чем один раз. Но у меня это в принципе невозможно, но я пометил себе негибкость решения, учту обязательно. Спасибо! – aepot Oct 01 '20 at 13:50
  • @EvgeniyZ подумал, таки перенес DataContext, обновил вопрос. – aepot Oct 01 '20 at 15:33
  • 2
    Ну так вы саму суть то не поменяли, у вас как была фабрика, которая по требованию отдает нужный объект, так и осталась. В IoC вы изначально создаете контейнер, в него регистрируете все свои необходимые типы, дальше говорите ему: "Слушай, дай мне главный класс и запихни в него все зависимости". Он вам отдает, например MainViewModel, которая требует для своей работы некий ILogger, вам не нужно будет писать ILogger = Container.Resolve(...);, это за вас уже сам контейнер сделает. Вот простой пример набросал. Заметьте, что внутри классов не делается Resolve. – EvgeniyZ Oct 01 '20 at 16:12
  • То есть, в вашем примере, достаточно из контейнера зарезолвить только MainViewModel, ну и окно (а должно-ли оно быть в контейнере?), задать один раз ее как DataContext и все, дальше вы забываете про контейнер (только если не надо зарегистрировать новый тип), вы просто пишете классы, с нужными для них зависимостями, а контейнер сам проинициализирует и подставит. – EvgeniyZ Oct 01 '20 at 16:14