Использование Ninject в MVVM-приложении

289
24 июня 2022, 08:10

Доброго времени суток!

Пишу WPF-приложение и хочу использовать Dependancy Injection. Однако, не могу использовать MVVM из-за того, что не получается каким-либо образом ввести зависимость во ViewModel. Свойство DataContext для окон задаю в XAML-разметке, поэтому если моя ViewModel имеет внедрение зависимости через конструктор, то получаю постоянно NullReferenceException.

В принципе, более-менее работает вариант с установкой DataContext через codeBehind окна, но из-за этого теряются многие удобные фичи XAML-редактора VS.

Сейчас мой код выглядит так:

App.xaml.cs:

public partial class App : Application
{
    private IKernel container;
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        container = new StandardKernel();
        container.Load(new Common.ReminderModule());
        Current.MainWindow = this.container.Get<MainWindow>();
        Current.MainWindow.Title = "DI with Ninject";
        Current.MainWindow.Show();
    }
    public IKernel GetContainer()
    {
        return container;
    }
}

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    private readonly ICoreStorage coreStorage;
    public MainWindow(ICoreStorage coreStorage)
    {
        this.coreStorage = coreStorage;
        DataContext = new MainWindowViewModel(this.coreStorage);
        InitializeComponent();
    }
}

MainWindowViewModel.cs

public class MainWindowViewModel:ViewModelBase
{
    private readonly ICoreStorage coreStorage;
    private ObservableCollection<ICompany> companies = new ObservableCollection<ICompany>();

    public MainWindowViewModel(ICoreStorage coreStorage)
    {
        this.coreStorage = coreStorage;
        GetCompanies();
    }        
    public ObservableCollection<ICompany> Companies
    {
        get { return companies; }
        set { companies = value; RaisePropertyChanged("Companies"); }
    }

    private async void GetCompanies()
    {
        Companies.Clear();
        foreach( var company in await coreStorage.GetCompanyStorage("D:\\Tests\\Reminder").GetCompanies())
        {
            Companies.Add(company);
            RaisePropertyChanged("Companies");
        }
    }
}

Мой вопрос больше концептуальный. Правильно ли я использую Ninject? Есть ли способ передать зависимость во ViewModel, если она определяется через разметку? И есть ли какие-то best practises для использования DI в MVVM-приложениях?

Answer 1

Дисклеймер: Я не буду использовать Ninject в ответе.

Попробую ответить простыми словами, так как сам совсем недавно узнал, как работает IoC+DI. Тема очень популярная, но сходу в ней разобраться не так-то просто. Мне помогли другие участники StackOverflow, за что им спасибо. Пытался вникнуть в тему я здесь (вопрос по ссылке был задан именно с целью разобраться, как работает IoC контейнер, ну или хотя-бы базовая его часть, практической ценности в вопросе мало, ознакомьтесь, если пока не понимаете, как работает DI+IoC изнутри).

Сам я выбрал контейнер Autofac, поэтому пример покажу с ним. Ninject тоже рассматривал, но там непонятная история с производительностью, у контейнера не идеальная репутация. Альтернативно рассматривал Unity контейнер, но остановился именно на Autofac.

Так как у вас есть ошибки при реализации приложения, начну с них и в самом конце расскажу про XAML-проблему.

Класс окна выглядит вот так

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

Всё, я не увидел ни одной причины инжектить сюда ICoreStorage.

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

В конструкторе окна можно взаимодействовать только с контролами этого же самого окна. В конструкторе ViewModel можно взаимодействовать только с полями и свойствами, присутствующими внутри класса. Если некоторые свойства требуют внешних данных для инициализации, их нельзя инициализировать в конструкторе.

И вот всё что я сказал про ограничения для конструктора окна - в MVVM относится ко всему классу окна. Если пишете код-бихайнд, делайте это так, чтобы не обращаться к данным (вообще к любым), работайте в код-бихайнде исключительно только с контролами и их свойствами. Я бы даже сказал, ограничьтесь тем, что имеете внутри обработчика события (sender, e), старайтесь не обращаться к тому, что за его пределами. Тогда почти наверняка не будет нарушен MVVM.

Теперь вьюмодель.

public class MainWindowViewModel : ViewModelBase
{
    // если поля называть с _подчеркивания, не придется везде втыкать this, но дело вкуса.
    private readonly ICoreStorage _coreStorage;
    private ObservableCollection<ICompany> _companies;
    public MainWindowViewModel(ICoreStorage coreStorage)
    {
        _coreStorage = coreStorage;
    }        
    public ObservableCollection<ICompany> Companies
    {
        get { return _companies; }
        set { _companies = value; RaisePropertyChanged(nameof(Companies)); }
    }
    public async Task GetCompanies()
    {
        // у вас в BaseViewModel реализован INotifyPropertyChanged, поэтому можно просто вот так
        var companies = await coreStorage.GetCompanyStorage(@"D:\Tests\Reminder").GetCompanies();
        Companies = new ObservableCollection(companies);
    }
}

Теперь Composition Root - точка сборки приложения из классов, которая у вас, да и у меня расположена в OnStartup().

using Autofac;
protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);
    // регистрация классов
    ContainerBuilder builder = new ContainerBuilder();
    builder.RegisterType<CoreStorage>().As<ICoreStorage>().SingleInstance();
    builder.RegisterType<MainWindowViewModel>().SingleInstance();
    builder.RegisterType<MainWindow>();
    // поехали
    IContainer container = builder.Build();
    MainWindow window = container.Resolve<MainWindow>();
    window.Loaded += async (s, e) =>
    {
       try
       {
           await ((MainWindowViewModel)window.DataContext).GetCompanies();
       }
       catch (Exception ex)
       {
           // всегда обрабатывайте все возможные исключения для async void методов, иначе вы попросту их не увидите
           Debug.Fail(ex.Message);
       }
    }
    window.Show();
}

Нет никакой нужды трогать здесь Application.Current.MainWindow, WPF сделает это за вас.

Событие Window.Loaded подходит для загрузки данных из внешних источников намного лучше, чем конструктор. В данном случае как минимум потому что обработчик события можно сделать асинхронным и обработать в нем исключение, в конструкторе такой фокус не прокатит.

Теперь, что же делать с подсказками в XAML редакторе, а ему нужно просто показать, какой тип будет у DataContext.

<Window x:Class="..."
        ...
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:..."
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance local:MainWindowViewModel}">

И всё, теперь IntelliSense вам будет подсказывать, где там какие свойства у вьюмодели для биндингов.

READ ALSO
Многопоточный доступ к объектам

Многопоточный доступ к объектам

Допустим имеется метод обращающийся к полям класса, метод вызывается в потокахКак я понял, поля класса являются разделяемыми между всеми...

268
Ошибка cs0120 как исправить

Ошибка cs0120 как исправить

Я не понимаю, как тут исправить данную ошибку:

266
Как заставить выполняться длительный код по нажатию кнопки ASP.NET Core

Как заставить выполняться длительный код по нажатию кнопки ASP.NET Core

У меня есть код который должен работать 24/7 и есть сервер через который я должен его запускатьТо есть отправил запрос http://site

255