winforms databinding best practices
Demands / problems:
- I would like to bind multiple properties of an entity to controls in a form. Some of which are read only fr开发者_StackOverflowom time to time (according to business logic). - Edit: The logic is based on the bound instance, not only on its the type.
- When using an entity that implements
INotifyPropertyChanged
as theDataSource
, every change notification refreshes all the controls bound to that data source (easy to verify - just bind two properties to two controls and invoke a change notification on one of them, you will see that both properties are hit and reevaluated). - There should be user friendly error notifications (the entity implements
IDataErrorInfo
). (probably usingErrorProvider
)
Using the entity as the DataSource
of the controls leads to performance issues and makes life harder when its time for a control to be read only.
I thought of creating some kind of wrapper that holds the entity and a specific property so that each control would be bound to a different DataSource
. Moreover, that wrapper could hold the ReadOnly
indicator for that property so the control would be bound directly to that value.
The wrapper could look like this:
interface IPropertyWrapper : INotifyPropertyChanged, IDataErrorInfo
{
object Value { get; set; }
bool IsReadOnly { get; }
}
But this means also a different ErrorProvider
for each property (property wrapper)
I feel like I'm trying to reinvent the wheel... What is the 'proper' way of handling complex binding demands like these?
Thanks ahead.
You could write a wrapper for you entity that implements ICustomTypeDescriptor
. That way, you could decide which properties are read-only or not... but that's quite a lot of work for a not so complex scenario.
A simpler solution would be to change the DataSourceUpdateMode
of the binding to Never
when you want the property to be read-only.
UPDATE: here's a basic wrapper implementing ICustomTypeDescriptor
:
class EntityWrapper<T> : CustomTypeDescriptor
{
public EntityWrapper(T entity)
{
this.Entity = entity;
var properties = TypeDescriptor.GetProperties(typeof(T))
.Cast<PropertyDescriptor>()
.ToArray();
ReadOnly = properties.ToDictionary(p => p.Name, p => p.IsReadOnly);
_properties = new PropertyDescriptorCollection(properties
.Select(p => new WrapperPropertyDescriptor(p, this))
.ToArray());
}
public T Entity { get; private set; }
public Dictionary<string, bool> ReadOnly { get; private set; }
public override PropertyDescriptorCollection GetProperties()
{
return _properties;
}
public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
return _properties;
}
private PropertyDescriptorCollection _properties;
private class WrapperPropertyDescriptor : PropertyDescriptor
{
private EntityWrapper<T> _entityWrapper;
private PropertyDescriptor _property;
public WrapperPropertyDescriptor(PropertyDescriptor property, EntityWrapper<T> entityWrapper)
: base(property)
{
_property = property;
_entityWrapper = entityWrapper;
}
public override bool CanResetValue(object component)
{
return _property.CanResetValue(component);
}
public override Type ComponentType
{
get { return _property.ComponentType; }
}
public override object GetValue(object component)
{
return _property.GetValue(component);
}
public override bool IsReadOnly
{
get
{
return _entityWrapper.ReadOnly[this.Name];
}
}
public override Type PropertyType
{
get { return _property.PropertyType; }
}
public override void ResetValue(object component)
{
_property.ResetValue(component);
}
public override void SetValue(object component, object value)
{
_property.SetValue(component, value);
}
public override bool ShouldSerializeValue(object component)
{
return _property.ShouldSerializeValue(component);
}
}
}
As you can see, it's perfectly possible to make a property read-only just for one instance :
MyEntity a = new MyEntity { Foo = "hello", Bar = 42 };
MyEntity b = new MyEntity { Foo = "world", Bar = 5 };
EntityWrapper<MyEntity> wa = new EntityWrapper<MyEntity>(a);
EntityWrapper<MyEntity> wb = new EntityWrapper<MyEntity>(b);
var fooA = wa.GetProperties()["Foo"];
var fooB = wb.GetProperties()["Foo"];
wa.ReadOnly["Foo"] = false;
wb.ReadOnly["Foo"] = true;
Console.WriteLine("Property Foo of object a is read-only : {0}", fooA.IsReadOnly);
Console.WriteLine("Property Foo of object b is read-only : {0}", fooB.IsReadOnly);
I wouldn't wrap each property individually ... I would wrap the root domain object. In there I would implement the readonly logic ... and only set the value on the real domain objet if the readonly flag is set to false.
精彩评论