Комментарии 53
Касательно покрытия тестами VM — этим обычно… ну, во всяком случае я, — не занимаются. Текст VM совершенно плоский. И вызовы делегатов из VM — тоже вполне планарны. А вот то, что они вызывают в модели — покрывать тестами действительно стоит.
ЗЫ: Я бы все таки покрыл тестами вызов правильных методов в модели из VM.
Просто есть идеальное видение, с полным покрытием кода, и есть реальный мир, где нередко программисты вообще не пишут юнит-тесты. Растеряв силы или заскучав на проверке VM, они могут и не добраться до модели, а так — пусть хоть модель покроют.
А что будет если Вы пишете веб приложение и юзер отправит в Ваш контроллер данные не через валидирующую форму а напрямую в контроллер, через url get/post запрос. Тогда это может сломать Вашу модель или не дай бог базу данных.
Теперь вопрос по существу (возможно на вопрос «зачем» из 1-й части моего комментария Вы ответите здесь). Предположим есть UserControl, он состоит из 2-х частей View (Xaml) и Codebehind (cs). Во View значит я верстаю саму форму, кладу TextBox. В Codebehind я создаю такие же команды и свойства, как это делаете Вы в своей ViewModel, и во View точно также создаю привязки к этим свойствам и командам. Т.е. Codebehind UserControl'а это Ваш ViewModel. Так для чего же мне уже имея View и Codebehind создавать ещё один файл и выносить туда какой-либо код?
1. Для чего MVVM? Отвечаю: для того, чтобы у вас была самодостаточная модель, сгруппированная в одном месте. Чтобы была отделена от инфраструктурного кода. Если, условно, инфраструктурный код — это 70%(и больше) от всего кода, а бизнес-логика — 30% — то очень хорошо эту модель иметь в одном месте, а не размазанную по обработчикам событий кучи контролов. Тогда модель поддается тестированию, изменению функционала, она компактнее, понятнее и т.д. Всегда можно взять модель и написать совершенно другой инфраструктурный код, для другой платформы, другой библиотеки и т.д.
2. Почему нельзя использовать кодбехайнд в качестве VM?
Отвечаю: никто не запрещает вам использовать кодбехайнд в качестве DataContext самого себя, но только в случае, если у вас на один View одна VM. Но часто бывает, что одна VM используется для многих View. Это удобно делать, чтобы не городить огромную вьюшку, а разбить ее на ряд вьюшек поменьше — их тогда и перекомпоновать будет проще. В таком случае нелогично использовать один контрол в качестве DataContext других контролов.
В любом приложении есть модель и её представление. Шаблонов проектирования, в которых модель связывается с представлением много: MVC, MVP, MVVM. То, что модель должна быть отделена от инфраструктурного кода — это как раз понятно. Вопрос больше про то, для чего использовать сам шаблон MVVM. Что его использование даёт по сравнению с тем, что я напишу контроллер, через который вид взаимодействует с моделью. Или я буду просто в Codebehind взаимодействовать с моделью. Вопрос не о M, а о VM из этого шаблона. Я на самом деле знаю ответ на этот вопрос. Просто, читая статью о шаблоне проектирования, хотел бы видеть в ней объяснение. Возможно новички прочитают и не поймут для чего всё это нужно.
- Самому UserControl'у необязательно устанавливать DataContext'ом ссылку на себя, для того, чтобы использовать привязки внутри. Можно, например, задать имя контролу и в привязках ссылаться на контрол по этому имени:
<UserControl x:Name="CalcView"> <TextBox Text="{Binding Path=Value1, ElementName=CalcView}" /> </UserControl>
Использовать один контрол в качестве DataContext другого нелогично, я с Вами согласен. Вот Вы пишете, что часто бывает одна большая VM которая используется для многих View, но городить один большой View не хотите. А почему тогда получается 1 большая VM, котороая во многих View участвует? Почему её не нужно разбивать на более мелкие части, как это делаете с View? Или это что-то вроде медиатора, который устанавливает связь между различными частями модели в 1-м месте, но используется во многих местах? Т.е. другими словами мне пока не понятен смысл выносить код взаимодействия с моделью в отдельный класс, когда есть Codebehind у UserControl. В Вашем примере получается, что Codebehind во всех представлениях чаще всего останется пустым.
Ну, вот конкретно в WPF в том, что MVVM «аппаратно» поддерживается WPF. View понимает и INotifyPropertyChange, и Observable и др. — ее не надо руками обновлять через презентер. Интерфейс вьюшки городить не надо. Биндинг в конце-концов. Мне MVP в WinForms'ах не нравился тем, что уж очень там много руками делать приходилось.
Одна VM для нескольких вьюшек используется, не столько для того, чтобы расшарить контекст, а скорее для того, чтобы разбить вью на физические части, а потом скомпоновать в общей вьюшке. Просто View в XAMLе могут быть достаточно… громоздкими, аляповатыми, знаете, со всеми этими <Grid.RowDefinitions>… да и вообще, как любой XMLобразный код.
В вашем примере с привязками вы биндитесь к DependencyProperty, в случае же использования DataContext подойдут регулярные свойства. Они по объему кода гораздо скромнее
На мой взгляд то, где будет VM физически — это незначимая деталь реализации. Но можно дать такой ответ:
В вашей конструкции при биндинге надо всякий раз указывать имя элемента управления, и второе: неясно, что делать, в случае нескольких View на один VM.
Если всякие украшательства, типа анимации — то в XAMLe. Если в XAML они не лезут, то тогда в код бехайнд.
Чистый MVVM значит, что можно миксовать MVVM и MVP. Хотя можно с помощью windows.interactivity попытаться оставаться в рамках MVVM, но если программа сильно «вьюхоцентричная», то можно создать интерфейс вьюшки и вызывать его.
code behind… всегда будет оставаться пустым, если используется чистый MVVM
Использование code behind никак не противоречит MVVM, если он используется для вьюшной логики, т.е. не зависящей от модели. Например, если видимость зависит от состояния модели (наличие заказов заказов у клиента), то да, оно будет во вью-модели как IsVisible (или вообще через конвертер вычисляться). А если видимость зависит от размера окна, то во вью-модели ей делать нечего.
А из вашей же фразы можно сделать вывод, что если code behind используется, то MVVM «грязный». И да, некоторые «умельцы», рассуждая так, доходят до того, что прокидывают ссылку на вью во вью-модель, пилят логику с ней там и хвастаются, что у них code-behind пустой :).
Один из плюсов использования связки mvvm и wpf это использование шаблонов. Такой подход позволяет заменить почти всех наследников UserControl на DataTemplate.
Дополнительно упрощается поддержка вложенных vm.
Мое мнение, что лучше его вообще не использовать, кроме двух случаев.
1 Это кросплатформенная программа тогда в идеале у каждой платформы меняется только View.
2 У вас есть офигенный дизайнер который может ваять формы в Blend, а программеры уже просто подключают свои поля куда надо. (но для большинства это наверно из области фантастики)
Больше я не вижу, чем MVVM лучше. Куча геморроя, с текстовыми значениями, без перехода по ссылкам, с диким пробросом данных в кривые контролы и попапы.
А уж если кто-то «мудрый» решил подключить Caliburn…
Предположим есть UserControl, он состоит из 2-х частей View (Xaml) и Codebehind (cs). Во View значит я верстаю саму форму, кладу TextBox. В Codebehind я создаю такие же команды и свойства, как это делаете Вы в своей ViewModel, и во View точно также создаю привязки к этим свойствам и командам. Т.е. Codebehind UserControl'а это Ваш ViewModel. Так для чего же мне уже имея View и Codebehind создавать ещё один файл и выносить туда какой-либо код?
Допустим, я сделел UserControl для отрезка дат (с даты — до даты, два DatePicker`а). И для своей модели сделал им привязку к, например, датам поиска вылета самолёта. Тут следует начать с того, что это нарушение принципа единственной обязанности, что скоро меня приведёт к печальным последствиям.
Тут меня попросили сделать такой же контрол, но для дат выезда поездов. Что мне делать? Наследоваться? Копипастить? Я бы, всё-таки, хотел иметь возможность просто указать, откуда мне брать данные для начала и конца отрезка дат. Вот тут мне может помочь VM и не может помочь CodeBehind.
В Codebehind я создаю такие же команды и свойства, как это делаете Вы в своей ViewModel, и во View точно также создаю привязки к этим свойствам и командам.Если вы привяжетесь к одной какой-то модели, то эта привязка станет частью контрола, и использвоать в другом проекте вы его не сможете, не потащив за собой модель этого. Это очевидные вещи, да. Но вы почему-то о них спрашиваете.
Если вы привяжетесь к одной какой-то модели, то эта привязка станет частью контрола, и использвоать в другом проекте вы его не сможете, не потащив за собой модель этого. Это очевидные вещи, да. Но вы почему-то о них спрашиваете.
Здесь я Вас уже перестал понимать, в контексте предложенной Вами задачи.
Я задавал вопрос автору статьи про Codebehind и хотел выяснить разницу между VM и Codebehind. Вы же привели в пример конкретную задачу.Я привёл конкретный пример вашей же задачи для простоты восприятия.
Всё началось с:
Предположим есть UserControl, он состоит из 2-х частей View (Xaml) и Codebehind (cs). Во View значит я верстаю саму форму, кладу TextBox.Любой UserControl должен быть самостоятельным элементом управления.
Затем
В Codebehind я создаю такие же команды и свойства, как это делаете Вы в своей ViewModel, и во View точно также создаю привязки к этим свойствам и командам. Т.е. Codebehind UserControl'а это Ваш ViewModel.
Некорректность утверждения о том, что «Codebehind UserControl'а это Ваш ViewModel» я вам в этой ветке и освещаю. Разница в том, что Codebehind — это часть самого элемента View, а VM же подсказывает View, откуда ему брать данные и какие команды использовать. VM может содержать ссылки на модель, Codebehind — нет. VM могут быть разные, предоставляющие разные данные и разные команды, Codebehind для контрола всегда один и тот же.
В итоге, могу предположить, что вы что-то своё понимаете под UserControl.
Вобщем, вступать в дальнейший спор (или продолжать) я не хочу.
Я по Вашим ответам понял, какие преимущества дает MVVM. Большое спасибо!
Автор, а вы ничего не путаете? Логика в модели? И логика работы с DAL тоже в модели? По-моему модель это именно модель данных, набор свойств с минимумом логики, необходимой для этих свойств. В вашем примере со сложением логике сложения, конечно, самое место в модели, но если логика чуть сложнее, то в модели ей не место.
Пример выбран неудачно, мввм тут из пушки по воробьям .
Конечно сложение чисел с МВВМ — это оверкил, но позволяет сосредоточится на теории, не загромождая все реализацией. К тому же это не последний пример
Модель, в принципе, может не хранить никакого состояния. Т.е. она может вполне быть реализована статическим методом статического класса.В широком понимании модели, да. Но, как правило, под моделью понимается доменная модель.
если у нас во View есть три текстовых поля, или три места, которые должны вводить/выводить данные — следовательно в VM (своего рода подложке) должны быть минимум три свойства, которые эти данные принимают/предоставляют.Это не так. В VM у нас может быть один объект и это могут быть его свойства.
Синее — это VM, к которой эти три зеленых точки железно прибиты (прибиндены)Биндинги в WPF — это нечто прямо противоположное «железному прибитию». По умолчанию, они прописываются простыми строками и им плевать на то, есть ли свойство или нет — просто всё перестанет работать.
public int Number1Пожалуйста, не называйте так свойства. И поля. И вообще ничего. В принципе, рекомендую уделить внимание качеству кода.
Затем — операция добавление некоторого числа в коллекцию — это обязанность модели. VM не может залезать во внутренность модели и самостоятельно добавлять в коллекцию модели число, она обязана просить сделать это саму модель. В противном случае это будет нарушение принципа инкапсуляции.Тут немножко странные вещи описаны, начиная с того, что инкапсуляция — это не принцип, а механизм и нарушить её хоть и можно, но не таким способом.
Для того, чтобы создать связь кнопки и VM, необходимо использовать DelegateCommand.Я надеюсь, что имелся в виду ICommand.
//таким нехитрым способом мы пробрасываем изменившиеся свойства модели во ViewУвы, нет. Мы не «пробрасываем свойства», мы используем один из плохих приёмов программирования — программирование по совпадению. Мы вызываем событие у одного объекта, используя название изменившегося свойства другого. Да, в этом примере всегда будет передаваться Sum, и по совпадению (!) такое свойство есть у VM и оно даже (по совпадению!) отображает те данные что нам нужны. Изменить название свойства или его логику — всё поломается. Так делать нельзя.
_model.PropertyChanged += (s, e) => { RaisePropertyChanged(e.PropertyName); };
//проверка на валидность ввода — обязанность VMСмелое утверждение. Я вот считаю, что проверка на валидность того, что вводит пользователь — обязанность View. Если же проверка на валидность — обязанность VM, то как и где он будет уведомлять пользователя в случае ошибки?
Это не так. В VM у нас может быть один объект и это могут быть его свойстваНе придирайтесь. Я имею ввиду, что сколько во вью открыто «слотов» для биндинга, столько должно ему предоставлять VM. А под словом «железно» имеется ввиду, что VM это не нечто, как модель — абстрактное в вакууме, а такое нечто, что зависит от вью, имеет открытых «слотов» столько, чтобы удовлетворить свою View
…
Биндинги в WPF — это нечто прямо противоположное «железному прибитию»
… инкапсуляция — это не принцип, а механизм
Инкапсуляция — это как раз принцип. А уж как именно вы инкапсулируете — это механизм.
Увы, нет. Мы не «пробрасываем свойства», мы используем один из плохих приёмов программирования — программирование по совпадению…В статье акцент сделан в первую очередь на MVVM, а это удобная конструкция, избавляющая от необходимости прописывать каждое пробрасываемое свойство из модели. В реальных проектах такое не прокатывает и чревато — это понятно.
… считаю, что проверка на валидность того, что вводит пользователь — обязанность View..Имелось ввиду валидация в конкретном случае. Вообще же — валидация сначала во View (уже даже самим типом контрола — используя RadioButton сложно ввести невалидное значение). Если нельзя во вью, то в VM. Если нельзя в VM, то задействуем Модель.
Не придирайтесь.Я не придираюсь к вам, я освещаю некоторые моменты, которые, неосведомлённый читатель поймёт неверно и сделает неверные выводы.
модель — абстрактное в вакууме
VM [...] такое нечто, что зависит от вьюЭти две фразы неверны. VM не зависит от View, а наоборот. Это позволяет использовать VM с разными View.
Модель — это вполне конкретная вещь.
Инкапсуляция — это как раз принцип.Ну я не знаю, хотя бы в википедии посмотрите. Возможно, вы путаете её с принципом сокрытия данных.
В статье акцент сделан в первую очередь на MVVM, а это удобная конструкция, избавляющая от необходимости прописывать каждое пробрасываемое свойство из модели.Ну так в примере вы именно тем и занимаетесь, что прописываете свойство модели в свойстве VM (это я про сумму). Если будет больше полей — будете больше пропихивать свойств.
Если нельзя во вью, то в VM. Если нельзя в VM, то задействуем Модель.Вот такие «где получится — там и будет» рассуждения и приводят к СКС в стиле картинки №1.
Эти две фразы неверны. VM не зависит от View, а наоборот. Это позволяет использовать VM с разными View.
Я использую две методики написания программ в MVVM: 1.Model first 2. View first
В обоих VM разрабатывается после View.
Про модель в вакууме — значит, что она не зависит от VM и View, и ничего про них не знает. У нею своя атмосфера.
Смелое утверждение. Я вот считаю, что проверка на валидность того, что вводит пользователь — обязанность View. Если же проверка на валидность — обязанность VM, то как и где он будет уведомлять пользователя в случае ошибки?
А использование интерфейса IDataErrorInfo для VM и указание соответствующих свойств в биндинге во View куда можно отнести? В общем-то получается для валидации задействовано и то, и другое. Возможно я не прав, но разве View должна знать какие данные верны, а какие нет? Она должна лишь уведомлять о том, что данные не верны (красное поле ввода, подсказка что не так).
Я для себя делал так (уверен что найдется масса ошибок, но я пока ещё учусь):
Есть правило валидации:
public class ValidationRule
{
public ValidationRule(string columnName, Func<string> rule)
{
_columnName = columnName;
_rule = rule;
}
private string _columnName;
private readonly Func<string> _rule;
public string ColumnName {
get {
return _columnName;
}
}
public Func<string> Rule {
get {
return _rule;
}
}
}
И соответственно есть список правил. Их мы задаем в VM. И там же указываем что делать при изменении корректности всего списка правил (например блокируем/разблокируем кнопку сохранения). Через интерфейс IDataErrorInfo поля ввода во View получают информацию о корректности данных и в случае ошибки отображают заданную подсказку.
public class ValidationObject : IDataErrorInfo
{
#region IDataErrorInfo
private List<ValidationRule> _validationRules = new List<ValidationRule>();
private Dictionary<string, bool> _propertiesCorrectly = new Dictionary<string, bool>();
private Action<bool> _changingCorrect;
public void AddValidationRule(ValidationRule rule)
{
if (_validationRules.Where(r => r.ColumnName == rule.ColumnName).Count() != 0)
return;
_validationRules.Add(rule);
_propertiesCorrectly.Add(rule.ColumnName, false);
}
public void SetActionOnChangingCorrect(Action<bool> act)
{
_changingCorrect = act;
}
string IDataErrorInfo.this[string columnName]
{
get
{
ValidationRule rule = _validationRules.FirstOrDefault(x => x.ColumnName == columnName);
string result = null;
if (rule != null)
{
result = rule.Rule();
_propertiesCorrectly[rule.ColumnName] = (result == null) ? true : false;
_changingCorrect?.Invoke(!_propertiesCorrectly.Values.Contains(false));
}
return result;
}
}
string IDataErrorInfo.Error
{
get
{
throw new NotImplementedException();
}
}
#endregion
}
Возможно я не прав, но разве View должна знать какие данные верны, а какие нет?А почему, собственно, нет? Уточню, что View должна знать о верности данных в контексте взаимодействия с ней пользователя. То есть, например, у нас может быть поле Price, которое в форме ввода не может быть больше 1000, но при этом сами данные — могут быть, если мы их получили в результате каких-то накруток налогов, например, либо получив автоматически из третьего источника. А в другой форме, по каким-то своим причинам, мы ограничим стоимость уже 100.
При всём при этом, у нас есть возможность ограничить через DataAnnotations сами данные, например, указав, что цена не может быть отрицательной никогда и ни за что.
Как дополнительный бонус, при просмотре XAML наглядно видно, какие поля какими ограничениями обладают.
Есть правило валидации:Вы, возможно по совпадению, употребляете название библиотечного класса ValidationRule, не наследуясь от него и не используя его во View, где он и должен быть. Почитайте статью, там прописано и как использовать этот класс, и как валидировать во View.
Ещё раз подчеркну, что во View валидировать необязательно, если только нет каких-то специфических требований, связанных только с конкретным View.
Кстати, в целом, система валидации в WPF довольно кривая. Объединять ошибки из ValidationRule`s и DataAnnotations — это то ещё веселье. А если использовать виверы (тот же Validar) — то становится ещё веселее.
Допустим из одного окна (родительского), по нажатию кнопки показывается другое окно (дочернее).
Где, согласно принципам MVVM, должны быть инстранцированы View и ViewModel дочернего окна?
допустимо ли делать это в code behind родительского окна?
ShowSomeWindowCommand = new DelegateCommand<Window>(own =>
{
//someVar и anotherVar используются в конструкторе SomeWindow, чтобы создать себе VM
var dlg = new SomeWindow(someVar, anotherVar) {Owner = own};
var result = dlg.ShowDialog();
...
});
В качестве CommandParameter передаю родительское window.
В Caliburn Micro есть IWindowManager.ShowDialog(object viewModel). А в Prism это решалось event+behavior. Но однозначно не стоит создавать Window во ViewModel.
Я вам не могу ничего запрещать ) но на мой лично субъективный взгляд это немного не правильно. В таком случае лучше передавать в DelegateCommand просто object и через внешний Service/Manager связывать переданный ViewModel с соответствующей View, например, как это сделано в Caliburn.Micro:
ConfirmCommand = new DelegateCommand<object>(owner =>
{
var confirmViewModel = new ConfirmViewModel();
var settings = new Dictionary<string, object>
{
{ "Owner", owner }
};
if (WindowManager.ShowDialog(confirmViewModel, null, settings) == true)
{
// do some action
}
}
В ооочень больших проектах с MVVM влазишь в кашу из зависимостей, логики команд и свойств модели. Отчасти решается выносом команд в отдельные классы и добавлением контроллера. И получается уже гибрид MVVMC.
У вас в первом примере ошибка в свойстве.
Я в коде поправил на так:
public int Number3 { get => MathFuncs.GetSumOf(Number1, Number2); }
this.WhenActivated(disposable =>
{
this.Bind(ViewModel, x => x.Title, x => x.TitleBox.Text)
.DisposeWith(disposable);
});
MVVM: полное понимание (+WPF) Часть 1