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 контейнеры с синглтонами?
P.S.
- IoC в WPF по правилам MVVM от @EvgeniyZ и @tym32167 я уже читал. Было полезно, спасибо!
Ответы (1 шт):
Кажется, осилил.
- Конструкторы изменил так, чтобы они принимали конкретный тип View Model и закидывали его себе в
DataContext.
public MainWindow(MainViewModel viewModel) {...}
public Page1(Page1ViewModel viewModel) {...}
public Page2(Page2ViewModel viewModel) {...}
Интерфейс
IViewModelвыкинул за ненадобностьюРеализовал 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;
}
}
- Код
OnStartupтеперь выглядит так:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
IocContainer.Register<MainViewModel>();
IocContainer.Register<Page1ViewModel>();
IocContainer.Register<Page2ViewModel>();
IocContainer.Resolve<MainWindow>().Show();
}
- А конструктор
MainViewModelтак:
public MainViewModel()
{
Pages = new List<IView>
{
IocContainer.Resolve<Page1>(),
IocContainer.Resolve<Page2>()
};
CurrentPage = Pages[0];
}
Спасибо всем комментаторам, накачали меня действительно нужной информацией, особенно SimpleIoC помог.
P.S. Архив с этим решением на Яндекс.Диске - ссылка.
