Pull to refresh

Небольшой велосипед для WPF приложений (MVVM)

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

Изначально это все задумывалось ради коллекции, которую удобно было бы использовать в View Model. С нее и начну.

Коллекция должна транслировать нам коллекцию из модели, при этом не кастить ее целиком в новый тип, а делать это по требованию. Конечно же, она должна проверять, является ли коллекция в модели оповещающей об изменениях и хавать эти оповещения. И вот когда я это описал в своей голове, появилась необходимость описать классы для View Model и Model. Почему? Да вот почему.

VmList.cs
    public class VmList<TModel, TViewModel> : IList<TViewModel>, IEnumerator<TViewModel>, INotifyCollectionChanged, INotifyPropertyChanged where TModel : ModelBase where TViewModel : ViewModelBase, new()
    {
        IList<TModel> _list;
        IEnumerator<TModel> _enumerator;
        bool _modelListIsCollectionNotifier;

        public event NotifyCollectionChangedEventHandler CollectionChanged;
        public event PropertyChangedEventHandler PropertyChanged;

        public VmList(IList<TModel> list)
        {
            _list = list;

            {
                var notifier = _list as INotifyCollectionChanged;
                
                if (notifier != null)
                {
                    _modelListIsCollectionNotifier = true;
                    notifier.CollectionChanged += Notifier_CollectionChanged;
                }
            }

            {
                var notifier = _list as INotifyPropertyChanged;

                if (notifier != null)
                    notifier.PropertyChanged += Notifier_PropertyChanged;
            }
            

            _enumerator = _list.GetEnumerator();
        }

        private void Notifier_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            PropertyChanged?.Invoke(this, e);
        }

        void Notifier_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            CollectionChanged?.Invoke(this, e);
        }

        public TViewModel this[int index]
        {
            get
            {
                return new TViewModel { Model = _list[index] };
            }

            set
            {
                _list[index] = (TModel)value.Model;
                if(!_modelListIsCollectionNotifier)
                    CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, index));
            }
        }

        public int Count
        {
            get
            {
                return _list.Count;
            }
        }

        public TViewModel Current
        {
            get
            {
                return new TViewModel { Model = _enumerator.Current };
            }
        }

        public bool IsReadOnly
        {
            get
            {
                return _list.IsReadOnly;
            }
        }

        object IEnumerator.Current
        {
            get
            {
                return Current;
            }
        }

        public void Add(TViewModel item)
        {
            _list.Add((TModel)item.Model);
            if (!_modelListIsCollectionNotifier)
                CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
        }

        public void Clear()
        {
            _list.Clear();
            if (!_modelListIsCollectionNotifier)
                CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }

        public bool Contains(TViewModel item)
        {
            return _list.Contains((TModel)item.Model);
        }

        public void CopyTo(TViewModel[] array, int arrayIndex)
        {
            throw new NotImplementedException();
        }

        public void Dispose()
        {
            _enumerator.Dispose();
        }

        public IEnumerator<TViewModel> GetEnumerator()
        {
            return this;
        }

        public int IndexOf(TViewModel item)
        {
            return _list.IndexOf((TModel)item.Model);
        }

        public void Insert(int index, TViewModel item)
        {
            _list.Insert(index, (TModel)item.Model);
            if (!_modelListIsCollectionNotifier)
                CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
        }

        public bool MoveNext()
        {
            return _enumerator.MoveNext();
        }

        public bool Remove(TViewModel item)
        {
            var res = _list.Remove((TModel)item.Model);
            if (res && !_modelListIsCollectionNotifier)
                CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item));
            return res;
        }

        public void RemoveAt(int index)
        {
            _list.RemoveAt(index);
            if (!_modelListIsCollectionNotifier)
                CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, index));
        }

        public void Reset()
        {
            _enumerator.Reset();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _list.GetEnumerator();
        }
    }



Как видите, я реализовал тут парочку интерфейсов для работы с наборами однотипных данных, а так же для оповещений.
Для реализации моей задумки мне потребовались дополнительные базовые классы: ModelBase и ViewModelBase.
ViewModelBase должен иметь пустой конструктор (только для того, чтобы пройти таможню в определении VmList where TViewModel: ViewModelBase, new()). А вообще View Model должна создаваться на основе модели, поэтому я добавил в ВМ инициализатор, который срабатывает при указании модели.

ViewModelBase общего назначения
    public class ViewModelBase: MvvmBase
    {
        bool _initialized;
        ModelBase _model;
        Lookup<string, string> _relatives;

        public ViewModelBase() { }

        public ViewModelBase(ModelBase model)
        {
            Model = model;
        }

        void Initialize()
        {
            DependentAttribute attr;
            var tmpRel = new List<KeyValuePair<string, string>>();
            foreach (PropertyDescriptor pd in TypeDescriptor.GetProperties(this))
            {
                attr = (DependentAttribute)pd.Attributes[typeof(DependentAttribute)];
                if (attr != null)
                    tmpRel.Add(new KeyValuePair<string, string>(attr.PropertyName, pd.Name));
                else
                {
                    var modelProp = TypeDescriptor.GetProperties(Model)[pd.Name];
                    if (modelProp != null)
                        tmpRel.Add(new KeyValuePair<string, string>(modelProp.Name, pd.Name));
                }
            }
            _relatives = (Lookup<string, string>)tmpRel.ToLookup(kv => kv.Key, kv => kv.Value);
            
            Model.PropertyChanged += Model_PropertyChanged;
            Initialized = Model != null;
        }

        protected virtual void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            foreach(var propName in _relatives[e.PropertyName])
                OnPropertyChanged(propName);
        }

        internal protected ModelBase Model
        {
            get
            {
                return _model;
            }
            set
            {
                _model = value;
                Initialize();
            }
        }

        public bool Initialized
        {
            get
            {
                return _initialized;
            }

            set
            {
                _initialized = value;
            }
        }
    }



ViewModelBase<TModel> и генерик
    public class ViewModelBase<TModel> : ViewModelBase where TModel: ModelBase
    {
        public ViewModelBase() { }
        public ViewModelBase(TModel model) : base(model) { }

        protected internal new TModel Model
        {
            get
            {
                return (TModel)base.Model;
            }
            set
            {
                base.Model = value;
            }
        }
    }



Тут, наверное, сразу бросится в глаза метод Initialize(). Из названия понятно, для чего он. Но что именно он делает? Да все просто. Он ищет свойства, помеченные атрибутом, указывающим на зависимость от свойства модели и добавляет все эти свойства в Lookup<string, string>, где первый параметр — название свойства в модели, а второй — в ViewModel.

Вот определение этого атрибута DependentAttribute.cs
    [AttributeUsage(AttributeTargets.Property,AllowMultiple = true, Inherited = true)]
    public sealed class DependentAttribute: Attribute
    {
        public string PropertyName { get; set; }

        public DependentAttribute(string propertyName)
        {
            PropertyName = propertyName;
        }
    }



Как видите, ViewModelBase я наследую от MvvmBase. Этот базовый класс просто добавляет возможность оповещать об изменении свойств

MvvmBase.cs
    public class MvvmBase: INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string name)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }



А класс ModelBase вообще ничего не умеет. Он нужен только для отделения ViewModel от Model. В дальнейшем, думаю, дописать к нему атрибут Serializable или наследовать от IClonable

    public class ModelBase: MvvmBase { }


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

Вот пример свойства в ViewModel
    public string Property
    {
        get
        {
            return Model.Property;
        }
        set
        {
            Model.Property = value;
        }
    }

Биндим это свойство к TextBox'у и пробуем его менять. В этом случае сеттер в ViewModel изменит соответствующее свойство в модели, сеттер в модели вызовет событие PropertyChanged для этого свойства. ViewModel на него подписан, поэтому при возникновении этого события возьмет имя свойства модели и посмотрит в своем Lookup, вызвав PropertyChanged для своего свойства.

Я с удовольствием выслушаю ваше мнение, объясню непонятные моменты и внесу изменения, если это потребуется.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.