In the Prism 4.1 Developer’s Guide there was a multi-purpose object, DomainObject, that implemented INotifyPropertyChanged and INotifyDataErrorInfo. This was a nice generalized object to inherit from in MVVM. In models where I need to implement DataContract this was nice because I can just throw in the [DataContract(IsReference = true)] attribute so I can serialize.
The problem is that DomainObject uses ValidateProperty(“PropertyName”) and RaisePropertyChanged(“PropertyName”) instead of the SetProperty(ref _holder, value) and OnPropertyChanged(() => Property) that BindableBase uses. To start with I despise strings in code unless it is explicitly intended to be the text a user should see. I also would like to make everything consistent. It would be nice to just extend BindableBase but since it doesn’t implement DataContract I can’t. Not only that but I want to add validation and implement INotifyDataErrorInfo.
Fortunately, since Prism is open-source we can dive into the code and figure out how they implemented BindableBase and add that code to our own DomainObject while still implementing INotifyDataErrorInfo.
[DataContract(IsReference = true)] public abstract class DomainObject : INotifyPropertyChanged, INotifyDataErrorInfo { protected DomainObject() { } #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// sets the storage to the value /// </summary> /// <typeparam name="T"></typeparam> /// <param name="storage"></param> /// <param name="value"></param> /// <param name="propertyName"></param> /// <returns>True if the value was changed, false if the existing value matched the desired value.</returns> protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null) { ValidateProperty(propertyName, value); if (object.Equals(storage, value)) return false; storage = value; OnPropertyChanged(propertyName); return true; } /// <summary> /// Notifies listeners that a property value has changed. /// </summary> /// <param name="propertyName">Name of the property used to notify listeners. This /// value is optional and can be provided automatically when invoked from compilers /// that support <see cref="CallerMemberNameAttribute"/>.</param> protected void OnPropertyChanged(string propertyName) { var eventHandler = this.PropertyChanged; if (eventHandler != null) { eventHandler(this, new PropertyChangedEventArgs(propertyName)); } } /// <summary> /// Raises this object's PropertyChanged event. /// </summary> /// <typeparam name="T">The type of the property that has a new value</typeparam> /// <param name="propertyExpression">A Lambda expression representing the property that has a new value.</param> protected void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression) { var propertyName = Microsoft.Practices.Prism.Mvvm.PropertySupport.ExtractPropertyName(propertyExpression); this.OnPropertyChanged(propertyName); } #endregion #region INotifyDataErrorInfo Members private ErrorsContainer<string> errorsContainer; public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged = delegate { }; protected ErrorsContainer<string> ErrorsContainer { get { if (errorsContainer == null) { errorsContainer = new ErrorsContainer<string>(pn => OnErrorsChanged(pn)); } return this.errorsContainer; } } public IEnumerable GetErrors(string propertyName) { return ErrorsContainer.GetErrors(propertyName); } public bool HasErrors { get { return ErrorsContainer.HasErrors; } } protected virtual void OnErrorsChanged(string propertyName) { var eventHandler = this.PropertyChanged; if (eventHandler != null) { eventHandler(this, new PropertyChangedEventArgs(propertyName)); } } protected void OnErrorsChanged<T>(Expression<Func<T>> propertyExpression) { var propertyName = Microsoft.Practices.Prism.Mvvm.PropertySupport.ExtractPropertyName(propertyExpression); OnErrorsChanged(propertyName); } #endregion #region property validation protected void ValidateProperty(object value, [CallerMemberName] string propertyName = null) { ValidateProperty(propertyName, value); } protected virtual void ValidateProperty(string propertyName, object value) { } #endregion }
So let’s look at this wall of code. First, on line 18 SetProperty is implemented, almost identical to how BindableBase in Prism 5 is. The awesome thing is the use of the Caller Information attribute, CallerMemberName, that was added in C# 5. CallerMemberName, when calling from a get or set, is the name of the property. This makes the boilerplate code a lot cleaner, as noted in my previous post on upgrading to Prism 5. The only real difference here is that I call ValidateProperty, which is optionally implemented in the models that extend from DomainObject. Most of the time nothing happens but there are times where errors get added to the ErrorsContainer because the model implements ValidateProperty.
The other cool thing from BindableBase is the use of Expression<Func> propertyExpression that then extracts the property name. This makes the code a lot cleaner since you can just pass in () => Property. The original DomainObject used slightly different terminology with RaisePropertyChanged and RaiseErrorChanged. In the above code, at lines 36 and 88 you can see I’ve changed the methods to be the consistent with BindableBase. Again, this is all so I can get the benefits of INotifyPropertyChanged and INotifyDataErrorInfo while still being able to utilize DataContract. Your situation may be differently than mine but this provides a generalized class that has made MVVM easier for me.
Thanks,
Brian