Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

MVVM: новейший взор

Anna | 17.06.2014 | нет комментариев
Вступление

Некоторое время назад я затеял разработку бесплатного текстового редактора с прекрасным интерфейсом и широким комфортным функционалом на платформе 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}”

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

Данный подход чем-то напоминает реализацию стандартного интерфейса 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}”

Результаты

Вероятно, довольно, я преподнёс много информации в сжатом виде, следственно для полноты понимания отменнее ознакомиться с примером плана.

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

 

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB