Как обрабатывать инъекцию зависимостей в приложении 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;
    }
}

С этим я могу легко написать модульные тесты, чтобы убедиться, что они работают правильно, используя mocks и т.д.

Проблема в том, когда дело доходит до использования в реальном приложении. Я знаю, что у меня должен быть контейнер IoC, который связывает реализацию по умолчанию для интерфейса IStorage, но как я могу это сделать?

Например, как бы это было, если бы у меня был следующий xaml:

<Window 
    ... xmlns definitions ...
>
   <Window.DataContext>
        <local:SomeViewModel />
   </Window.DataContext>
</Window>

Как я могу правильно "указать" WPF для встраивания зависимостей в этом случае?

Кроме того, предположим, что мне нужен экземпляр SomeViewModel из моего кода cs, как это сделать?

Я чувствую, что полностью потерял в этом, я был бы признателен за любой пример или руководство, как наилучшим образом справиться с этим.

Я знаком с StructureMap, но я не эксперт. Кроме того, если есть лучшая/упрощенная/готовая структура, сообщите мне.

Спасибо заранее.

Ответ 1

Я использую 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), который обычно рассматривается как анти-шаблон, поскольку он скрывает зависимости класса. Однако очень сложно избежать какого-либо ручного поиска службы для классов пользовательского интерфейса, поскольку они должны иметь конструктор без параметров, и вы все равно не сможете управлять импотенцией, поэтому вы не можете вводить виртуальную машину. По крайней мере, этот способ позволяет вам изолировать виртуальную машину, в которой находится вся бизнес-логика.

Если у кого-то есть лучший способ, пожалуйста, разделите.

EDIT: Lucky Likey предоставил ответ, чтобы избавиться от статического локатора сервисов, позволив Ninject создавать классы пользовательского интерфейса. Детали ответа можно увидеть здесь

Ответ 2

В вашем вопросе вы задаете значение свойства 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, вы теряете некоторую поддержку времени разработки. Если ваша модель просмотра содержит некоторые данные, она появится во время разработки, что может быть очень полезно. К счастью, вы можете использовать атрибуты времени разработки в 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.
  }

}

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

Ответ 3

Я перехожу к подходу "view first", где передаю модель представления в конструктор представлений (в своем коде), который присваивается контексту данных, например.

public class SomeView
{
    public SomeView(SomeViewModel viewModel)
    {
        InitializeComponent();

        DataContext = viewModel;
    }
}

Это заменяет ваш подход на основе XAML.

Я использую фреймворк Prism для обработки навигации - когда некоторый код запрашивает отображение определенного вида (путем "навигации" к нему), Prism решит это представление (внутренне, используя инфраструктуру приложения DI); структура DI в свою очередь решит любые зависимости, которые имеет вид (модель представления в моем примере), затем разрешает ее зависимости и т.д.

Выбор структуры DI в значительной степени не имеет отношения к делу, так как все они выполняют одно и то же, то есть вы регистрируете интерфейс (или тип) вместе с конкретным типом, который вы хотите создать, когда он обнаруживает зависимость от этого интерфейса, Для записи я использую Castle Windsor.

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

Альтернативно взгляните на одну из сред MVVM, такую ​​как MVVM Light. У меня нет опыта в этом, поэтому я не могу прокомментировать, что они хотели использовать.

Ответ 4

Установите индикатор MVVM.

Часть установки - это создание локатора модели представления. Это класс, который предоставляет ваши viewmodels в качестве свойств. Затем получателю этих свойств могут быть возвращены экземпляры из вашего механизма IOC. К счастью, свет MVVM также включает в себя структуру 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.

Ответ 5

Что я публикую здесь, это улучшение для sondergard Ответ, потому что то, что я расскажу, не вписывается в комментарий:)

В самом деле я представляю аккуратное решение, которое позволяет избежать необходимости ServiceLocator и обертки для StandardKernel -Instance, которая в sondergard Solution называется IocContainer. Зачем? Как уже упоминалось, это анти-шаблоны.

Обеспечение доступности StandardKernel везде

Ключ к магии Ninject - это StandardKernel -Instance, который необходим для использования .Get<T>() -Method.

В качестве альтернативы sondergard IocContainer вы можете создать StandardKernel внутри App -Class.

Просто удалите 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 жив, вы можете выполнять все виды инъекций, например, Inetification Set Setter или наиболее распространенный инжектор конструктора.

Вот как вы вводите ViewModel в свой Window DataContext

public partial class MainWindow : Window
{
    public MainWindow(MainWindowViewModel vm)
    {
        DataContext = vm;
        InitializeComponent();
    }
}

Конечно, вы также можете вставлять IViewModel, если вы выполняете правильные привязки, но это не является частью этого ответа.

Доступ непосредственно к ядру

Если вам нужно напрямую вызвать методы в ядре (например, .Get<T>() -Method), вы можете позволить ядру вводить себя.

    private void DoStuffWithKernel(IKernel kernel)
    {
        kernel.Get<Something>();
        kernel.Whatever();
    }

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

    [Inject]
    public IKernel Kernel { private get; set; }

Хотя это может быть очень полезно, я бы не рекомендовал вам это делать. Просто обратите внимание, что объекты, введенные таким образом, не будут доступны внутри конструктора, потому что они вставляются позже.

В соответствии с этой ссылкой вы должны использовать factory -Extension вместо того, чтобы вводить IKernel (контейнер DI).

Рекомендуемый подход к использованию контейнера DI в программной системе заключается в том, что корневой состав приложения является единственным местом, где непосредственно коснулся контейнер.

Как использовать Ninject.Extensions.Factory также может быть красный здесь.

Ответ 6

Используйте Managed Extensibility Framework.

[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, чтобы предоставить вам глобальный контейнер (кешированный, natch).

Что касается того, как вводить модели просмотра, вы вводите их так же, как и все остальные. Создайте конструктор импорта (или поместите оператор import в свойство/поле) в код файла XAML и скажите ему импортировать модель представления. Затем привяжите свой Window DataContext к этому свойству. Обычно ваши корневые объекты выходят из контейнера, обычно состоят из объектов Window. Просто добавьте интерфейсы в классы окон и экспортируйте их, а затем возьмите из каталога, как указано выше (в App.xaml.cs..., что файл начальной загрузки WPF).

Ответ 7

Я бы предложил использовать подход ViewModel - First https://github.com/Caliburn-Micro/Caliburn.Micro

см: https://caliburnmicro.codeplex.com/wikipage?title=All%20About%20Conventions

используйте Castle Windsor как контейнер IOC.

Все о соглашениях

Одна из основных особенностей Caliburn.Micro проявляется в ее способности устранять необходимость в кодовом пластинном коде, действуя на ряд конвенций. Некоторые люди любят конвенции, а некоторые ненавидят их. Вот почему соглашения CMs полностью настраиваются и даже могут быть полностью отключены, если не желательны. Если вы собираетесь использовать соглашения, и, поскольку они по умолчанию включены, хорошо знать, что такое эти соглашения и как они работают. Это тема этой статьи. Разрешение просмотра (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" в наименовании своих виртуальных машин, поэтому мы просто изменяем его на "Просмотр" везде, где мы находим его, удаляя слово "Модель". Это приводит к изменению имен типов и пространств имен. Таким образом, ViewModels.CustomerViewModel станет Views.CustomerView. Или, если вы организуете свое приложение по функциям: CustomerManagement.CustomerViewModel становится CustomerManagement.CustomerView. Надеюсь, это довольно прямолинейно. Как только у нас будет имя, мы будем искать типы с таким именем. Мы ищем любую сборку, которую вы открыли для CM, как доступную для поиска через AssemblySource.Instance.2 Если мы найдем этот тип, мы создаем экземпляр (или получаем один из контейнера IoC, если он зарегистрирован), и возвращаем его вызывающему. Если мы не найдем этот тип, мы создадим представление с соответствующим сообщением "не найдено".

Теперь вернемся к этому значению "контекст". Именно так CM поддерживает несколько представлений над одним и тем же ViewModel. Если предоставляется контекст (обычно строка или перечисление), мы делаем дальнейшее преобразование имени на основе этого значения. Это преобразование эффективно предполагает, что у вас есть папка (пространство имен) для разных представлений, удалив слово "Вид" с конца и добавив контекст. Итак, с учетом контекста "Мастер" наш ViewModels.CustomerViewModel станет Views.Customer.Master.

Ответ 8

Удалите стартовый uri из вашего приложения .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();
    }

}