MVVM: новый взгляд

Внимание!
Более свежие и прогрессивные материалы по MVVM паттерну представлены в статье Context Model Pattern via Aero Framework и подробно разобраны в следующем цикле статей

Предисловие

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

К делу

Разработчикам WPF, Silverlight и WinPhone-приложений хорошо знаком паттерн проектирования MVVM (Model — View — ViewModel). Однако если дополнительно применить к нему ещё немного фантазии, то может получиться что-то более интересное, и немного даже, осмелюсь заверить, революционное.

Допустим, у нас есть классическое окно (View) текстового редактора с меню, тулбар треем и статус баром, которые можно спрятать при желании. Перед нами стоит задача – сохранить позицию и размеры окна, а также визуальное состояние элементов при закрытии приложения, чтобы потом восстановить их.

Обычное решение, которое сразу напрашивается на ум, состоит в добавлении во вью-модель ряда дополнительных свойств для привязки (Top, Left, Width, Heigth, ShowToolBarTray, ShowStatusBar и других), а затем сохранение их значений, например, в файл. Но не будем спешить… Что если я вам скажу, что можно создать такую вью-модель, которая будет реализовывать необходимую функциональность по умолчанию, поэтому для решения задачи не нужно НИ ОДНОЙ дополнительной строки кода?

Сразу рекомендую скачать пример приложения, который я сделал специально для этой статьи (ссылка один или два), он поможет понять основные идеи и прочувствовать красоту подхода. Здесь же я приведу определённые части кода, на которые стоит обратить особое внимание.

В WPF часто используется привязка к свойствам, но существует также возможность привязки к элементам массива, которой пользуются довольно редко. Но вот она-то и открывает нам новые горизонты. Попробуем рассмотреть вью-модель, как словарь, где ключом-индексом будет имя свойства, по которому можно получить его значение.

Но как же нам лучше сохранять эти значения? Попробуем сериализовать вью-модели! Но?.. Это ведь не DTO-объект, да и как потом их десериализовать, ведь в конструктор часто нужно инжектировать другие параметры, а для десериализации обычно нужен конструктор без параметров? А вам никода не казалось инжектирование в конструктор несколько неудобным, например, при добавлении или удалении параметра ломались юнит тесты, и их тоже необходимо было править, хотя интерфейс тестируемого объекта, по сути, оставался прежним?

Поэтому откажемся от инжекций в конструктор, благо, существуют и другие способы для подобных целей, и пометим вью-модели атрибутом [DataContract], а свойства, которые нужно сериализовать, атрибутом [DataMember] (эти аттрибуты очень упрощают сериализацию).

Теперь создадим небольшой класс Store.

    public static class Store
    {
        private static readonly Dictionary<Type, object> StoredItemsDictionary = new Dictionary<Type, object>();

        public static TItem OfType<TItem>(params object[] args) where TItem : class
        {
            var itemType = typeof (TItem);
            if (StoredItemsDictionary.ContainsKey(itemType))
                return (TItem) StoredItemsDictionary[itemType];

            var hasDataContract = Attribute.IsDefined(itemType, typeof (DataContractAttribute));
            var item = hasDataContract
                ? Serializer.DeserializeDataContract<TItem>() ?? (TItem) Activator.CreateInstance(itemType, args)
                : (TItem) Activator.CreateInstance(itemType, args);

            StoredItemsDictionary.Add(itemType, item);
            return (TItem) StoredItemsDictionary[itemType];
        }

        public static void Snapshot()
        {
            StoredItemsDictionary
                .Where(p => Attribute.IsDefined(p.Key, typeof (DataContractAttribute)))
                .Select(p => p.Value).ToList()
                .ForEach(i => i.SerializeDataContract());
        }
    }


Тут всё просто – лишь два метода. OfType возвращающает нам статический экземпляр объекта требуемого типа, по возможности десериализуя его, и Snapshot делает «снимок» объектов находящихся в контейнере, сериализуя их. Вызов Snapshot в общем случае можно осуществить лишь один раз при закрытии приложения, например, в обработчике Exit класса Application.

И напишем Json-сериализатор.

    public static class Serializer
    {
        public const string JsonExtension = ".json";

        public static readonly List<Type> KnownTypes = new List<Type>
        {
            typeof (Type),
            typeof (Dictionary<string, string>),
            typeof (SolidColorBrush),
            typeof (MatrixTransform),
        };

        public static void SerializeDataContract(this object item, string file = null, Type type = null)
        {
            try
            {
                type = type ?? item.GetType();
                if (string.IsNullOrEmpty(file))
                    file = type.Name + JsonExtension;
                var serializer = new DataContractJsonSerializer(type, KnownTypes);
                using (var stream = File.Create(file))
                {
                    var currentCulture = Thread.CurrentThread.CurrentCulture;
                    Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
                    serializer.WriteObject(stream, item);
                    Thread.CurrentThread.CurrentCulture = currentCulture;
                }
            }
            catch (Exception exception)
            {
                Trace.WriteLine("Can not serialize json data contract");
                Trace.WriteLine(exception.StackTrace);
            }
        }

        public static TItem DeserializeDataContract<TItem>(string file = null)
        {
            try
            {
                if (string.IsNullOrEmpty(file)) 
                    file = typeof (TItem).Name + JsonExtension;
                var serializer = new DataContractJsonSerializer(typeof (TItem), KnownTypes);
                using (var stream = File.OpenRead(file))
                {
                    var currentCulture = Thread.CurrentThread.CurrentCulture;
                    Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
                    var item = (TItem) serializer.ReadObject(stream);
                    Thread.CurrentThread.CurrentCulture = currentCulture;
                    return item;
                }
            }
            catch
            {
                return default(TItem);
            }
        }
    }


Базовый класс для вью моделей выглядит тоже не сложно.

    [DataContract]
    public class ViewModelBase : PropertyNameProvider, INotifyPropertyChanging, INotifyPropertyChanged
    {
        protected Dictionary<string, object> Values = new Dictionary<string, object>();
        private const string IndexerName = System.Windows.Data.Binding.IndexerName; /* "Item[]" */
        public event PropertyChangingEventHandler PropertyChanging = (sender, args) => { };
        public event PropertyChangedEventHandler PropertyChanged = (sender, args) => { };

        public object this[string key]
        {
            get { return Values.ContainsKey(key) ? Values[key] : null; }
            set
            {
                RaisePropertyChanging(IndexerName);
                if (Values.ContainsKey(key)) Values[key] = value;
                else Values.Add(key, value);
                RaisePropertyChanged(IndexerName);
            }
        }

        public object this[string key, object defaultValue]
        {
            get
            {
                if (Values.ContainsKey(key)) return Values[key];
                Values.Add(key, defaultValue);
                return defaultValue;
            }
            set { this[key] = value; }
        }

        public void RaisePropertyChanging(string propertyName)
        {
            PropertyChanging(this, new PropertyChangingEventArgs(propertyName));
        }

        public void RaisePropertyChanged(string propertyName)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        [OnDeserializing]
        private void Initialize(StreamingContext context = default(StreamingContext))
        {
            if (PropertyChanging == null) PropertyChanging = (sender, args) => { };
            if (PropertyChanged == null) PropertyChanged = (sender, args) => { };
            if (Values == null) Values = new Dictionary<string, object>();
        }
    }


Также унаследуемся от небольшого класса PropertyNameProvider, который пригодится нам в дальнейшем для работы с лямбда-выражениями.

    [DataContract]
    public class PropertyNameProvider
    {
        public static string GetPropertyName<T>(Expression<Func<T>> expression)
        {
            var memberExpression = expression.Body as MemberExpression;
            var unaryExpression = expression.Body as UnaryExpression;

            if (unaryExpression != null)
                memberExpression = unaryExpression.Operand as MemberExpression;

            if (memberExpression == null || memberExpression.Member.MemberType != MemberTypes.Property)
                throw new Exception("Invalid lambda expression format.");

            return memberExpression.Member.Name;
        }
    }


Отлично, на данном этапе мы реализовали возможность привязки к свойствам-индексам. В xaml можно писать выражения следующего вида

Height="{Binding '[Height, 600]', Mode=TwoWay}"

где первый параметр — это имя свойства, а второй (опциональный) — его дефолтное значение.

Этот подход чем-то напоминает реализацию стандартного интерфейса IDataErrorInfo. Почему бы нам тоже не реализовать его? Хорошая идея, но не станем спешить, а примем её во внимание… Поиграем ещё с переопределением индексатора. Все помнят про ICommand, а в WPF существует ещё крутой механизм работы RoutedCommands и CommandBindings. Вот было бы классно писать реализацию команд во вью-модели подобным образом.

            this[ApplicationCommands.Save].CanExecute += (sender, args) => args.CanExecute = HasChanged;
            this[ApplicationCommands.New].CanExecute += (sender, args) =>
            {
                args.CanExecute = !string.IsNullOrEmpty(FileName) || !string.IsNullOrEmpty(Text);
            };

            this[ApplicationCommands.Help].Executed += (sender, args) => MessageBox.Show("Muse 2014");
            this[ApplicationCommands.Open].Executed += (sender, args) => Open();
            this[ApplicationCommands.Save].Executed += (sender, args) => Save();
            this[ApplicationCommands.SaveAs].Executed += (sender, args) => SaveAs();
            this[ApplicationCommands.Close].Executed += (sender, args) => Environment.Exit(0);
            this[ApplicationCommands.New].Executed += (sender, args) =>
            {
                Text = string.Empty;
                FileName = null;
                HasChanged = false;
            };


Ну, какая же вью-модель без автоматической нотификации свойств и лябда-выражений? Это должно быть по-любому.

        public string Text
        {
            get { return Get(() => Text); }
            set { Set(() => Text, value); }
        }


А что если… Создать PropertyBinding наподобие CommandBinding и совсем чуть-чуть снова поиграть с индексатором?

       this[() => Text].PropertyChanged += (sender, args) => HasChanged = true;
       this[() => FontSize].Validation += () => 4.0 < FontSize && FontSize < 128.0 ? null : "Invalid font size";


Выглядит неплохо, неправда ли?

И, конечно, наша чудо-вью-модель.

    [DataContract]
    public class ViewModel : ViewModelBase, IDataErrorInfo
    {
        public ViewModel()
        {
            Initialize();
        }

        string IDataErrorInfo.this[string propertyName]
        {
            get
            {
                return PropertyBindings.ContainsKey(propertyName)
                    ? PropertyBindings[propertyName].InvokeValidation()
                    : null;
            }
        }

        public PropertyBinding this[Expression<Func<object>> expression]
        {
            get
            {
                var propertyName = GetPropertyName(expression);
                if (!PropertyBindings.ContainsKey(propertyName))
                    PropertyBindings.Add(propertyName, new PropertyBinding(propertyName));
                return PropertyBindings[propertyName];
            }
        }

        public CommandBinding this[ICommand command]
        {
            get
            {
                if (!CommandBindings.ContainsKey(command))
                    CommandBindings.Add(command, new CommandBinding(command));
                return CommandBindings[command];
            }
        }

        public string Error { get; protected set; }
        public Dictionary<ICommand, CommandBinding> CommandBindings { get; private set; }
        public Dictionary<string, PropertyBinding> PropertyBindings { get; private set; }
        public CancelEventHandler OnClosing = (o, e) => { };

        public TProperty Get<TProperty>(Expression<Func<TProperty>> expression, TProperty defaultValue = default(TProperty))
        {
            var propertyName = GetPropertyName(expression);
            if (!Values.ContainsKey(propertyName))
                Values.Add(propertyName, defaultValue);
            return (TProperty) Values[propertyName];
        }

        public void Set<TProperty>(Expression<Func<TProperty>> expression, TProperty value)
        {
            var propertyName = GetPropertyName(expression);
            RaisePropertyChanging(propertyName);
            if (!Values.ContainsKey(propertyName))
                Values.Add(propertyName, value);
            else Values[propertyName] = value;
            RaisePropertyChanged(propertyName);
        }

        public void RaisePropertyChanging<TProperty>(Expression<Func<TProperty>> expression)
        {
            var propertyName = GetPropertyName(expression);
            RaisePropertyChanging(propertyName);
        }

        public void RaisePropertyChanged<TProperty>(Expression<Func<TProperty>> expression)
        {
            var propertyName = GetPropertyName(expression);
            RaisePropertyChanged(propertyName);
        }

        [OnDeserializing]
        private void Initialize(StreamingContext context = default(StreamingContext))
        {
            CommandBindings = new Dictionary<ICommand, CommandBinding>();
            PropertyBindings = new Dictionary<string, PropertyBinding>();
            PropertyChanging += OnPropertyChanging;
            PropertyChanged += OnPropertyChanged;
        }

        private void OnPropertyChanging(object sender, PropertyChangingEventArgs e)
        {
            var propertyName = e.PropertyName;
            if (!PropertyBindings.ContainsKey(propertyName)) return;
            var binding = PropertyBindings[propertyName];
            if (binding != null) binding.InvokePropertyChanging(sender, e);
        }

        private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            var propertyName = e.PropertyName;
            if (!PropertyBindings.ContainsKey(propertyName)) return;
            var binding = PropertyBindings[propertyName];
            if (binding != null) binding.InvokePropertyChanged(sender, e);
        }
    }


Теперь мы вооружены по полной, но нет предела совершенству. Как правило, вью-модель связывается со своим представлением (вью) в C# коде, но насколько бы было красиво эту привязку осуществлять непосредственно в xaml! Помните про наш отказ от инжекций в конструктор? Вот он нам и даёт такую возможность. Напишем небольшое расширение для разметки*.

    public class StoreExtension : MarkupExtension
    {
        public StoreExtension(Type itemType)
        {
            ItemType = itemType;
        }

        [ConstructorArgument("ItemType")]
        public Type ItemType { get; set; }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            var service = (IProvideValueTarget) serviceProvider.GetService(typeof (IProvideValueTarget));
            var frameworkElement = service.TargetObject as FrameworkElement;
            var dependancyProperty = service.TargetProperty as DependencyProperty;
            var methodInfo = typeof(Store).GetMethod("OfType").MakeGenericMethod(ItemType);
            var item = methodInfo.Invoke(null, new object[] { new object[0] });
            if (frameworkElement != null &&
                dependancyProperty == FrameworkElement.DataContextProperty &&
                item is ViewModel)
            {
                var viewModel = (ViewModel) item;
                frameworkElement.CommandBindings.AddRange(viewModel.CommandBindings.Values);
                var window = frameworkElement as Window;
                if (window != null)
                    viewModel.OnClosing += (o, e) => { if (!e.Cancel) window.Close(); };
                frameworkElement.Initialized += (sender, args) => frameworkElement.DataContext = viewModel;
                return null;
            }

            return item;
        }
    }


Вуаля, готово!

DataContext="{Store viewModels:MainViewModel}"

Обращаю внимание на то, что во время привязки у контрола изменяется не только DataContext, но и заполняется коллекция CommandBindings, значениями из вью-модели.

(* чтобы перед расширениями для разметки не писать префиксов вроде "{foundation:Store viewModels:MainViewModel}", они должны быть реализованы в отдельном проекте и в этом же проекте в файде AssemblyInfo.cs нужно написать что-то вроде
[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "Foundation")]
[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "Foundation.Converters")]
[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "Foundation.MarkupExtensions")]
)

Подобным образом приукрасим привязку к индексам, о которой речь шла выше.

    public class ViewModelExtension : MarkupExtension
    {
        private static readonly BooleanConverter BooleanToVisibilityConverter = new BooleanConverter
        {
            OnTrue = Visibility.Visible,
            OnFalse = Visibility.Collapsed,
        };

        private FrameworkElement _targetObject;
        private DependencyProperty _targetProperty;

        public ViewModelExtension()
        {
        }

        public ViewModelExtension(string key)
        {
            Key = key;
        }

        public ViewModelExtension(string key, object defaultValue)
        {
            Key = key;
            DefaultValue = defaultValue;
        }

        public string Key { get; set; }
        public string StringFormat { get; set; }
        public string ElementName { get; set; }
        public object DefaultValue { get; set; }
        public object FallbackValue { get; set; }
        public object TargetNullValue { get; set; }
        public IValueConverter Converter { get; set; }
        public RelativeSource RelativeSource { get; set; }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            var service = (IProvideValueTarget) serviceProvider.GetService(typeof (IProvideValueTarget));
            _targetProperty = service.TargetProperty as DependencyProperty;
            _targetObject = service.TargetObject as FrameworkElement;
            if (_targetObject == null || _targetProperty == null) return this;

            var key = Key;
            if (_targetProperty == UIElement.VisibilityProperty && string.IsNullOrWhiteSpace(key))
                key = string.Format("Show{0}",
                                    string.IsNullOrWhiteSpace(_targetObject.Name)
                                        ? _targetObject.Tag
                                        : _targetObject.Name);

            key = string.IsNullOrWhiteSpace(key) ? _targetProperty.Name : key;
            if (!string.IsNullOrWhiteSpace(StringFormat)) Key = string.Format(StringFormat, _targetObject.Tag);

            var index = DefaultValue == null ? key : key + "," + DefaultValue;
            var path = string.IsNullOrWhiteSpace(ElementName) && RelativeSource == null
                           ? "[" + index + "]"
                           : "DataContext[" + index + "]";

            if (_targetProperty == UIElement.VisibilityProperty && Converter == null)
                Converter = BooleanToVisibilityConverter;

            var binding = new Binding(path) {Mode = BindingMode.TwoWay, Converter = Converter};
            if (ElementName != null) binding.ElementName = ElementName;
            if (FallbackValue != null) binding.FallbackValue = FallbackValue;
            if (TargetNullValue != null) binding.TargetNullValue = TargetNullValue; 
            if (RelativeSource != null) binding.RelativeSource = RelativeSource;

            _targetObject.SetBinding(_targetProperty, binding);
            return binding.ProvideValue(serviceProvider);
        }
    }


В xaml можно писать так:

Width="{ViewModel DefaultValue=800}"

Итоги

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

Резюмируя всё сказанное, можно выделить следующие плюсы подхода:
— чистый, лаконичный и структурированный код. Интерфейсная логика, слабо связанная с бизнес-логикой, инкапсулируется внутри базовых классов вью-модели, в то время как конкретная реализация вью-модели содержит именно ту логику, которая тесно связана с бизнес-правилами;
— простота и универсальность решения. Ко всему прочему, сериализация позволяет очень гибко настраивать интерфейс приложения с помощью конфигурационных файлов;
— удобная реализация валидации через интерфейс IDataErrorInfo.

Минусы:
— отказ от инжекций в конструктор (хотя это и не обязательное требование);
— некоторая неявность решения для человека, не знакомого с ним.

Освоив данный подход и имея в распоряжении всего несколько базовых классов, вы сможете комфортно, быстро и качественно писать приложения с богатым интерактивным интерфейсом, при этом оставляя вью-модели чистыми и компактными.

Очень надеюсь, что статья окажется для вас полезной! Спасибо за внимание!

P.S. Не знаю точно, как в Silverlight, но на WinPhone-платформе есть некоторые ограничения (отсутствуют расширения разметки, RoutedCommands и CommandBindings), однако при большом желании их можно обойти. Более детально это описано в статье WinPhone: пути к совершенству.

P.P.S. Как я уже сказал выше, все описанные методы, применены мной при создании полноценного текстового редактора. Те, кому интересно, что же в итоге получилось за творение, могут найти его по этой ссылке или резервной. Мне кажется, что в программировании и поэзии очень много общего: также как мастер слова способен несколькими фразами выразить то, на что у обычного человека уйдет не один абзац, так и опытный программист решает сложную задачу несколькими строками кода.

Вдохновения вам!

~~~~~~~~~~~~~~~~~~~~~~~~~
Комментарии:

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

<Window DataContext={Store viewModels:MainViewModel}>
…
<!--<TextBlock DataContext={Store viewModels:DetailsViewModel} Text={Binding Name}/>--!>
<TextBlock Text={Binding Name, Source={Store viewModels:DetailsViewModel}}/>
…
</Window>


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                    SMVVM — Spaghetti MVVM.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                                                                    • +1
                                                                      Паттерн элементарный
                                                                      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 это бессмысленно.

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

                                                                        Насколько я понял, пользователю 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…
                      • +2
                        MVVM не должен содержать бизнес логики. Иначе, вы получаете Smart UI со всеми вытекающими. MVVM предполагает инкапсуляцию логики интерфейса и появился, потому что интерфейсы стали богаче и умнее. Бизнес логика aka domain во ViewModel перетекать не должна. ViewModel — это замена контролера, но более логичная и удобная для desktop-приложений.
                        • +2
                          Возможно, я не очень ясно выразился… Вью модели содержат у меня всю промежуточную логику между интерфейсом и бизнес-правилами, например, валидацию или логику выполнения команд. Понятно, что какие-то специфические операции для работы с моделями иногда лучше вынести в отдельные классы, например, поиск по записям.

                          Спасибо, что исправляете!
                        • 0
                          Такой паттерн сто лет как существует .
                          • 0
                            Я не претендую на авторство :)

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

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

                        Тут дело вкуса и конкретного случая.
                      • +1
                        P.S. Есть подозрение, что у решения есть еще один минус: с объявленными таким образом свойствами Intellisense не справится и их придется набирать по памяти, отслеживая опечатки вручную.
                        • 0
                          Да, опечатка будет восприниматься как новое неявное свойство :)
                          Но в этом нет большой проблемы, всё сразу будет заметно на юае, и запоминать особо ничего не нужно.
                        • +2
                          Статья хорошая, почерпнул пару идей для себя, спасибо. В сторону 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], который избавляет от написания имени свойства в виде строки.
                          • +1
                            CallerMemberName сложно сочетается с обфускацией. Его стоит применять с осторожностью.
                            • +1
                              Строки-константы в коде, а не в кофиг-файлах (или где-то еще) — не лучше. Помню года 4 назад были проблемы с Сodefort в WPF (благо проект писался с нуля, и обфускацию я внедрил с первыми же классами).

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

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

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

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

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

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

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

                                <Window DataContext={Store viewModels:MainViewModel}>

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



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

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

                                  WinPhone: пути к совершенству

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.