Да, есть множество готовых решений, но я решил таки потехи ради для тренировки написать библиотеку для паттерна MVVM.
Изначально это все задумывалось ради коллекции, которую удобно было бы использовать в View Model. С нее и начну.
Коллекция должна транслировать нам коллекцию из модели, при этом не кастить ее целиком в новый тип, а делать это по требованию. Конечно же, она должна проверять, является ли коллекция в модели оповещающей об изменениях и хавать эти оповещения. И вот когда я это описал в своей голове, появилась необходимость описать классы для View Model и Model. Почему? Да вот почему.
Как видите, я реализовал тут парочку интерфейсов для работы с наборами однотипных данных, а так же для оповещений.
Для реализации моей задумки мне потребовались дополнительные базовые классы: ModelBase и ViewModelBase.
ViewModelBase должен иметь пустой конструктор (только для того, чтобы пройти таможню в определении VmList where TViewModel: ViewModelBase, new()). А вообще View Model должна создаваться на основе модели, поэтому я добавил в ВМ инициализатор, который срабатывает при указании модели.
Тут, наверное, сразу бросится в глаза метод Initialize(). Из названия понятно, для чего он. Но что именно он делает? Да все просто. Он ищет свойства, помеченные атрибутом, указывающим на зависимость от свойства модели и добавляет все эти свойства в Lookup<string, string>, где первый параметр — название свойства в модели, а второй — в ViewModel.
Как видите, ViewModelBase я наследую от MvvmBase. Этот базовый класс просто добавляет возможность оповещать об изменении свойств
А класс ModelBase вообще ничего не умеет. Он нужен только для отделения ViewModel от Model. В дальнейшем, думаю, дописать к нему атрибут Serializable или наследовать от IClonable
Использовать все это лучше всего в тех случаях, когда к одной и той же модели одновременно цепляются несколько ViewModel. Причем изменения на форме сквозь ViewModel сразу влияют на модель.
Вот пример свойства в ViewModel
Биндим это свойство к TextBox'у и пробуем его менять. В этом случае сеттер в ViewModel изменит соответствующее свойство в модели, сеттер в модели вызовет событие PropertyChanged для этого свойства. ViewModel на него подписан, поэтому при возникновении этого события возьмет имя свойства модели и посмотрит в своем Lookup, вызвав PropertyChanged для своего свойства.
Я с удовольствием выслушаю ваше мнение, объясню непонятные моменты и внесу изменения, если это потребуется.
Изначально это все задумывалось ради коллекции, которую удобно было бы использовать в 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 для своего свойства.
Я с удовольствием выслушаю ваше мнение, объясню непонятные моменты и внесу изменения, если это потребуется.