1

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

Пишу 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&lt;ICompany&gt; Companies
{
    get { return companies; }
    set { companies = value; RaisePropertyChanged(&quot;Companies&quot;); }
}


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

}

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

1 Answers1

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&lt;ICompany&gt; Companies
{
    get { return _companies; }
    set { _companies = value; RaisePropertyChanged(nameof(Companies)); }
}

public async Task GetCompanies()
{
    // у вас в BaseViewModel реализован INotifyPropertyChanged, поэтому можно просто вот так
    var companies = await coreStorage.GetCompanyStorage(@&quot;D:\Tests\Reminder&quot;).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&lt;CoreStorage&gt;().As&lt;ICoreStorage&gt;().SingleInstance();
builder.RegisterType&lt;MainWindowViewModel&gt;().SingleInstance();
builder.RegisterType&lt;MainWindow&gt;();

// поехали
IContainer container = builder.Build();
MainWindow window = container.Resolve&lt;MainWindow&gt;();
window.Loaded += async (s, e) =&gt;
{
   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 вам будет подсказывать, где там какие свойства у вьюмодели для биндингов.

aepot
  • 49,560