I've created a general purpose ViewModel base class in what I believe it's purest behaviour. It features property notification, error notification and property value history. Please take a look at it and tell me what can be improved or what you think is missing.
For the sake of clarity I've merged everything in 1 codeblock:
namespace Trakt
{
[System.AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class TrackedAttribute : Attribute
{
public TrackedAttribute() { }
}
public abstract class ViewModelBase : INotifyPropertyChanged, INotifyDataErrorInfo
{
private const int MAXHISTORY = 10;
private bool dirty;
public bool Dirty
{
get { return dirty; }
protected set { SetProperty(ref dirty, value); }
}
private bool corrupted;
public bool Corrupted
{
get { return corrupted; }
private set { SetProperty(ref corrupted, value); }
}
private readonly Guid _guid;
private Stack<string> _changeList;
private Dictionary<string, Stack<object>> _changes;
private Dictionary<string, List<string>> _errors;
public void ClearHistory()
{
_changeList = new Stack<string>();
_changes = _changes
.Select(_change => new KeyValuePair<string, Stack<object>>(_change.Key, new Stack<object>(new object[] { _change.Value.Pop() })))
.ToDictionary(_change => _change.Key, _change => _change.Value);
}
public void ClearHistoryFor([CallerMemberName]string propertyName = "")
{
var lastValue = _changes[propertyName].Pop();
_changeList = new Stack<string>(_changeList.Where(i => i != propertyName)); // changelist order still ok???
_changes = _changes
.Where(_change => _change.Key != propertyName)
.Select(_change => new KeyValuePair<string, Stack<object>>(_change.Key, _change.Value))
.ToDictionary(_change => _change.Key, _change => _change.Value);
}
public void ClearHistoryFor<TProperty>(Expression<Func<TProperty>> propertyExpression)
{
ClearHistoryFor(ExtractPropertyNameFor(propertyExpression));
}
protected ViewModelBase()
{
_guid = Guid.NewGuid();
_changes = new Dictionary<string, Stack<object>>();
_changeList = new Stack<string>();
_errors = new Dictionary<string, List<string>>();
var properties = from property in this.GetType().GetProperties()
where Attribute.IsDefined(property, typeof(TrackedAttribute))
select property;
foreach (var property in properties)
{
//var trakt = property.GetCustomAttributes(typeof(TrackedAttribute), true);
var value = this.GetType().GetProperty(property.Name).GetValue(this);
_changes.Add(property.Name, new Stack<object>(new object[] { value }));
}
}
public void Undo(int steps)
{
var count = _changes[_changeList.Peek()].Count;
if (count <= 1)
return;
if (steps >= count)
steps = count -1;
var lastPropertyName = _changeList.Pop();
var previousPropertyValue = _changes[lastPropertyName].Peek();
int i = 0;
do
{
previousPropertyValue = _changes[lastPropertyName].Pop();
i++;
} while (i <= steps);
SetProperty(lastPropertyName, previousPropertyValue);
}
public void Undo()
{
Undo(1);
}
protected bool IsTracked<TProperty>([CallerMemberName]string propertyName = "")
{
return _changes.ContainsKey(propertyName);
}
protected void Track<TProperty>([CallerMemberName]string propertyName = "")
{
Track(default(TProperty), propertyName);
}
protected void Track<TProperty>(TProperty value, [CallerMemberName]string propertyName = "")
{
if (_changes.ContainsKey(propertyName))
return;
if (!this.GetType().GetProperties().Select(p => p.Name).Contains(propertyName))
return;
_changes.Add(propertyName, new Stack<object>(new object[] { value }));
}
protected void UnTrack<TProperty>([CallerMemberName]string propertyName = "")
{
if (!_changes.ContainsKey(propertyName))
return;
_changes.Remove(propertyName);
}
#region INotifyDataErrorInfo
public bool HasErrors { get { return Corrupted; } }
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
private void OnErrorsChanged([CallerMemberName]string propertyName = "")
{
var handler = ErrorsChanged;
handler?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
public IEnumerable GetErrors()
{
// TODO!
throw new NotImplementedException();
}
public IEnumerable GetErrors<TProperty>(Expression<Func<TProperty>> propertyExpression)
{
return GetErrors(ExtractPropertyNameFor(propertyExpression));
}
public IEnumerable GetErrors([CallerMemberName]string propertyName = "")
{
var errors = new List<string>();
_errors.TryGetValue(propertyName, out errors);
return errors;
}
protected void AddError(string errorMessage, [CallerMemberName]string propertyName = "")
{
if (!_errors.ContainsKey(propertyName))
_errors.Add(propertyName, new List<string>());
_errors[propertyName].Add(errorMessage);
OnErrorsChanged(propertyName);
Corrupted = true;
}
protected void ClearError([CallerMemberName]string propertyName = "")
{
_errors.Remove(propertyName);
OnErrorsChanged(propertyName);
Corrupted = _errors.Select(e => e.Value).Count() != 0;
}
protected bool HasError([CallerMemberName]string propertyName = "")
{
return _errors.ContainsKey(propertyName) && _errors[propertyName].Count > 0;
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName]string propertyName = "")
{
var handler = PropertyChanged;
handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetProperty<TProperty>(string propertyName, TProperty value/*, bool notify = true*/)
{
this.GetType().GetProperty(propertyName).SetValue(this, value);
}
protected void SetProperty<TProperty>(ref TProperty field, TProperty value, [CallerMemberName]string propertyName = "")
{
if (!EqualityComparer<TProperty>.Default.Equals(field, value))
{
field = value;
OnPropertyChanged(propertyName);
if (_changes.ContainsKey(propertyName))
{
if(_changes[propertyName].Count == MAXHISTORY)
{
// max history limit reached, need to shift items...
var list = new LinkedList<object>(_changes[propertyName]);
list.RemoveLast();
_changes[propertyName] = new Stack<object>(list.Reverse());
}
_changeList.Push(propertyName);
_changes[propertyName].Push(value);
}
if(propertyName != nameof(Dirty))
Dirty = true;
}
}
protected void SetProperty<TProperty>(ref TProperty field, TProperty value, Expression<Func<TProperty>> propertyExpression)
{
SetProperty(ref field, value, ExtractPropertyNameFor(propertyExpression));
}
private static string ExtractPropertyNameFor<T>(Expression<Func<T>> propertyExpression)
{
var memberExp = propertyExpression.Body as MemberExpression;
if (memberExp == null)
throw new ArgumentException("Expression must be a MemberExpression.", "propertyExpression");
return memberExp.Member.Name;
}
}
#endregion
}
Sample implementation:
public class SomeViewModel: ViewModelBase
{
private int trackedProperty;
[Tracked]public int TrackedProperty
{
get { return trackedProperty; }
set
{
SetProperty(ref trackedProperty, value, () => this.TrackedProperty);
if (trackedProperty == 1)
AddError("Value 1 is not allowed");
else
ClearError();
}
}
}
I'm very into SOLID (or try to) and clean code so don't be too gentle! :)