Как обрабатывать внедрение зависимостей в приложении WPF/MVVM
Я запускаю новое настольное приложение и хочу создать его с использованием MVVM и WPF.
Я также собираюсь использовать TDD.
Проблема в том, что я не знаю, как мне использовать контейнер IoC для внедрения моих зависимостей в мой производственный код.
Предположим, у меня есть следующие класс и интерфейс:
public interface IStorage
{
bool SaveFile(string content);
}
public class Storage : IStorage
{
public bool SaveFile(string content){
// Saves the file using StreamWriter
}
}
И тогда у меня есть другой класс, который имеет IStorage
в качестве зависимости предположим также, что этот класс является ViewModel или бизнес-классом...
public class SomeViewModel
{
private IStorage _storage;
public SomeViewModel(IStorage storage){
_storage = storage;
}
}
Благодаря этому я могу легко написать модульные тесты, чтобы убедиться, что они работают правильно, используя макеты и т. Д.
Проблема в том, что когда дело доходит до использования его в реальном приложении. Я знаю, что у меня должен быть контейнер IoC, который связывает реализацию по умолчанию для IStorage
интерфейс, но как я могу это сделать?
Например, как было бы, если бы у меня был следующий xaml:
<Window
... xmlns definitions ...
>
<Window.DataContext>
<local:SomeViewModel />
</Window.DataContext>
</Window>
Как я могу правильно "сказать" WPF вводить зависимости в этом случае?
Кроме того, предположим, мне нужен экземпляр SomeViewModel
от моего cs
код, как мне это сделать?
Я чувствую, что полностью потерялся в этом, я был бы признателен за любой пример или руководство о том, как лучше всего справиться с этим.
Я знаком со StructureMap, но я не эксперт. Кроме того, если есть более качественная / простая / готовая среда, пожалуйста, дайте мне знать.
Заранее спасибо.
10 ответов
Я использовал Ninject и обнаружил, что с ним приятно работать. Все настроено в коде, синтаксис довольно прост и содержит хорошую документацию (и множество ответов по SO).
Так что в основном это выглядит так:
Создайте модель представления и примите интерфейс IStorage в качестве параметра конструктора:
class UserControlViewModel
{
public UserControlViewModel(IStorage storage)
{
}
}
Создайте ViewModelLocator со свойством get для модели представления, которая загружает модель представления из Ninject:
class ViewModelLocator
{
public UserControlViewModel UserControlViewModel
{
get { return IocKernel.Get<UserControlViewModel>();} // Loading UserControlViewModel will automatically load the binding for IStorage
}
}
Сделайте ViewModelLocator ресурсом всего приложения в App.xaml:
<Application ...>
<Application.Resources>
<local:ViewModelLocator x:Key="ViewModelLocator"/>
</Application.Resources>
</Application>
Свяжите DataContext из UserControl с соответствующим свойством в ViewModelLocator.
<UserControl ...
DataContext="{Binding UserControlViewModel, Source={StaticResource ViewModelLocator}}">
<Grid>
</Grid>
</UserControl>
Создайте класс, наследующий NinjectModule, который будет устанавливать необходимые привязки (IStorage и viewmodel):
class IocConfiguration : NinjectModule
{
public override void Load()
{
Bind<IStorage>().To<Storage>().InSingletonScope(); // Reuse same storage every time
Bind<UserControlViewModel>().ToSelf().InTransientScope(); // Create new instance every time
}
}
Инициализируйте ядро IoC при запуске приложения с необходимыми модулями Ninject (на данный момент выше):
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
IocKernel.Initialize(new IocConfiguration());
base.OnStartup(e);
}
}
Я использовал статический класс IocKernel для хранения экземпляра ядра IoC во всем приложении, поэтому я могу легко получить к нему доступ при необходимости:
public static class IocKernel
{
private static StandardKernel _kernel;
public static T Get<T>()
{
return _kernel.Get<T>();
}
public static void Initialize(params INinjectModule[] modules)
{
if (_kernel == null)
{
_kernel = new StandardKernel(modules);
}
}
}
Это решение использует статический ServiceLocator (IocKernel), который обычно рассматривается как анти-шаблон, потому что он скрывает зависимости класса. Однако очень трудно избежать какого-либо ручного поиска службы для классов пользовательского интерфейса, поскольку они должны иметь конструктор без параметров, и вы все равно не можете управлять созданием экземпляра, поэтому вы не можете внедрить ВМ. По крайней мере, этот способ позволяет вам тестировать виртуальную машину изолированно, где вся бизнес-логика.
Если у кого есть лучший способ, пожалуйста, поделитесь.
РЕДАКТИРОВАТЬ: Лаки Лайки дал ответ, чтобы избавиться от статического локатора службы, позволив Ninject создавать экземпляры классов пользовательского интерфейса. Подробности ответа можно посмотреть здесь
В своем вопросе вы устанавливаете значение DataContext
свойство представления в XAML. Это требует, чтобы ваша модель представления имела конструктор по умолчанию. Однако, как вы заметили, это не очень хорошо работает с внедрением зависимостей, когда вы хотите внедрить зависимости в конструктор.
Таким образом, вы не можете установить DataContext
недвижимость в XAML. Вместо этого у вас есть другие альтернативы.
Если ваше приложение основано на простой иерархической модели представления, вы можете построить всю иерархию модели представления при запуске приложения (вам придется удалить StartupUri
собственность от App.xaml
файл):
public partial class App {
protected override void OnStartup(StartupEventArgs e) {
base.OnStartup(e);
var container = CreateContainer();
var viewModel = container.Resolve<RootViewModel>();
var window = new MainWindow { DataContext = viewModel };
window.Show();
}
}
Это основано на объектном графе моделей представления, основанных на RootViewModel
но вы можете внедрить некоторые фабрики моделей представлений в родительские модели представлений, позволяя им создавать новые дочерние модели представлений, чтобы не пришлось фиксировать граф объектов. Это также, надеюсь, ответит на ваш вопрос, если мне нужен экземпляр SomeViewModel
от моего cs
код, как мне это сделать?
class ParentViewModel {
public ParentViewModel(ChildViewModelFactory childViewModelFactory) {
_childViewModelFactory = childViewModelFactory;
}
public void AddChild() {
Children.Add(_childViewModelFactory.Create());
}
ObservableCollection<ChildViewModel> Children { get; private set; }
}
class ChildViewModelFactory {
public ChildViewModelFactory(/* ChildViewModel dependencies */) {
// Store dependencies.
}
public ChildViewModel Create() {
return new ChildViewModel(/* Use stored dependencies */);
}
}
Если ваше приложение более динамично по своей природе и, возможно, основано на навигации, вам придется подключиться к коду, который выполняет навигацию. Каждый раз, когда вы переходите к новому представлению, вам нужно создать модель представления (из контейнера DI), само представление и установить DataContext
вида на модель-вид. Вы можете сначала сделать это представление, когда вы выбираете модель представления на основе представления, или вы можете сначала сделать это представление, где модель представления определяет, какое представление использовать. Среда MVVM обеспечивает эту ключевую функциональность некоторым способом, позволяющим вам подключить свой DI-контейнер к созданию моделей представлений, но вы также можете реализовать его самостоятельно. Здесь я немного расплывчат, потому что в зависимости от ваших потребностей эта функциональность может стать довольно сложной. Это одна из основных функций, которые вы получаете от инфраструктуры MVVM, но использование собственных функций в простом приложении даст вам хорошее понимание того, что предоставляют структуры MVVM.
Не имея возможности объявить DataContext
в XAML вы теряете некоторую поддержку во время разработки. Если ваша view-модель содержит некоторые данные, они появятся во время разработки, что может быть очень полезно. К счастью, вы можете использовать атрибуты времени разработки и в WPF. Один из способов сделать это - добавить следующие атрибуты в <Window>
элемент или <UserControl>
в XAML:
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:MyViewModel, IsDesignTimeCreatable=True}"
Тип модели представления должен иметь два конструктора, по умолчанию для данных времени разработки и другого для внедрения зависимости:
class MyViewModel : INotifyPropertyChanged {
public MyViewModel() {
// Create some design-time data.
}
public MyViewModel(/* Dependencies */) {
// Store dependencies.
}
}
Делая это, вы можете использовать внедрение зависимостей и сохранить хорошую поддержку во время разработки.
То, что я публикую здесь, является улучшением ответа Сондергарда, потому что то, что я собираюсь рассказать, не вписывается в комментарий:)
На самом деле я представляю аккуратное решение, которое устраняет необходимость в ServiceLocator и оболочке для StandardKernel
-Пример, который в решении Сондергарда называется IocContainer
, Зачем? Как уже упоминалось, это анти-паттерны.
Делая StandardKernel
доступно везде
Ключ к магии Ninject является StandardKernel
-Экземпляр, который необходим для использования .Get<T>()
-Метод.
В качестве альтернативы Сондергарду IocContainer
Вы можете создать StandardKernel
внутри App
-Учебный класс.
Просто удалите StartUpUri из вашего App.xaml
<Application x:Class="Namespace.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
...
</Application>
Это CodeBehind приложения внутри App.xaml.cs
public partial class App
{
private IKernel _iocKernel;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
_iocKernel = new StandardKernel();
_iocKernel.Load(new YourModule());
Current.MainWindow = _iocKernel.Get<MainWindow>();
Current.MainWindow.Show();
}
}
Отныне Ninject жив и готов к бою:)
Впрыскивая ваше DataContext
Так как Ninject жив, вы можете выполнять все виды инъекций, например, инъекцию Property Setter Injection или наиболее распространенную инъекцию Constructor.
Вот как вы вводите вашу ViewModel в свой Window
"s DataContext
public partial class MainWindow : Window
{
public MainWindow(MainWindowViewModel vm)
{
DataContext = vm;
InitializeComponent();
}
}
Конечно, вы также можете ввести IViewModel
если вы делаете правильные привязки, но это не является частью этого ответа.
Прямой доступ к ядру
Если вам нужно вызвать методы на ядре напрямую (например, .Get<T>()
-Метод), вы можете позволить ядру вводить сам.
private void DoStuffWithKernel(IKernel kernel)
{
kernel.Get<Something>();
kernel.Whatever();
}
Если вам нужен локальный экземпляр Ядра, вы можете добавить его как Свойство.
[Inject]
public IKernel Kernel { private get; set; }
Несмотря на то, что это может быть довольно полезным, я бы не рекомендовал вам это делать. Просто обратите внимание, что объекты, внедренные таким образом, не будут доступны внутри Конструктора, потому что он внедряется позже.
По этой ссылке вы должны использовать заводское расширение вместо введения IKernel
(DI Контейнер).
Рекомендуемый подход к использованию контейнера DI в программной системе состоит в том, что корень композиции приложения - это единственное место, где контейнер напрямую касается.
Как будет использоваться Ninject.Extensions.Factory, также может быть красным.
Я использую подход "представление сначала", где я передаю модель представления конструктору представления (в его коде), который присваивается контексту данных, например
public class SomeView
{
public SomeView(SomeViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
Это заменяет ваш подход на основе XAML.
Я использую инфраструктуру Prism для управления навигацией - когда какой-либо код запрашивает конкретное представление (путем "навигации" к нему), Prism разрешит это представление (внутренне, используя инфраструктуру DI приложения); структура DI, в свою очередь, разрешает любые зависимости, которые есть у представления (модель представления в моем примере), затем разрешает его зависимости и так далее.
Выбор инфраструктуры DI в значительной степени не имеет значения, поскольку все они делают по существу одно и то же, то есть вы регистрируете интерфейс (или тип) вместе с конкретным типом, который вы хотите, чтобы среда создавалась при обнаружении зависимости от этого интерфейса. Для записи я использую Замок Виндзор.
Навигация с помощью призмы требует некоторого привыкания, но она довольно хороша, когда вы обдумываете ее, позволяя составлять приложение с использованием разных представлений. Например, вы можете создать "регион" Prism в главном окне, а затем с помощью навигации Prism вы можете переключаться с одного вида на другой в этом регионе, например, когда пользователь выбирает пункты меню или что-то еще.
В качестве альтернативы взгляните на одну из сред MVVM, такую как MVVM Light. У меня нет такого опыта, поэтому я не могу комментировать то, что они любят использовать.
Установите MVVM Light.
Часть установки заключается в создании локатора модели представления. Это класс, который выставляет ваши viewmodels как свойства. Получатель этих свойств может затем быть возвращен экземплярами из вашего механизма IOC. К счастью, MVVM light также включает в себя инфраструктуру SimpleIOC, но вы можете подключиться к другим, если хотите.
С простым IOC вы регистрируете реализацию для типа...
SimpleIOC.Default.Register<MyViewModel>(()=> new MyViewModel(new ServiceProvider()), true);
В этом примере ваша модель представления создается и передается объект поставщика услуг в соответствии с его конструктором.
Затем вы создаете свойство, которое возвращает экземпляр из IOC.
public MyViewModel
{
get { return SimpleIOC.Default.GetInstance<MyViewModel>; }
}
Умная часть заключается в том, что локатор модели представления затем создается в app.xaml или эквивалентном источнике данных.
<local:ViewModelLocator x:key="Vml" />
Теперь вы можете привязать его к свойству MyViewModel, чтобы получить вашу модель представления с внедренным сервисом.
Надеюсь, это поможет. Извиняюсь за любые неточности кода, закодированные из памяти на iPad.
Canonic DryIoc чехол
Отвечая на старый пост, но делаю это с DryIoc
и делать то, что я считаю хорошим использованием DI и интерфейсов (минимальное использование конкретных классов).
- Отправной точкой приложения WPF является
App.xaml
и там мы расскажем, что такое начальный вид для использования; мы делаем это с помощью кода вместо xaml по умолчанию: - Удалить
StartupUri="MainWindow.xaml"
в App.xaml в codebehind (App.xaml.cs) добавить это
override OnStartup
:protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); DryContainer.Resolve<MainWindow>().Show(); }
это точка запуска; это также единственное место, где resolve
должен быть назван.
корень конфигурации (в соответствии с книгой Марка Симана о внедрении зависимостей в.NET; единственное место, где следует упомянуть конкретные классы) будет находиться в том же коде, что и в конструкторе:
public Container DryContainer { get; private set; } public App() { DryContainer = new Container(rules => rules.WithoutThrowOnRegisteringDisposableTransient()); DryContainer.Register<IDatabaseManager, DatabaseManager>(); DryContainer.Register<IJConfigReader, JConfigReader>(); DryContainer.Register<IMainWindowViewModel, MainWindowViewModel>( Made.Of(() => new MainWindowViewModel(Arg.Of<IDatabaseManager>(), Arg.Of<IJConfigReader>()))); DryContainer.Register<MainWindow>(); }
Замечания и немного больше деталей
- Я использовал конкретный класс только с той точки зрения
MainWindow
; - Мне пришлось указать, какой конструктор использовать (нам нужно сделать это с DryIoc) для ViewModel, потому что конструктор по умолчанию должен существовать для конструктора XAML, а конструктор с инжекцией - это тот, который используется для приложения.
Конструктор ViewModel с DI:
public MainWindowViewModel(IDatabaseManager dbmgr, IJConfigReader jconfigReader)
{
_dbMgr = dbmgr;
_jconfigReader = jconfigReader;
}
ViewModel конструктор по умолчанию для дизайна:
public MainWindowViewModel()
{
}
Код за взглядом:
public partial class MainWindow
{
public MainWindow(IMainWindowViewModel vm)
{
InitializeComponent();
ViewModel = vm;
}
public IViewModel ViewModel
{
get { return (IViewModel)DataContext; }
set { DataContext = value; }
}
}
и что нужно в представлении (MainWindow.xaml), чтобы получить экземпляр дизайна с ViewModel:
d:DataContext="{d:DesignInstance local:MainWindowViewModel, IsDesignTimeCreatable=True}"
Заключение
Следовательно, мы получили очень чистую и минимальную реализацию приложения WPF с контейнером DryIoc и DI, сохраняя возможность создания экземпляров представлений и моделей представления.
Используйте структуру управляемой расширяемости.
[Export(typeof(IViewModel)]
public class SomeViewModel : IViewModel
{
private IStorage _storage;
[ImportingConstructor]
public SomeViewModel(IStorage storage){
_storage = storage;
}
public bool ProperlyInitialized { get { return _storage != null; } }
}
[Export(typeof(IStorage)]
public class Storage : IStorage
{
public bool SaveFile(string content){
// Saves the file using StreamWriter
}
}
//Somewhere in your application bootstrapping...
public GetViewModel() {
//Search all assemblies in the same directory where our dll/exe is
string currentPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var catalog = new DirectoryCatalog(currentPath);
var container = new CompositionContainer(catalog);
var viewModel = container.GetExport<IViewModel>();
//Assert that MEF did as advertised
Debug.Assert(viewModel is SomViewModel);
Debug.Assert(viewModel.ProperlyInitialized);
}
В общем, вы должны иметь статический класс и использовать Factory Pattern, чтобы предоставить вам глобальный контейнер (cached, natch).
Что касается того, как внедрить модели представлений, вы вводите их так же, как и все остальное. Создайте конструктор импорта (или поместите оператор импорта для свойства / поля) в коде файла XAML и скажите ему импортировать модель представления. Затем свяжите свой Window
"s DataContext
к этой собственности. Ваши корневые объекты, которые вы фактически извлекаете из контейнера, обычно состоят из Window
объекты. Просто добавьте интерфейсы к классам окон и экспортируйте их, затем извлеките их из каталога, как указано выше (в App.xaml.cs... это файл начальной загрузки WPF).
Я бы предложил использовать ViewModel - Первый подход https://github.com/Caliburn-Micro/Caliburn.Micro
см.: https://caliburnmicro.codeplex.com/wikipage?title=All%20About%20Conventions
использование Castle Windsor
как контейнер МОК.
Все о конвенциях
Одна из главных особенностей Caliburn.Micro проявляется в его способности устранить необходимость в коде котельной плиты, действуя на основе ряда соглашений. Некоторые люди любят условности, а некоторые ненавидят их. Вот почему соглашения CM полностью настраиваемы и даже могут быть отключены полностью, если не требуется. Если вы собираетесь использовать соглашения, и, поскольку они включены по умолчанию, полезно знать, что это за соглашения и как они работают. Это предмет этой статьи. Разрешение просмотра (ViewModel - First)
основы
Первое соглашение, с которым вы, вероятно, столкнетесь при использовании CM, связано с разрешением представления. Это соглашение затрагивает любые области ViewModel - First вашего приложения. В ViewModel - First у нас есть существующая ViewModel, которую нам нужно отобразить на экране. Для этого CM использует простой шаблон именования, чтобы найти UserControl1, который он должен связать с ViewModel и отобразить. Итак, что это за образец? Давайте просто посмотрим на ViewLocator.LocateForModelType, чтобы узнать:
public static Func<Type, DependencyObject, object, UIElement> LocateForModelType = (modelType, displayLocation, context) =>{
var viewTypeName = modelType.FullName.Replace("Model", string.Empty);
if(context != null)
{
viewTypeName = viewTypeName.Remove(viewTypeName.Length - 4, 4);
viewTypeName = viewTypeName + "." + context;
}
var viewType = (from assmebly in AssemblySource.Instance
from type in assmebly.GetExportedTypes()
where type.FullName == viewTypeName
select type).FirstOrDefault();
return viewType == null
? new TextBlock { Text = string.Format("{0} not found.", viewTypeName) }
: GetOrCreateViewType(viewType);
};
Давайте сначала проигнорируем переменную context. Чтобы получить представление, мы предполагаем, что вы используете текст "ViewModel" в именовании ваших виртуальных машин, поэтому мы просто меняем его на "View" везде, где мы его находим, удаляя слово "Model". Это приводит к изменению имен типов и пространств имен. Таким образом, ViewModels.CustomerViewModel станет Views.CustomerView. Или, если вы организуете свое приложение по функции: CustomerManagement.CustomerViewModel становится CustomerManagement.CustomerView. Надеюсь, это довольно просто. Как только у нас есть имя, мы ищем типы с этим именем. Мы ищем любую сборку, которую вы представили CM, как доступную для поиска через AssemblySource.Instance.2 Если мы находим тип, мы создаем экземпляр (или получаем его из контейнера IoC, если он зарегистрирован) и возвращаем его вызывающей стороне. Если мы не найдем тип, мы создадим представление с соответствующим сообщением "not found".
Теперь вернемся к этому "контекстному" значению. Вот как CM поддерживает несколько видов в одной и той же модели представления. Если предоставляется контекст (обычно строка или перечисление), мы делаем дальнейшую трансформацию имени на основе этого значения. Это преобразование фактически предполагает, что у вас есть папка (пространство имен) для разных представлений, удалив слово "View" в конце и добавив вместо этого контекст. Таким образом, учитывая контекст "Master", наша ViewModels.CustomerViewModel станет Views.Customer.Master.
Еще одно простое решение — создать расширение murkup, которое разрешает вашу модель представления по ее типу:
public class DISource : MarkupExtension {
public static Func<Type, object, string, object> Resolver { get; set; }
public Type Type { get; set; }
public object Key { get; set; }
public string Name { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider) => Resolver?.Invoke(Type, Key, Name);
}
Вы можете настроить это расширение для любого контейнера DI следующим образом:
protected override void OnStartup(StartupEventArgs e) {
base.OnStartup(e);
DISource.Resolver = Resolve;
}
object Resolve(Type type, object key, string name) {
if(type == null)
return null;
if(key != null)
return Container.ResolveKeyed(key, type);
if(name != null)
return Container.ResolveNamed(name, type);
return Container.Resolve(type);
}
Используйте его в XAML так просто:
DataContext="{local:DISource Type=viewModels:MainViewModel}"
Таким образом, вы сможете легко назначать DataContext вашему представлению и автоматически вводить все необходимые параметры непосредственно в вашу модель представления с помощью контейнера внедрения зависимостей. С помощью этого метода вам не нужно передавать контейнер внедрения зависимостей или другие параметры конструктору представления.
DISource не зависит от типа контейнера, поэтому вы можете использовать его с любой инфраструктурой внедрения зависимостей. Достаточно задать для свойства DISource.Resolver метод, который знает, как использовать ваш контейнер внедрения зависимостей.
Я описал этот метод более подробно в разделе Внедрение зависимостей в приложение WPF MVVM.
Удалите загрузочный URI из вашего app.xaml.
App.xaml.cs
public partial class App
{
protected override void OnStartup(StartupEventArgs e)
{
IoC.Configure(true);
StartupUri = new Uri("Views/MainWindowView.xaml", UriKind.Relative);
base.OnStartup(e);
}
}
Теперь вы можете использовать свой класс IoC для создания экземпляров.
MainWindowView.xaml.cs
public partial class MainWindowView
{
public MainWindowView()
{
var mainWindowViewModel = IoC.GetInstance<IMainWindowViewModel>();
//Do other configuration
DataContext = mainWindowViewModel;
InitializeComponent();
}
}