Using Silverlight and MVVM you often use property change tracking mechanisms, such as implementing INotifyPropertyChanged and using ObservableCollections (which implement INotifyCollectionChanged).
This is fine when you are only interested in letting the UI synchronise with your data in the ViewModel (and vice-versa) but sometimes you need to track these changes in the code, either within your own class or in another class elsewhere.
I had this issue recently, where I needed to know in my "Results" ViewModel when anything changed in any of my various "Input" ViewModels so that I could invalidate the results (so the user knows they need to re-calculate).
In order to achieve this I needed to listen to events in the following 3 categories on many properties:
- Property changed (setter called)
- Collection changed (items added/removed)
- Collection item property changed (element of a list has a property changed)
Rather than having to write event handling code and logic everywhere, I decided to design a base class that my "Input" viewmodels can inherit from that would take care of rolling up these 3 scenarios into a single event. In my "Results" class I then can simply add a listener for the one "InputsChanged" event.
My view model base derives from a base implementing INotifyPropertyChanged and looks as follows:
public abstract class ResultsInputViewModelBase : ViewModelBase
{
private Dictionary<string, PropertyInfo> _collectionChangingPropertiesToTrack = new Dictionary<string, PropertyInfo>();
private Dictionary<string, PropertyInfo> _enumerablePropertiesToTrack = new Dictionary<string, PropertyInfo>();
public ResultsInputViewModelBase()
{
this.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(this.ResultsInputViewModelBase_PropertyChanged);
foreach (string propertyName in this.TrackedInputProperties)
{
object currentValue = this;
if (!string.IsNullOrWhiteSpace(propertyName))
{
PropertyInfo pi = null;
if (propertyName.Contains("."))
{
Type currentType = this.GetType();
string[] props = propertyName.Split('.');
int pc = 0;
foreach (string p in props)
{
pc++;
pi = currentType.GetProperty(p);
if (pi != null && pc < props.Length)
{
currentType = pi.PropertyType;
currentValue = pi.GetValue(currentValue, null);
}
else
{
break;
}
}
}
else
{
pi = this.GetType().GetProperty(propertyName);
}
if (pi != null)
{
if (typeof(INotifyCollectionChanged).IsAssignableFrom(pi.PropertyType))
{
this._collectionChangingPropertiesToTrack.Add(propertyName, pi);
this.TryAttachCollectionTracker(pi, currentValue);
}
if (typeof(IEnumerable).IsAssignableFrom(pi.PropertyType))
{
this._enumerablePropertiesToTrack.Add(propertyName, pi);
this.TryAttachEnumeratedPropertyChangeTracker(pi, currentValue);
}
}
}
}
}
public event EventHandler InputsChanged;
protected abstract string[] TrackedInputProperties
{
get;
}
protected void OnInputsChanged()
{
if (this.InputsChanged != null)
{
this.InputsChanged(this, EventArgs.Empty);
}
}
private void TryAttachCollectionTracker(PropertyInfo pi, object container)
{
INotifyCollectionChanged collection_propValue = pi.GetValue(container, null) as INotifyCollectionChanged;
if (collection_propValue != null)
{
collection_propValue.CollectionChanged += new NotifyCollectionChangedEventHandler(this.ResultsInputViewModelBase_CollectionChanged);
}
}
private void TryAttachEnumeratedPropertyChangeTracker(PropertyInfo pi, object container)
{
IEnumerable collection_propValue = pi.GetValue(container, null) as IEnumerable;
if (collection_propValue != null)
{
foreach (var element in collection_propValue)
{
if (element is ResultsInputViewModelBase)
{
((ResultsInputViewModelBase)element).InputsChanged += this.InputsChanged;
}
else if (element is INotifyPropertyChanged)
{
((INotifyPropertyChanged)element).PropertyChanged += new PropertyChangedEventHandler(this.ResultsInputViewModelBase_TrackedElementPropertyChanged);
}
}
}
}
private void ResultsInputViewModelBase_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (Array.IndexOf(this.TrackedInputProperties, e.PropertyName) >= 0)
{
object currentValue = this;
if (!string.IsNullOrWhiteSpace(e.PropertyName))
{
PropertyInfo pi = null;
if (e.PropertyName.Contains("."))
{
Type currentType = this.GetType();
string[] props = e.PropertyName.Split('.');
int pc = 0;
foreach (string p in props)
{
pc++;
pi = currentType.GetProperty(p);
if (pi != null && pc < props.Length)
{
currentType = pi.PropertyType;
currentValue = pi.GetValue(currentValue, null);
}
else
{
break;
}
}
}
}
if (this._collectionChangingPropertiesToTrack.ContainsKey(e.PropertyName))
{
this.TryAttachCollectionTracker(this._collectionChangingPropertiesToTrack[e.PropertyName], currentValue);
}
if (this._enumerablePropertiesToTrack.ContainsKey(e.PropertyName))
{
this.TryAttachEnumeratedPropertyChangeTracker(this._enumerablePropertiesToTrack[e.PropertyName], currentValue);
}
this.OnInputsChanged();
}
}
private void ResultsInputViewModelBase_TrackedElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
this.OnInputsChanged();
}
private void ResultsInputViewModelBase_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (var element in e.NewItems)
{
if (element is ResultsInputViewModelBase)
{
((ResultsInputViewModelBase)element).InputsChanged += this.InputsChanged;
}
else if (element is INotifyPropertyChanged)
{
((INotifyPropertyChanged)element).PropertyChanged += new PropertyChangedEventHandler(this.ResultsInputViewModelBase_TrackedElementPropertyChanged);
}
}
}
if (e.OldItems != null)
{
foreach (var element in e.OldItems)
{
if (element is ResultsInputViewModelBase)
{
((ResultsInputViewModelBase)element).InputsChanged -= this.InputsChanged;
}
else if (element is INotifyPropertyChanged)
{
((INotifyPropertyChanged)element).PropertyChanged -= new PropertyChangedEventHandler(this.ResultsInputViewModelBase_TrackedElementPropertyChanged);
}
}
}
this.OnInputsChanged();
}
}
This class essentially adds internal listeners to all the different kinds of events that the derived class may raise that we are interested in. It leaves one single property that must be implemented in the derived class, where the programmer can list the properties which should be tracked.
For example:
protected override string[] TrackedInputProperties
{
get { return new string[] {
"AgeBandFilters",
"MaritalStatusFilters"
}; }
}
Finally, in the "Results" ViewModel, I pass in an instance of my derived class and then listen for the "InputsChanged" event:
public ExampleInputViewModel ExampleInputViewModel
{
get { return _exampleInputViewModel; }
set {
RemoveInputChangedHandler(_exampleInputViewModel);
_exampleInputViewModel = value;
AddInputChangedHandler(_exampleInputViewModel);
RaisePropertyChanged("ExampleInputViewModel"); }
}
private void RemoveInputChangedHandler(ResultsInputViewModelBase inputConfigViewModel)
{
if(inputConfigViewModel != null)
inputConfigViewModel.InputsChanged -= new EventHandler(inputConfigViewModel_InputsChanged);
}
private void AddInputChangedHandler(ResultsInputViewModelBase inputConfigViewModel)
{
if (inputConfigViewModel != null)
inputConfigViewModel.InputsChanged += new EventHandler(inputConfigViewModel_InputsChanged);
}
void inputConfigViewModel_InputsChanged(object sender, EventArgs e)
{
CurrentResults = null;
}