There are M, V, VM. The model has a number of properties. VM broadcasts them directly.

public string Title { get { return _model.Title; } set { _model.Title = value; } } 

Something like this. And the following is written in the VM constructor

 _model.Titlechanged += model_Titlechanged; 

That is, if the user changes something on the form, then the property of the model changes in the VM setter, an event is triggered in the model setter, indicating that the property has changed. The VM picks up this event and in the handler calls

 OnPropertyChanged(nameof(Title)); 

Now the question is:

How to make similar for properties with collections if collections of model contain elements of type MyClassModel , and collections of VM - MyClassViewModel ?

For me, the difficulty is this. I use ObservableCollection as collections in the model and VM. But since the types of elements in them are different, I just cannot translate like that. That is, in the VM, I need to change the type of the contents of the collection, and therefore follow it directly in the case of CollectionChanged to see what changed where and repeat this action in the model collection (which in itself is no longer convenient).

Well, then the following happens. The model collection accepts changes (that is, changes itself), and, accordingly, signals that it has changed. And on her changes signed by VM. It turns out a vicious circle. It is possible, of course, to compare collections element by element in order to reveal their equivalence, but it seems to me too much. Explain what I'm doing wrong!

  • And how do you send changes to the model from the VM? - VladD
  • @VladD, well, at the beginning of the post there is an example of a property in VM - iRumba
  • I do not see the transfer to the model there , only the signature on the model property. - VladD
  • @VladD, ummm ... _model.Title = value; not? - iRumba
  • Okay. And what about the collection? - VladD

2 answers 2

That's what I got in the end.

 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(); } } 

Sorry for the lack of comments in the code. Something like this happens. As you can see, this is a universal class dependent on two basic types: ModelBase and ViewModelBase . The entire collection is not duplicated from the model. When I request an element from VmList'a , a class takes the corresponding element from the collection of the ModelBase type and makes a new element of the ViewModelBase type from it.

Here is the ModelBase listing. Do not pay attention that it is empty, its task is not in logic, but in presence. That is, it is necessary to clearly distinguish between the model and the twist model. In the future, I will supply it with serialization and anything else, but this is not required to solve the current problem.

 [DataContract] public class ModelBase: MvvmBase { } 

And here is a listing of 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; } } } 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; } } } 

As you can see there are 2 classes. General and typed. There is also a type of DependentAttribute .

 [AttributeUsage(AttributeTargets.Property,AllowMultiple = true, Inherited = true)] public sealed class DependentAttribute: Attribute { public string PropertyName { get; set; } public DependentAttribute(string propertyName) { PropertyName = propertyName; } } 

Something like this happens. When a PropertyChanged event is triggered by a model, I twist the model to see if it has the same property, and if so, it triggers the PropertyChanged event itself. So, if in the model you have a property called CircRad , and when you twist the model you want to name it Radius , then you just hang the attribute [Dependent("CircRad")] on the Radius property and when you change the CircRad property in the model, the VM will report change the Radius property. that is, ideally, the property in the VM will look like this

 public string SomeProperty { get; set; } 

Instead

 public string SomeProperty { get { return _model.SomeProperty; } set { _model.SomeProperty = value; OnPropertyChanged(nameof(SomeProperty)); } } 

Of course, if you need to provide interaction logic, then the property will have to be disclosed. Although I simply subscribe to my own PropertyChanged inside the VM and depending on the name of the property that has changed, I perform certain actions. I do not know how it is right, but it's easier for me.

  • It's not entirely clear why the list is implemented by INotifyPropertyChanged . _list most likely will not implement INotifyPropertyChanged itself. And the rest, yes, it seems to be the correct implementation. - VladD
  • @VladD, in fact, various collections have properties that are not indicated in the interfaces (the number of elements, the maximum number, freezing, etc.). I did not study all available collections (especially collections can be user-defined) for the implementation of alerts, but simply provided for it immediately. By the way, not provided for the end. In theory, you still need to inherit from the TypeDescriptor class, which would catch the call to "non-existent" properties from XAML and allow them to be processed - iRumba
  • It seems to me that you do not need to provide all interfaces. INotifyCollectionChanged enough. Even INotifyPropertyChanged seems to me superfluous (if it is not for WinForms). - VladD
  • @VladD, most likely you are right, in 99.9% of cases it will not be needed, in other cases the problem can be solved differently. But I still leave it. - iRumba
  1. Why is it so difficult to bind VM and M and declare wrapper properties? Isn't it easier in VM to declare the Model property, and M to do with INPC-properties (so that she herself throws PropertyChanged)? In XAML, respectively, there will be {Binding Model.Title} binding.

  2. If you do the same with the collection properties as I wrote above, then there will be only one collection - in M. I didn’t quite understand if I still had to follow the changes in the properties of the elements in this collection. If so, you will have to add code to keep track of existing and added / removed items in the ObservableCollection.

In general, it is better to use any of the known MVVM frameworks that will simplify all these things. I personally use ReactiveUI, but this is not the easiest option for newbies.

  • one
    The difficulty is that the model may have another thread affinity: it can run in the background thread (and usually does). Therefore, in order to access it, you need at least synchronization. - VladD
  • There are other reasons not to do so. In general, I have already described the VmList class and, if necessary, I will post it later for everyone to see. In short, this class does not create a new collection based on the collection from the model, but translates the elements of the collection of the model, create only the necessary element with the view of the model from the requested collection element (and, of course, follows the changes if the model reports them and sends them from your name). It goes without saying that the base class of the model view can be created on the basis of the model and VmList knows about it) - iRumba
  • ok, I forgot about synchronization) for cases with collections I use the extension from ReactiveUI: modelCollection.CreateDerivedCollection (m => new MyClassViewModel (m)) - galead
  • Well, I do not always see the point in creating a class ModelCollection : IList<Model> . Most often, it is easier to get by with the usual List<Model> or ObservableCollection<Model> . And it's no use to create a VM) - iRumba Sept
  • You misunderstand - the derived-collection is created in the VM and is available for binding through the property of this VM. Naturally, the model should not know anything about the VM. I just use a ready-made solution, and you decide to write it yourself :) - galead