.NET-Framework 4.0 introduces the new class DynamicObject. It is a base class for defining dynamic behavior at run time. This library is based on this and wraps any existing object to extend its functionalities. Basically, it allows the following:
- Extend any class with INotifyPropertyChanged without any additional code
- Using IEditableObject without any additional code to provide a simple commit/rollback mechanism
- Provide a simple usage of ValidationAttributes to validate even yet uncommitted values based on the validation rules defined by the underlying class
- Extend the defined ValidationAttributes for any property at runtime
- Using IDataErrorInfo to provide validation error via data binding for the GUI
It is intended to add a transparent layer between the GUI and the data provided by the view model.
Contents
A common task in every application is editing data in a modal window. In a basic scenario, the main window contains a simple list control (listview
, grid
or something similar). Each row represents a single object. When the user doubleclicks a row, a new modal window is shown. It contains several controls to edit any variable data from the selected object. The user may close the window by selecting the "OK"-Button to "submit" the data or a "Cancel"-Button to reject the changes made.
Using databinding in this scenario leads to several problems:
- How to prevent the immediate update of the displayed list on the main window when the user changes any data on the modal window?
(Happens because the controls on both windows are bound to the same object)
- How to "reject" all changes made when the "Cancel"-Button is clicked?
(With two-way-binding, all changes are written to the properties immediately)
- How to explicitly validate the data when the "OK"-Button is clicked?
(May be necessary to validate unchanged properties which are required - such as a lastname when entering data for a new person)
Possible solutions are:
- Don't use databinding on the modal window.
- Make a copy from the object bound on the main window and bind to this on the modal window.
Using the "dynamic proxy classes", we can use full databinding on both windows - without cloning/copying any object.
DynamicObject is new to the .NET-Framework 4.0. It allows the developer to enter any property or method. The properties or method names will be resolved at runtime. It provides just a minimum of functionality and can be used as base class for own implementations.
Additionally the framework provides the more sophisticated class ExpandoObject. It inherits from DynamicObject
and stores any value for any property in a dictionary.
Example from MSDN:
class Program
{
static void Main(string[] args)
{
dynamic employee, manager;
employee = new ExpandoObject();
employee.Name = "John Smith";
employee.Age = 33;
manager = new ExpandoObject();
manager.Name = "Allison Brown";
manager.Age = 42;
manager.TeamSize = 10;
WritePerson(manager);
WritePerson(employee);
}
private static void WritePerson(dynamic person)
{
Console.WriteLine("{0} is {1} years old.",
person.Name, person.Age);
}
}
Because it is declared as sealed
, this class is not useful for our needs. DynamicObject
provides a more basic functionality. It has several methods which are called when a property is accessed:
When a property value is read:
public virtual bool TryGetMember( GetMemberBinder binder, out object result)
When a property value is written:
public virtual bool bool TrySetMember(SetMemberBinder binder, object value)
We can use this for wrapping an existing object. Accessing any property will try find this property in the wrapped object via reflection and get/set the value.
public class MyEntity
{
[Required(AllowEmptyStrings=false, ErrorMessage = "Empty name not allowed")]
public string Name { get; set; }
public int Age { get; set; }
}
This is the base class for all further implementations. It provides a simple implementation to access the properties of the underlying object:
public class DynamicProxy : DynamicObject, INotifyPropertyChanged
{
#region protected methods
protected PropertyInfo GetPropertyInfo(string propertyName)
{
return ProxiedObject.GetType().GetProperties().First
(propertyInfo => propertyInfo.Name == propertyName);
}
protected virtual void SetMember(string propertyName, object value)
{
GetPropertyInfo(propertyName).SetValue(ProxiedObject, value, null);
RaisePropertyChanged(propertyName);
}
protected virtual object GetMember(string propertyName)
{
return GetPropertyInfo(propertyName).GetValue(ProxiedObject, null);
}
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(ProxiedObject, new PropertyChangedEventArgs(propertyName));
}
protected virtual void RaisePropertyChanged(string propertyName)
{
OnPropertyChanged(propertyName);
}
#endregion
#region constructor
public DynamicProxy() { }
public DynamicProxy(object proxiedObject)
{
ProxiedObject = proxiedObject;
}
#endregion
public override bool TryConvert(ConvertBinder binder, out object result)
{
if (binder.Type == typeof(INotifyPropertyChanged))
{
result = this;
return true;
}
if (ProxiedObject != null && binder.Type.IsAssignableFrom(ProxiedObject.GetType()))
{
result = ProxiedObject;
return true;
}
else
return base.TryConvert(binder, out result);
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
result = GetMember(binder.Name);
return true;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
SetMember(binder.Name, value);
return true;
}
#region public properties
public object ProxiedObject { get; set; }
#endregion
#region INotifyPropertyChanged Member
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
The method TryConvert
is implemented to allow some special actions when the user implicitly tries to convert a "DynamicProxy
" object to another type. If the object is converted to the type of the underlying object, it returns the underlying object. When converting to the interface "INotifyPropertyChanged
", it returns the proxy object itself.
public void Example()
{
var entity = new MyEntity();
dynamic proxy = new DynamicProxy(entity);
((INotifyPropertyChanged)proxy).PropertyChanged += (s, e) => DoSomething();
MyEntity underlyingObject = proxy;
proxy.Name = "another name";
}
Provides functionality to commit or rollback changes to an object that is used as a data source. For that, it contains a Dictionary<string, object>
.
After calling the method BeginEdit
all changed values of properties are redirected to that dictionary. The underlying object remains unchanged.
Calling the method EndEdit
will then write all values in the dictionary to the corresponding properties of the underlying object. It's like committing the values.
CancelEdit
throws away the dictionary and leaves the underlying object unchanged. It behaves like a rollback.
A nested class is used to achieve this functionality. It contains two dictionaries. One of them holds the original value for each changed property. The other holds the new value.
I won't show up the complete class here. Just the interesting parts. You will find the complete implementation in the accompanying source code.
[...]
protected override void SetMember(string propertyName, object value)
{
if (IsEditing)
{
_editBackup.SetOriginalValue(propertyName,
GetPropertyInfo(propertyName).GetValue(ProxiedObject, null));
_editBackup.SetNewValue(propertyName, value);
RaisePropertyChanged(propertyName);
}
else
base.SetMember(propertyName, value);
}
protected override object GetMember(string propertyName)
{
return IsEditing && _editBackup.NewValues.ContainsKey(propertyName) ?
_editBackup.NewValues[propertyName] :
base.GetMember(propertyName);
}
[...]
#region IEditableObject methods
public void BeginEdit()
{
if (!IsEditing)
_editBackup = new BackupState();
}
public void CancelEdit()
{
if (IsEditing)
{
_editBackup = null;
}
}
public void EndEdit()
{
if (IsEditing)
{
var editObject = _editBackup;
_editBackup = null;
foreach (var item in editObject.NewValues)
SetMember(item.Key, item.Value);
}
}
#endregion
While not in editing mode, the behaviour equals DynamicProxy
. After calling BeginEdit
, the property changed event is still called even though the underlying object remains unchanged.
This way, the developer can easily show a modal window (as described in "Background" above). The "OK"-Button will call EndEdit
and the "Cancel"-Button will call CancelEdit
.
public void Example()
{
var entity = new MyEntity();
dynamic proxy = new DynamicProxy(entity);
proxy.BeginEdit();
try
{
proxy.Name = "Another value";
proxy.EndEdit();
}
catch (Exception)
{
proxy.CancelEdit();
throw;
}
}
The .NET-Framework supports the validation of property via attributes. This is useful for database constraints and any other basic validations. The simple entity class provided for this article defines the attribute Required
for the property "Name
".
The class ValidatingProxy
extends EditableProxy
. On changing a property, it gets these validation attributes from the underlying object and uses them to validate the new value. Error messages are stored in a dictionary, which contains for every changed property a list of error messages (if there are any).
In addition, when calling the method Validate
, it forces validation for all properties with defined validation attributes. Even though they are unchanged, the method has several overloads to gain a finer control about what to validate.
[...]
protected override void SetMember(string propertyName, object value)
{
if (ValidateOnChange)
Validate(propertyName, value);
base.SetMember(propertyName, value);
}
protected virtual IEnumerable<ValidationAttribute>
GetValidationAttributes(PropertyInfo propertyInfo)
{
var validationAttributes = new List<ValidationAttribute>();
foreach (ValidationAttribute item in propertyInfo.GetCustomAttributes
(typeof(ValidationAttribute), true))
validationAttributes.Add(item);
return validationAttributes;
}
protected virtual bool Validate(PropertyInfo propertyInfo, object value)
{
var validationAttributes = GetValidationAttributes(propertyInfo);
if (validationAttributes.Count<validationattribute>() == 0)
return true;
var validationContext = new ValidationContext(ProxiedObject, null, null);
var validationResults = new Collection<validationresult>();
var returnValue = Validator.TryValidateValue(
value,
validationContext,
validationResults,
validationAttributes);
if (returnValue)
{
if (_validationResults.ContainsKey(propertyInfo.Name))
_validationResults.Remove(propertyInfo.Name);
}
else
{
if (_validationResults.ContainsKey(propertyInfo.Name))
_validationResults[propertyInfo.Name] = validationResults;
else
_validationResults.Add(propertyInfo.Name, validationResults);
}
return returnValue;
}
[...]
As you may have noticed, there is no implementation of any real "error handling interface" consumable via data binding by the GUI (WPF or Silverlight) yet. The simple reason for that is that I wanted to provide a base class without any concrete implementation.
Implementing the interface IDataErrorInfo
is very easy now. The interface provides two properties:
- One to get all current validation errors
- The other to get only the current validation errors of a specific property
public class DataErrorInfoProxy : ValidatingProxy, IDataErrorInfo
{
#region IDataErrorInfo Member
public string Error
{
get
{
var returnValue = new StringBuilder();
foreach (var item in _validationResults)
foreach (var validationResult in item.Value)
returnValue.AppendLine(validationResult.ErrorMessage);
return returnValue.ToString();
}
}
public string this[string columnName]
{
get
{
return _validationResults.ContainsKey(columnName) ?
string.Join(Environment.NewLine, _validationResults[columnName]) :
string.Empty;
}
}
#endregion
}
Because we already have all needed information in our base class, we just need to collect and format them for display.
The sample contains two windows:
The main window with simple list control (listview
). Each row represents a single object. The shown data is generated in a simple loop. When the user doubleclicks a row, a new modal window is shown.
The modal window contains several controls to edit the data from the selected object. The user may close the window by selecting the "OK"-Button to "submit" the data or a "Cancel"-Button to reject the changes made. When the "OK"-Button is selected, the validation of the values is executed and any changes of the validation errors are displayed immediately to the user.
Both windows are using data binding with the same property names.
If the users clicks "Add new entity" on the main window, the modal window will be shown with an "empty" entity object. Even if the user doesn't enter any name or age, the validation will be executed on "OK" and errors will be shown.
One of the most interesting parts was the need for updating the display to reflect any changes in the validation state of the controls. The article WPF ErrorProvider - Integrating IDataErrorInfo, WPF, and the Validation Application Block (VAB) from Rahul Singla helped me a lot and I converted parts of his code as extension methods to C#:
public static class ExtensionDependencyObject
{
private static DependencyProperty GetDependencyProperty
(PropertyInfo propertyInfo, DependencyObject element)
{
return typeof(DependencyProperty).IsAssignableFrom(propertyInfo.PropertyType) ?
propertyInfo.GetValue(element, null) as DependencyProperty :
null;
}
private static DependencyProperty GetDependencyProperty
(FieldInfo fieldInfo, DependencyObject element)
{
return typeof(DependencyProperty).IsAssignableFrom(fieldInfo.FieldType) ?
fieldInfo.GetValue(element) as DependencyProperty :
null;
}
private static DependencyProperty GetDependencyProperty
(MemberInfo memberInfo, DependencyObject element)
{
switch (memberInfo.MemberType)
{
case MemberTypes.Field: return GetDependencyProperty
(memberInfo as FieldInfo, element);
case MemberTypes.Property: return GetDependencyProperty(
memberInfo as PropertyInfo, element);
default: return null;
}
}
public static void FindBindingsRecursively
(this DependencyObject element, IDataErrorInfo dataErrorInfo)
{
FindBindingsRecursively(element, dataErrorInfo, null);
}
public static void FindBindingsRecursively
(this DependencyObject element, IDataErrorInfo dataErrorInfo,
string boundPropertyName)
{
var members = element.GetType().GetMembers
(BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy);
foreach (var member in members)
{
var dp = GetDependencyProperty(member, element);
if (dp != null)
{
var bb = BindingOperations.GetBinding(element, dp);
if (bb != null)
{
if (string.IsNullOrEmpty(boundPropertyName) ||
bb.Path.Path == boundPropertyName)
{
if (element is FrameworkElement)
{
var errors = dataErrorInfo[bb.Path.Path];
var be = (element as FrameworkElement).GetBindingExpression(dp);
if (string.IsNullOrEmpty(errors))
Validation.ClearInvalid(be);
else
Validation.MarkInvalid(
be,
new ValidationError(new ExceptionValidationRule(), be, errors, null));
}
}
}
}
}
if (element is FrameworkElement || element is FrameworkContentElement)
foreach (var childElement in LogicalTreeHelper.GetChildren(element))
if (childElement is DependencyObject)
FindBindingsRecursively(childElement as DependencyObject,
dataErrorInfo, boundPropertyName);
}
}
The class "DynamicObject
" has some nice features. One of them is the possibility to declare "real" properties in derived classes and access them the same way as "virtual properties". Reflection checks the "real" properties first and only if there is none matching property found, it will call TryGetMember
.
- 1.0: Initial release
- 1.1: Upload of most recent version with already fixed bug as reported by myker