Доброго времени суток!
Пишу 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-приложениях?
Дисклеймер: Я не буду использовать 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 вам будет подсказывать, где там какие свойства у вьюмодели для биндингов.
Допустим имеется метод обращающийся к полям класса, метод вызывается в потокахКак я понял, поля класса являются разделяемыми между всеми...
А также:
У меня есть код который должен работать 24/7 и есть сервер через который я должен его запускатьТо есть отправил запрос http://site