MVVM и IOC: обработка Просмотр инвариантов класса модели
Это проблема, с которой я сталкивался с тех пор, как начал использовать MVVM, сначала в WPF и теперь в Silverlight.
Я использую контейнер IOC для управления разрешением Views и ViewModels. Представления, как правило, очень простые, с конструктором по умолчанию, но ViewModels имеют тенденцию к доступу к реальным услугам, все из которых необходимы для их построения. Опять же, я использую контейнер МОК для разрешения, поэтому инъекционные услуги не являются проблемой.
Что становится проблемой - передача необходимых данных в ViewModel с помощью IOC. В качестве простого примера рассмотрим экран, который позволяет редактировать клиента. В дополнение к любым услугам, которые могут потребоваться, для ViewModel для этого экрана требуется объект клиента для отображения/редактирования данных клиента.
При разработке любого типа (не MVVM) библиотеки я считаю это непреложным правилом, что инварианты класса передаются через конструктор. В случаях, когда мне нужны контекстно-зависимые данные для времени построения класса, и рассматриваемый класс управляется контейнерами, я как правило использую абстрактный factory * в качестве моста. В MVVM это кажется излишним, так как большинству ViewModels потребуется их собственный factory.
Несколько других подходов, которые я попытался/рассмотрел, включили (1) метод инициализации/загрузки, в котором я передаю данные, что нарушает правило принудительного применения инвариантов класса через конструктор, (2) передачу данных через контейнер в качестве параметра переопределяет (Unity) и (3) передает данные через глобальную сумку состояния (тьфу).
Каковы альтернативные способы передачи данных, специфичных для контекста, из одного ViewModel в следующий? Может ли какая-либо из фреймворков MVVM решить эту конкретную проблему?
*, который может иметь свои собственные проблемы, например, требуя выбора между вызовом Container.Resolve() или отсутствием управления контейнером ViewModel. У Castle Windsor есть хорошее решение для этого, но AFAIK нет других рамок.
Edit:
Я забыл добавить: некоторые из перечисленных мной вариантов даже не возможны, если вы делаете MVVM "View First", если вы сначала не передаете данные в представление, а затем в ViewModel.
Ответы
Ответ 1
Раньше я много занимался этим вопросом.
Насколько я могу судить, нет других жизнеспособных подходов; вы, кажется, уже глубоко задумались над этим вопросом.
Я просто хочу, чтобы два добавили мой два 0,5 цента по причинам, почему я довольно часто выбираю вариант (1):
- метод init проще реализовать, чем любые другие параметры (ну, Windsor Typed Factory так же просто);
- слабость дизайна, не имеющая параметра contructor, может быть смягчена, обеспечив проверку параметров инициализации позже в жизненном цикле VM
- "место", где вы вызываете метод init, будет таким же, как вы бы назвали конструктор (или абстрактный factory);
- в отличие от абстрактного factory, вы можете разложить метод init в определенном интерфейсе, чтобы обрабатывать несколько виртуальных машин по разному пути наследования (если они были найдены);
- Это справедливый компромисс (по крайней мере, в этом контексте): если вы действительно не можете с ним жить, просто перейдите к решению Factory, не заботясь о (очень мало) сложной накладной.
Ответ 2
Я не совсем понимаю, в чем проблема, поэтому я буду использовать простой и надуманный пример.
Скажем, у вас есть CustomerListViewModel
, в котором содержится сводка каждого клиента. Когда вы выбираете клиента, вы хотите отобразить CustomerDetailViewModel
. Это может привести либо идентификатор клиента, либо тип ICustomer
, который ранее был заполнен в CustomerListViewModel
с информацией о клиенте (в зависимости от того, когда вы хотите загрузить данные, например).
Я думаю, что вы спрашиваете, что произойдет, если CustomerDetailViewModel
также принимает серию сервисов в качестве зависимостей, которые вы хотите разрешить через контейнер (обычно для цепей зависимостей).
Как вы сначала используете модель просмотра, вам нужно создать экземпляр CustomerDetailViewModel
из CustomerListViewModel
, и вы хотите сделать это через контейнер, чтобы соответствующие зависимости были введены соответствующим образом.
Поэтому, как вы упомянули, вы обычно делаете это с помощью абстрактного шаблона factory, например ICustomerDetailViewModelFactory
, который передается как служба для CustomerListViewModel
.
Этот тип factory имеет метод ICustomerDetailViewModel GetCustomerDetailViewModel(ICustomer customer)
. Этот тип factory потребует ссылки на ваш контейнер IoC.
При разрешении ICustomerDetailViewModel
в вашем методе GetCustomerDetailViewModel
factory вы можете указать значение, которое вы хотите использовать для параметра конструктора ICustomer, когда вы вызываете Resolve на свой контейнер.
Например, Unity имеет параметр переопределяет параметр и видит здесь для Castle Виндзорская поддержка. У Castle Windsor также есть набранный factory объект, так что вам не нужно реализовывать конкретные типы factory, просто абстракции.
Итак, я бы выбрал вариант 2! Мы используем Caliburn.Micro, он решает много проблем MVVM, но я не знаю каких-либо фреймворков, которые решают эту проблему.
Ответ 3
Я не уверен, что MVVM и IoC могут иметь инварианты классов в конструкторах. По моему опыту, ViewModels создаются в результате ICommand.Execute, что позволяет использовать простой двухэтапный процесс:
var vm = Container.Resolve<CustomerViewModel>();
vm.Model = CustomerRepository.GetCustomerModel(id);
На этом этапе мой просмотр не знает о ViewModel, и это произойдет только тогда, когда я вставляю ViewModel в любой контейнер, связанный с представлением. Я также использую DataTemplates для рендеринга ViewModel, что означает, что мне не нужно создавать экземпляр представления напрямую, чтобы предоставить DataContext.
Итак, чтобы ответить, я бы использовал (1) и нарушил "правило".