Как стать автором
Обновить

Комментарии 60

А можно в двух словах о революционности подхода. А то не все владеют С# и пишут под виндовс. Паттерн MVVM как и другие более глобален. Мне бы как вебпрограммисту, было бы тоже интересно оценить революционность идеи.
ru.wikipedia.org/wiki/Model-View-ViewModel
Мне кажется, что прелесть идеи в том, что мы декларативно (в верстке) задаем как отображать модель пользователю, правда в wpf это доведено до «абсурда» — XAML прекрасен. Среди веб фреймворков я бы назвал backbone, как максимально близкую реализацию классического MVVM.
У меня есть небольшой проект bindit, как раз для веб-проектов, это не совсем чистый MVVM, но вдохновение я черпал оттуда. Вчера довел библиотеку до версии 0.3 и скоро напишу статью на эту тему.
Человек спрашивал не про MVVM, а про революционность его изменений автором в данной статьи ;-)
Перечитал комментарий, Вы правы :)
Поправьте если ошибаюсь, но разве backbone вообще реализует MVVM? Там ведь нет two-way binding. Если говорить о MVVM, а не MVC/MVP, то сразу приходит на ум knockout или angular. Цитата с википедии:
Backbone это JavaScript библиотека основанная на шаблоне проектирования Model-View-Presenter (MVP), предназначена для разработки веб-приложений с поддержкой RESTful JSON интерфейса.
Блин пора на работу, неделя почти без кода плохо сказывается на мозге — конечно хотел написать knockout
Я не очень силён в специфике веб-разработки, но несколько основных идей статьи назову:
— перенос свойств интерфейсной логики во вью модель, минимум CodeBehind;
— задание «чисто» интерфейсных свойств неявным образом и сокращение объёмов кода;
— отказ от инжекций в конструктор;
— усовершенствование механизма RoutedCommands и CommandBindings;
— добаление механизма PropertyBindings в дополнение к обычным Binding;
— интенсивное использование лямбда-выражений;
— стремление к простоте, удобству и красоте кода!
А для чего значения размеров окна хранить на уровне ViewModel? Если я правильно понимаю методологию, там полагается хранить значения, связанные с моделью данных. Размеры окна же являются сугубо View-свойствами, такими же как текущий фокус или положение полос прокрутки, и место им и всем манипуляциям с ними — в CodeBehind.

Мы в своем продукте тоже столкнулись с необходимостью сохранять размеры и расположение окон между запусками, однако реализовали это в виде промежуточного класса BaseWindow, от которого наследуются все окна в программе, чтобы не захламлять ViewModel'ы.
Хотел оспорить, но, обдумав, понял, что всё верно — размерам окна не место во ViewModel.

Но наследования стоит избегать при возможности (favor composition over inheritance). Помню, что решал эту задачу (сохранение размеров) при помощи Attached Behavior, кажется.
«Наследования стоит избегать» — это правило из разряда «goto всегда плохо». В наследовании как таковом проблем нет, проблемы есть в языках, которые ограничивают возможности его применения. Но если в данном конкретном случае эти ограничения вас не касаются, то писать больше кода только ради того, чтобы не использовать наследование — глупо.

Вот если вы дизайните public API (какую-нибудь библиотеку, например), то там, да, придется задуматься о наследовании, потому что проблемы потом будут уже у клиентов вашей библиотеки.
Нет, проблемы не в языках. От наличия множественного наследования в языке оно (наследование) не становится лучше. Как и с goto, в большинстве случаев правило это верное. Спорить мало смысла, расписано это очень много где, каждый должен сам прочувствовать на практике.

писать больше кода только ради того, чтобы не использовать наследование

Если рассматривать нашу задачу с сохранением размеров окна, то вариант с Attached Behavior будет разве что на пару строк больше (подписка на события вместо override). А плюсов много:
— можно не привязываться к классу Window, а взять базовый FrameworkElement (чище и универсальней)
— можно применять результат извне, нет нужды менять код самого окна
— собственно, нет наследования, то есть нет проблем с теми окнами, которые уже унаследованы
— выглядит понятнее, чем наследование, может быть несколько таких Behavior: <Window behaviors:SaveSize=«true» behaviors:BlaBla=«true» />
Это очень резонный вопрос, я и сам думал над этим…
Понятно, что о теории любой методологии можно спорить долго, но я в первую очередь руководствуюсь практической ценностью и удобством.

Для себя я решил, что View это просто интерфейс-картинка (разметка xaml) и в идеале он CodeBehind вовсе не содержит, Мodel, само собой, модель данных, а ViewModel — это то, как отображаются данные, а размер окна это и есть одно из свойств этого отображения. То есть вью-модель это смесь бизнес-логики и интерфейсной… И это нормально.

Но дело-то вот в чём, к примеру, на представлении есть несколько контролов, которые работают с одним свойством ShowDetails по двусторонней привязке (оно на данные никак не влияет, только на их отображение). Дата контекст у всех контролов общий — вью-модель, а сами в свою очередь они отображают бизнес-данные, поэтому дата-контекст просто не поменяешь. Не самое красивое решение создавать привязку по цепочке контролов, поэтому логичнее привязать из к чему-то общему — непосредственно к свойству ShowDetails. Где разместить это свойство? В CodeBehind и создавать умный юай (Smart UI) или поместить во вью-модель? Второе гораздо удобнее, на мой взгляд, да и методологию не нарушает. К тому же применение индексатора позволяет во многих случаях избежать создания свойства ShowDetails в классе как такового, оно лишь подразумевается в xaml…

Не знаю, понятно ли я описал идею, но по своему опыту скажу, что в проекте, который я разрабатываю, это сократило объём кода в разы, а вью-модели остались очень чистыми, и это не пустые слова. Думаю, чтобы ощутить всё это удобство и красоту — нужно просто попробовать.

В этом и есть отчасти некоторая революционность подхода — перенос ряда свойств интерфейса во вью модель.

P.S. Спасибо, что интересуетесь статьёй и обсуждаете её. Смело спрашивайте ещё, если я не совсем понятно объясняю )
> То есть вью-модель это смесь бизнес-логики и интерфейсной…

Ключевое слово «смесь». Это полная противоположность разделению ответственностей. Как только хочется написать «смесь» надо бить себя по рукам.

А по поводу «сравните объёмы и чистоту написанного кода» — это для любых макарон работает, до определенного момента. Это не обязательно плохо, но это не «революция MVVM», а костыль.
Здесь есть разделение ответственности — базовые класы инкапсулируют интерфейсную логику, а конкретные реализации бизнес-логику.
Костылём я бы не назвал, всё это позволило создать полноценное и функциональное приложение, а «тот самый момент», так и не наступил.

Мне тут предложили новое название для паттерна — MVVMP :)
(mvvm+mvp)=mv(vm+p)

Похоже на уравнение из физики.
Не знаю, эволюционный или революционный это подход, но, думаю, имеет право на жизнь :)
По крайней мере, для небольших утилит подходит как нельзя лучше. Хотя есть подозрения, что и в крупных проектах может пригодиться…
> базовые класы инкапсулируют интерфейсную логику

То есть базовые классы — это View, а наследники — ViewModel? Мягко говоря, интересная точка зрения.

> (mvvm+mvp)=mv(vm+p)

SMVVM — Spaghetti MVVM.

Конечно имеет, в небольших утилитах то и не такое писали :)

Я вам подскажу путь дальнейших изысканий. Надо описанным способом сохранять набитые в редакторе тексты, и можно будет Model почистить.
Вы не совсем поняли.
Если рассмотреть пример, то базовые классы — это ViewModelBase и ViewModel, а конкретная реализация MainViewModel. Базовые классы не есть представление (представление — это просто xaml-разметка), скорее они больше похожи на презентатор.

Боюсь, ваше предложение насчёт Model совсем не уместно…

Моя цель не жёсткое следование методологии, а удобство разработки, лаконичность и чистота кода. На реальном проекте (Poet) подход показал себя очень хорошо, и я решил поделиться им с другими людьми. Использовать его или нет, решать вам самим. Согласен, что с первого взгляда он может показаться немного диковатым, но лишь применив его на практике можно почувствовать всю его прелесть, гибкость и мощь.

> лишь применив его на практике можно почувствовать всю его прелесть, гибкость и мощь.

Ок, мне нужно сохранить состояние окна не в файл, а на сервере для разных пользователей, с возможностью прикрутить интерфейс редактирования свойств админу на случай «ой, у меня панелька исчезла, обратно не включаецца!!». Сколько частей MVVMP при этом отредактируется? Что остаётся от гибкости и мощи?
А что вам мешает?

Всё, что нужно, — это делать сериализацию не в файл, а передавать сериализованный объект на сервер.
Конечно, придётся сделать логику администрирования и взаимодействия с сервером, но в этом тоже ничего сложного не вижу.

Может, я не так вас понял? Поправьте меня…
Наверное это не очевидно, но на сервере обычно храниться в РСУБД. Там мне сериализованный объект не очень нужен.
Почему вы не хотите хранить сериализованный файл в БД?
Неужели это кажется таким избыточным? По-моему, очевидно, просто и очень удобно…
Конечно, может быть, у приложения миллионы пользователей, но это уже совсем другая история…
Я в реляционной БД храню реляционные данные.

Например, что для редактирования нормальной БД интерфейс можно сделать в 3 клика, а с сериализованным объектом надо думать. И уж морочиться точно не ради того, чтоб паттерн гуя навязывал мне формат хранения данных.
А картинки вы храните в каком виде? UI — это своего рода не картинка?..

И какие трудности с редактированием сериализованного XML или Json-файла?
Их можно хоть вручную редактировать или сделать простенькую утилиту, в чём сложность?
Даже если кто-то случайно сломает сеарилизованный файл, то пользователю просто-напросто вернётся дефолтная вью-модель в том состоянии, которое было при первом запуске.

Причём, если немного развить этот подход, то можно сделать что-то вроде различных вью-модов. То есть, например, полное представление информации, сокращённое, совсем краткое или даже кастомное. Другими словами — это сохранение настроек (состояний) интерфейса с возможностью выбора.
возможностью выбора любого из сломанных сериализованных файлов — это хорошо. Сразу пароль в БД выдавайте, чего уж там.
Я UI картинками не храню.

> Их можно хоть вручную

хоть на коленке, я уже понял подход. А хоть какая-то валидация?

Сложность в том, что вы мне указываете как хранить данные потому, что ваш паттерн на другую модель хранения нормально не ляжет.
Я вам пример про сломанный файл привёл лишь для того, чтобы подчеркнуть стабильность идеи сериализации.
Понятно, что если вы хотите сделать валидацию, то придётся написать несложную утилиту для этого.

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

Допустим, у меня на сложном представлении до 100 параметров, которые нужно сохранить, вы предлагаете сделать для этого в БД таблицу с сотней столбцов? Ради чего?

Даже в текстовом редакторе на некоторых представлениях число настроек у меня доходило до 30-50.
Зачем усложнять себе жизнь, когда можно сделать намного проще и тоже достаточно красиво?!
> Зачем усложнять себе жизнь

ТЗ такое
По-моему, достаточно аргументов, чтобы обсудить и видоизменить ТЗ, если его, конечно, писали знающие люди. А если вас и слушать не хотят, то повод задуматься…
По-моему тут ни одного аргумента, но обсуждать аргументы и справедливость жизни, вместо обсуждения SMVVM я не хочу.
Мой вам совет — ничего не используйте из этой статьи. Это вам не подходит.
Я и говорю, что ваш паттерн не работает, пока все системы вокруг под него не прогнуться
Не стоит прогибаться под изменчивый мир-
Пусть лучше он прогнётся под нас,
Однажды он прогнётся под нас.

Машина Времени
Не те строчки

Он пробовал на прочность этот мир каждый миг — Мир оказался прочней.

Пойду я, пожалуй. А то уже какие-то аргументы пошли…
Раз на то пошло, то вы уж всю песню ещё раз прослушайте =)

Пессимизм или оптимизм — выбирать всегда вам.

P.S. Что ещё тебе рассказать...
Вы выберите лучше «не стоит прогибаться» или у вас «гибкость и мощь», а то песенки…
> Я и говорю, что ваш паттерн не работает, пока все системы вокруг под него не прогнуться

Не подменяйте понятия, это характеризует вас не с лучшей стороны.
Думайте масштабнее и прослушайте видеоролик из предыдущего моего поста. Не хочу спорить впустую.
вы и не спорите, вы ушли в зубы заговаривать ещё с «Что остаётся от гибкости и мощи?»
Приведённый подход совершенно не требует, чтобы все остальные системы под него прогибались.
И я совершенно не понимаю, что вызывает в вас такое сопротивление. Ничего личного, но, на мой взгляд, вы чуткий человек, однак вам самим нужно быть более гибким.
Если вы хотите, то создавайте в БД таблицы с кучей столбцов или параметров, можете и вовсе не использовать подход из статьи. Вы хотите доказать мне и всем, что я не прав?
Да, хорошо, я не прав. Но пускай читатели сами оценят те материалы, которые я привёл. Никто никого ни к чему не принуждает. Вы обладаете полной свободой своего выбора!
> Если вы хотите, то создавайте в БД таблицы с кучей столбцов или параметров, можете и вовсе не использовать подход из статьи.

У вас нет ощущения, что что-то сильно не так, когда вы предлагаете использовать или не использовать паттерн построения гуя в зависимости от предпочтений хранения?

Вас же дети тут читают, нахватаються бездумно
У меня нет такого ощущения.

Я предлагаю использовать паттерн в зависимоти от вашего желания, а нет от способа хранения.

Мне вот просто интересно… Предложите, пожалуйста, более красивый подход для хранения 100 параметров UI, не создавая их руками в БД?

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

Паттерн элементарный
create table GuiSettingsInt (WindowId int, ParamId int, ParamValue int);
create table GuiSettingsStr (WindowId int, ParamId int, ParamValue varchar(max));


То, что запрос select * from GuiSettingsInt where WindowId=:pWID делает в 100 раз больше индексированных чтений, чем хранение в сериализованном виде, несущественно на этих объёмах (100 записей). То, что сериализованное представление не позволяет менять данные средствами SQL, несущественно, потому что для настроек GUI это бессмысленно.

Таким образом, выбор подхода — чистая вкусовщина. Без разницы, как реализовано сохранение настроек, на характеристики приложения это не влияет никак.
Полностью согласен, что это один из возможных вариантов и выбор способа сохранения просто дело вкуса. Подобный паттерн часто встречается в приложениях.

Насколько я понял, пользователю areht не понравилось то, что теряется реляционное отображение для GUI-свойств (по правилам нужно создать по новому столбцу для каждой настройки). Но я, как и вы, предерживаюсь мысли, что для интерфейсных настроек это, как правило, не нужно, поэтому суть предложенного в статье подхода именно в том, чтобы избавиться от подобной рутинной работы по созданию реальных свойств и маппингу их на столбцы в БД.

Дабы не быть голословным я просто приведу реальный пример. Вот вью-модель для диалога сохранения файлов в редакторе Poet.

[DataContract]
public class SaveViewModel : ViewModel
{
    public SaveViewModel()
    {
        Initialize();
    }
      
    public bool DialogResult { get; set; }
    public ObservableCollection<DocumentView> Items { get; private set; }
    public ObservableCollection<DocumentView> SelectedItems { get; private set; }      
    public bool IsAllItemsSelected
    {
        get
        {
            return Items.Where(i => ApplicationCommands.Save.CanExecute(null, i)).
            	All(i => SelectedItems.Contains(i));
        }
    }
    
    public void SetItems(
    	ObservableCollection<DocumentView> items, 
    	IEnumerable<DocumentView> selectedItems)
    {
        Items = items;
        RaisePropertyChanged(() => Items);
        SelectedItems.Clear();
        if (selectedItems != null)
            SelectedItems.AddRange(selectedItems);
    }
      
    [OnDeserialized]
    private void Initialize(StreamingContext context = default(StreamingContext))
    {
        Executed();
        CanExecute();
        SelectedItems = new ObservableCollection<DocumentView>();
        SelectedItems.CollectionChanged += (sender, args) => 
        	RaisePropertyChanged(() => IsAllItemsSelected);
    }
      
    private void CanExecute()
    {
        this[ApplicationCommands.Save].CanExecute += (sender, args) => 
            args.CanExecute = SelectedItems.Any(i => 
            	ApplicationCommands.Save.CanExecute(null, i));
    }
      
    private void Executed()
    {
        this[ApplicationCommands.Save].Executed += (sender, args) =>
        {
            var cancelArgs = new CancelEventArgs();
            foreach (var item in SelectedItems.
            	Where(item => ApplicationCommands.Save.CanExecute(null, item)))
            {
                ApplicationCommands.Save.Execute(cancelArgs, item);
                if (cancelArgs.Cancel) break;
            }
              
            if (SelectedItems.Any(i => 
            	ApplicationCommands.Save.CanExecute(null, i)))
            {
                DialogResult = true;
                return;
            }
              
            DialogResult = true;
            OnClosing(sender, new CancelEventArgs());
        };
          
        this[ApplicationCommands.Close].Executed += (sender, args) =>
        {
            DialogResult = args.Parameter == null;
            OnClosing(sender, new CancelEventArgs());
        };
          
        this[ApplicationCommands.SelectAll].Executed += (sender, args) =>
        {
            var isChecked = (bool) args.Parameter;
            foreach (var item in Items)
            {
                if (!SelectedItems.Contains(item) && isChecked) 
                	SelectedItems.Add(item);
                if (SelectedItems.Contains(item) && !isChecked) 
                	SelectedItems.Remove(item);
            }
        };
    }
}


Совсем ничего сложного, однако если изучить сам диалог, то можно обнаружить очень «умное» поведение ListBox'а: все колонки можно переупорядочить, изменить их размер, настроить видимость, выбрать способ сортировки, причём всё это сохраняется даже при перезапуске приложения!
Казалось бы, такая лаконичная вью-модель и такое поведение у представления… А всё это и реализовано с помощью тех самых неявных (индексных)-свойств. Найдите файл в каталоге с программой SaveViewModel.json, в нём вы и обнаружите их около 30…
MVVM не должен содержать бизнес логики. Иначе, вы получаете Smart UI со всеми вытекающими. MVVM предполагает инкапсуляцию логики интерфейса и появился, потому что интерфейсы стали богаче и умнее. Бизнес логика aka domain во ViewModel перетекать не должна. ViewModel — это замена контролера, но более логичная и удобная для desktop-приложений.
Возможно, я не очень ясно выразился… Вью модели содержат у меня всю промежуточную логику между интерфейсом и бизнес-правилами, например, валидацию или логику выполнения команд. Понятно, что какие-то специфические операции для работы с моделями иногда лучше вынести в отдельные классы, например, поиск по записям.

Спасибо, что исправляете!
Я не претендую на авторство :)

В статье я просто рассказал про основные идеи, которые родились при разработке реального проекта с нуля.
Подход гибкий, поэтому если кто-то считает, что реализовано что-то не по феншую, то всегда можно допилить под свои нужды.
Мое мнение как разработчика, достаточно долго пишущего на WPF, что ViewModel ни что иное как абстракция View (на самом деле из названия понтяно)). Суть вьюмодели в том чтобы в абстрактном виде задавать то, как должна выглядить вьюха. Так например в wpf есть классы, которые абстрактно описывают коллекции (например ListCollectionView, ObservableCollection). Мы и пишем вьюмодель ориентруясь на асбтракцию представления. А то КАК будет отображена это коллекция (ListView или ListBox или TreeView) решается на уровне представления. Это я все к тому что, по хорошему, не место значений размеров окна во вью модели, это исключительно ответсвенность вьюхи, вьюмодель — абстракция
Вам решать) Об абстрактных сущностях спорить можно целую вечность… Создайте конкретное приложение со множеством представлений, богатым интерфейсом и сохранением их визуального состояния, а затем просто сравните объёмы и чистоту написанного кода. Если получится лучше, то мы лишь вместе порадуемся этому факту, а пока так :)
Кстати, если не хочется хранить некоторые параметры в самих вью-моделях, то можно создать отдельную SettingsViewModel, например, и писать так

Width={Binding [Width,100], Source={Store Type=vm:SettingsViewModel}}

Тут дело вкуса и конкретного случая.
P.S. Есть подозрение, что у решения есть еще один минус: с объявленными таким образом свойствами Intellisense не справится и их придется набирать по памяти, отслеживая опечатки вручную.
Да, опечатка будет восприниматься как новое неявное свойство :)
Но в этом нет большой проблемы, всё сразу будет заметно на юае, и запоминать особо ничего не нужно.
Статья хорошая, почерпнул пару идей для себя, спасибо. В сторону dynamic вместо индексеров не смотрели?
Как сказали выше, задачу по сохранению состояния UI лучше решать по-другому и не привязывать к ViewModel.

И WTF-factor у кода с таким подходом к MVVM довольно высок (https://www.google.ru/search?q=WTF+code+review), я бы поостерёгся использовать в больших проектах без особой нужды. В большинстве случаев простейший подход с backing field и ручным SendPropertyChanged() выглядит лучше всего — всем понятен, и работает быстро (не в пример Expression Trees и Dictionary для хранения значений).

К тому же в .NET 4.5 появился атрибут [CallerMemberName], который избавляет от написания имени свойства в виде строки.
CallerMemberName сложно сочетается с обфускацией. Его стоит применять с осторожностью.
Строки-константы в коде, а не в кофиг-файлах (или где-то еще) — не лучше. Помню года 4 назад были проблемы с Сodefort в WPF (благо проект писался с нуля, и обфускацию я внедрил с первыми же классами).

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

К тому же, если вдруг, гипотетически, какое-то неявное свойство меняется, ну, с очень большой частотой и сопровождается большими вычислениями, то можно вынести его в обычное явное, и всё тут :)

CallerMemberName тоже вариант, но лябда-выражения всё же более гибкие. Думаю, дело вкуса.
По-поводу dynamic вы можете глянуть на мою статью на CodeProject.com:
General DynamicObject Proxy and Fast Reflection Proxy (на английском)
Там также есть базисные тесты производительности. Насколько я понимаю она довольно близка к вашему проекту.
А вам никода не казалось инжектирование в конструктор несколько неудобным, например, при добавлении или удалении параметра ломались юнит тесты, и их тоже необходимо было править, хотя интерфейс тестируемого объекта, по сути, оставался прежним

Это очень спорная точка зрения. Вы ломаете инкапсуляцию и добавляете новый горизонт для NullReferenceException.

Вы же сами пишите
Но как же нам лучше сохранять эти значения? Попробуем сериализовать вью-модели! Но?.. Это ведь не DTO-объект
, а DataMember — это как раз про DTO.
От инжекций в конструктор отказываться не обязательно, но, на мой взгляд, в этом есть определённые плюсы. Мне всегда казалось избыточным, что в тестах в конструкторах часто проверяются параметры на null и больше ничего интересного не происходит. Если вдруг контейнер вернёт null, то заметишь и исправишь в любом месте кода (для этого, собственно, тесты и нужны), а зачем переносить такие проверки в конструктор не совсем понятно.

Конечно, я знаю, что DataMember это про DTO, но в этом-то и некоторая новизна идеи, что мы будем сериализовать вью-модели…
Хочу ещё немного дополнить всё вышесказанное.

— в качестве представления (View) можно использовать любой контрол, поэтому порой не обязательно выделять представление в отдельную сущность (новый класс), а также добавлять свойство в другую вью-модель. Поясню на примере:

<Window DataContext={Store viewModels:MainViewModel}>

<TextBlock DataContext={Store viewModels:DetailsViewModel} Text={Binding Name}/>



То есть нам не нужно инжектировать DetailsViewModel в MainViewModel, только ради того, чтобы где-то на интерфейсе отобразить свойство Name, также не нужно, например, создавать DetailsShortView. В проекте получается меньше классов, а структура остаётся понятной.

— в статье я показал основные принципы, используя которые можно быстро и качественно сделать функциональное приложение. Совершенно не обязательно использовать всё как есть, вы в праве совершенствовать, видоизменять и фантазировать! В этом и есть развитие, успехов вам! :)
Вышла статья по WinPhone-разработке с примером, где использованы текущие идеи с учётом ограничений мобильной платформы.

WinPhone: пути к совершенству
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации