I have a small architectural problem with MVVM. In this question I will note Model through M and ViewModel through VM.
I have 4 collections that must be synchronized, and the items in them must have their properties synchronized. The collection synchronizing part is done. But now, I wonder if I have to change this architecture for easier maintenance in future because I have to change properties' values of a VM when an indirectly linked M changes the value of some of its properties.
Maybe I have to store in each VM a link to a corresponding M, but it is not always possible because I sync VMs to Ms, these Ms to other VMs, and these last VMs to other Ms, and some information is lost on this way.
In the figure below bool
s are boolean flags that indicate whether synching in that direction is allowed or blocked. I block synching of a collection with another collection to prevent infinite recursivity.
The classes behind the collections are not fixed, I can do any refactoring as necessary and I did not made them very well from the start.
The 4 classes are:
ClockGroupM
inherits fromBindableBase
(INotifyPropertyChanged
implementation in Prism)- props:
Name
(of typestring
)IsSelected
(bool
)Icon
(Bitmap
)
- props:
ClockGroupVM
inherits fromClockGroupM
- props:
DisplayString
(string
)Foreground
(Brush
)FontStyle
(FontStyle
)
- props:
FilterM
- props:
GroupNames
(List<string>
)SearchString
(string
)ShowActive
(bool
)ShowInactive
(bool
)ShowTimers
(bool
)ShowAlarms
(bool
)Clocks
(reference to the correspondingClockMCollection
on which the filter is applied)Groups
(reference to the correspondingClockGroupMCollection
)Autocorrected
(bool
, indicates whether there was coercion of the search string in the user interface)
- has a
Combine
method that combines two filters into one and - just a c-tor that accepts a reference to a
ClockMCollection
and a string (the search string)
- props:
FilterVM
- props:
MyFilter
(FilterM
)DisplayString
(string
)MyConstantImageSource
(ImageSource
)MyEmptyImageSource
(ImageSource
)MyNonEmptyImageSource
(ImageSource
)MyImageSource
(ImageSource
)IsSelected
(bool
)Items
(int
)HasContent
(bool
)
- method:
UpdateItemCount
(updates theItems
counter property, returnsvoid
)
- props:
The 4 collections are the following (they are not directly the ObservableCollection
instances, because I created wrappers for the ObservableCollection
s for more flexibility, and in each VMCollection
there is a reference to the corresponding MCollection
):
FilterMCollection
- represent search filters, there are a few basic filters that work with multiple groups and that should not be deletable (e.g. Active, Inactive etc.)FilterVMCollection
- the filters' VMs as shown in the "Filters" panelClockGroupVMCollection
- the groups' VMs as shown for example in the menu used to change the appartenance of an item to a group, so basic filters are excluded from thisClockGroupMCollection
- the groups as stored in the data file
The synching is always done through a single static method with many parameters. I use C#, WPF and Visual Studio.
I am a beginner in MVVM and this is the first question of mine on this site after a long period of time.
Thank you for patience, for any help, advice or suggestion.
Update 1
My application is a clock manager (a clock can be a timer or an alarm, in future maybe a stopwatch too). The clocks can be part of a group (similar to a folder). The clocks can be filtered. There are 5 base filters: All, Active, Inactive, Timers, Alarms. In future I think there will be custom saved filters based on search strings.
The M suffix stands for Model, and VM stands for ViewModel.
I can provide any piece of code needed from my application source, but please excuse my too big comments. Currently my application is not open source but it will be. Here is the method that does the synchronization:
internal static void SynchronizeCollectionChange<T, U>(
ObservableCollection<T> source,
NotifyCollectionChangedEventArgs e,
ObservableCollection<U> target,
Action<U> afterAddition = null,
Action<T> prepareDeletion = null,
Func<T, U, bool> equalsWithinTargetTo = null,
Func<T, U> toTarget = null,
int startingIndexInSource = 0,
int startingIndexInTarget = 0)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (T item in e.NewItems)
{
var mItem = (T)item;
int idx = source.IndexOf(mItem);
// if it doesn't exist in target, put it in target at idx, not with Add method
//int uIdx = target.IndexOf(toTarget(mItem));
//int newIndex = idx;
//if (startingIndexInTarget != -1) // only from this index will the addition take place
//{
// if (idx <startingIndexInTarget) // if the index in sourceis lower than the starting index in source that you take care of
// {
// continue; // go to next added item, ignore this one
// }
// //// else
// //newIndex = startingIndexInSource + idx;
//}
//else
//{
// newIndex = idx;
//}
// if the index in target is... it does not matter... the new index is computed by adding starting
//if(uIdx < startingIndexInTarget)
//{
//}
// the operation could have been either Insert or Add
//if (0 <= uIdx && uIdx <= source.Count - 1)
//{
// target.Insert(newIndex, toTarget(mItem)); // move
//}
//else
//{
int ni = idx - startingIndexInSource + startingIndexInTarget;
if (ni < 0 || ni > target.Count)
{
// the item must not be copied to target, it is a basic (auto) filter
break;
}
target.Insert(ni, toTarget(mItem)); // add
//target.Add(toTarget(mItem));
afterAddition?.Invoke(toTarget(mItem));
//}
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (T item in e.OldItems)
{
// here the T item is already deleted from source
// this should cvt T item to an U , but not when deleting it!
//U mItem = toTarget(item); // does not know that and that
//if (mItem == null)
//{
// return;
//}
prepareDeletion?.Invoke(item);
// here the T item is already deleted from source
// find VM objects that wrap the relevant model object and remove them
IEnumerable<U> query;
while ((query = from it in target
where equalsWithinTargetTo(item, it) // based on fact that U : T
select it).Count() > 0)
{
U m = query.First();
//if (startingIndexInTarget != -1)
//{
// if (target.IndexOf(m) < startingIndexInTarget)
// {
// continue;
// }
//}
//int index = target.IndexOf(m);
target.Remove(x => ReferenceEquals(x, m));
}
}
break;
case NotifyCollectionChangedAction.Reset:
if (startingIndexInTarget != 0 || startingIndexInSource != 0)
{
for (int i = target.Count - 1; i >= 0; --i)
{
int ni = i - startingIndexInSource + startingIndexInTarget;
if (ni < 0 || ni >= target.Count)
{
// the item must not be cleared from target on reset, it is a basic (auto) filter
continue;
}
target.RemoveAt(i);
}
}
else
{
target.Clear();
}
break;
case NotifyCollectionChangedAction.Move:
{
//if (startingIndexInTarget != -1 &&
// (e.OldStartingIndex < startingIndexInTarget ||
// e.NewStartingIndex < startingIndexInTarget))
//{
// break;
//}
// TODO: handle multiple items
int oi = e.OldStartingIndex - startingIndexInSource + startingIndexInTarget;
int ni = e.NewStartingIndex - startingIndexInSource + startingIndexInTarget;
if (ni < 0 || ni > target.Count)
{
break;
}
if (oi < 0 || oi > target.Count)
{
break;
}
target.Move(oi, ni);
break;
}
case NotifyCollectionChangedAction.Replace:
{
//if (startingIndexInTarget != -1 &&
// (e.OldStartingIndex < startingIndexInTarget ||
// e.NewStartingIndex < startingIndexInTarget))
//{
// break;
//}
int oi = e.OldStartingIndex - startingIndexInSource + startingIndexInTarget;
if (oi < 0 || oi > target.Count)
{
break;
}
// TODO: handle multiple items
target[oi] = toTarget((T)e.NewItems[0]);
break;
}
default:
throw new NotImplementedException();
}
}
Here can be found the synching routines using the method above (some were omitted because they are in another part of the source not in the discussion, I think).
I will refactor to make ClockGroupM not inherit from BindableBase. Now I understand that the VM should be a BindableBase, not the M. And I should have a method in the VM that updates its properties with values from the M, these properties will keep the Views updated through data binding.
Why I do not synchronize just the models and model collections: I am not very sure... I think it is because:
I create new models and remove old models and I do not know how to notify the collection of VMs about this and let it create/remove corresponding VMs.
Another reason is that I need to be notified when a M instance changes one of its properties, this is why I inherit from BindableBase.
Another situation I faced was that a VM changed its inner M reference because simply a clock changed its type (i.e. from timer it became an alarm) and the VM had to handle this:
internal void ChangeTypeOfClock(ClockM cd, Type newType)
{
Model.PropertyChanged -= Model_PropertyChanged;
if (newType == typeof(TimerData))
{
if (cd.IsActive)
{
cd.ActivateOrDeactivate();
}
var tdata = new TimerData(Model.MyDataFile, Model.MyDataFile.MultiAudioPlayer);
tdata.GroupName = cd.GroupName;
tdata.Icon = cd.Icon;
tdata.Style = cd.Style;
tdata.UserBackColor = cd.UserBackColor;
tdata.Tag = cd.Tag;
tdata.IsUnsaved = true;
tdata.ID = cd.ID;
tdata.Checkable = cd.Checkable;
tdata.Checked = cd.Checked;
Model = tdata;
}
else // AlarmData
{
if (cd.IsActive)
{
cd.ActivateOrDeactivate();
}
var adata = new AlarmData(Model.MyDataFile, Model.MyDataFile.MultiAudioPlayer);
adata.GroupName = cd.GroupName;
adata.Icon = cd.Icon;
adata.Style = cd.Style;
adata.UserBackColor = cd.UserBackColor;
adata.Tag = cd.Tag;
adata.IsUnsaved = true;
adata.ID = cd.ID;
adata.Checkable = cd.Checkable;
adata.Checked = cd.Checked;
Model = adata;
}
Model.PropertyChanged += Model_PropertyChanged;
RaisePropertyChanged("CurrentValue");
RaisePropertyChanged("ResetToValue");
RaisePropertyChanged("IsActive");
RaisePropertyChanged("IsUnsaved");
ClockTypeChanged?.Invoke(this, new ClockTypeChangedEventArgs()
{
ClockVM = this,
NewClockM = Model,
OldClockM = cd
});
IsUnsaved = true;
}
In one week I would like to publish a new version of my application.
Thank you very much.
Update 2
I posted the 8 classes here - they are somehow functional but the arrows in the figure above only synchronize items in collections, not the properties of existing items. After that version I started refactoring, and the code is currently not working because I still have questions:
If I use two-way data binding with the
VM
, the best thing to do is to inherit fromBindableBase
in theVM
class, or to use manually made synchronization methods (SyncFromModel
,SyncToModel
), for both directions, or both?If an
M
changes the value of some of its properties, how does theVM
know that it changed? Changes are coming not from the UI but from the database in the background... For example, when theM
of a group of clocks changes its ItemsCount property how do I send this notification to the View? Currently I send it from theM
to theClockGroupMCollection
, then from it I update theClockMCollection.IsUnsaved
property to have the valuetrue
.What would be the MVVM way to change the model: from the
VM
, from the database code? How do I access the models from the DB code? I need a collection of models for this, right?Should I have just collectons of
VM
s, or collections ofM
s too? I need to have a collection of clock group models because it is that collection that is traversed to apply a filter to the list of clocks that is visible to the user in the UI. Is this a case where synching is better than the alternative?
If it helps to say this, I also have in another part of the application a VM
that stores the edited-in-UI value corresponding to a value in the model that was previously saved or loaded from a database. I think I have to use a bound command that requests all the edited values from all the VM
s and saves them to the database. Currently I do this without commands but it works well. In this are implicated these classes:
SettingDataM
SettingDataMCollection
SettingDataVM
and its subclasses:AudioFileSettingDataVM
CheckBoxSettingDataVM
ImageFileSettingDataVM
IntegerSettingDataVM
TimeSpanSettingDataVM
I use these in the Settings window of the application.
I also have these classes:
ClockM
(with subclassesAlarmData
andTimerData
)ClockVM
ClockMCollection
ClockVMCollection
LogMessageVM
andLogMessageM
TimeOutWindowVM
andTimeOutWindowM
And those are all my MVVM classes in my application excluding the Views.
Thank you.
Update 3
I still need some claifications.
I still did not understand if I should have collections of Models, collection of ViewModels, or of both.
How can I improve collection synchronization? Especially, how can I do synchronization more modular for situations when I have 4 collections like in the figure in my question? I want especially to know what is the logical difference between synching 2 collections and more than 2, such as 3-4.
I hope these are my last questions in this topic.
Thank you.
Clock
stands for in your problem domain, or, if it is not a true clock (which is the most probable case), why it was named as such and what it represents.