Решил наконец-то научиться делать интерфейс приложений расширяемый на несколько 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 контейнеры с синглтонами?
P.S.
- IoC в WPF по правилам MVVM от @EvgeniyZ и @tym32167 я уже читал. Было полезно, спасибо!

HttpClientотдельно, в виде сервиса, вам его надо достать вPage1, вы пишетеprivate HttpClientService client; public Page1(HttpClientService service) {this.service = service;}и все, ваше приложение падает, когда контейнер должен сам выдать этотHttpClientService. – EvgeniyZ Oct 01 '20 at 12:20Pages = new List<IView>, по сути тоже должен делать контейнер за вас. По поводу нарушений - да по сути самаFrameсильно ограничивает MVVM и не редко его нарушает, а так, кроме того, что у вас заDataContextотвечает контейнер, мне лично придраться не к чему. Могу посоветовать вам этот материал, который мне однажды очень хорошо помог в понимание как устроены IoC. – EvgeniyZ Oct 01 '20 at 12:24view.DataContext = viewModel;- Property DI, разве нет? 3) Почему два контейнера: можно свалить это в один класс, можно даже сотворить один метод, но я не хотел усложнять синтаксис передачей двух типов в метод, так сказать, двойной дженерик. Поэтому разделил. – aepot Oct 01 '20 at 13:09new List<IView>- просто посадить вgetterчто-то типаget => ViewService.GetAllViews<Page>();? Но там еще View главного окна болтается, не получится ли лишних плясок вокруг выфильтровыванияPageиз контейнера? В общем, пока не придумал, что можно изменить. И не понял, куда и зачем унести из контейнераDataContext. Спасибо за ссылку. – aepot Oct 01 '20 at 13:09По поводу лишних плясок - IoC заменяет
– EvgeniyZ Oct 01 '20 at 13:41var first = new First(); var second = new Second(first);, он делает это за вас, вам необходимо лишь зарезолвить один раз главный объект, который в последующем, при помощи DI будет получать все необходимое для него. Саму структуру приложения это, по сути, не меняет, меняет лишь подход в некоторых местах.DataContext, обновил вопрос. – aepot Oct 01 '20 at 15:33MainViewModel, которая требует для своей работы некийILogger, вам не нужно будет писатьILogger = Container.Resolve(...);, это за вас уже сам контейнер сделает. Вот простой пример набросал. Заметьте, что внутри классов не делается Resolve. – EvgeniyZ Oct 01 '20 at 16:12MainViewModel, ну и окно (а должно-ли оно быть в контейнере?), задать один раз ее какDataContextи все, дальше вы забываете про контейнер (только если не надо зарегистрировать новый тип), вы просто пишете классы, с нужными для них зависимостями, а контейнер сам проинициализирует и подставит. – EvgeniyZ Oct 01 '20 at 16:14