I'm working on an (legacy) WinForms application and I like to do a more dynamic way of supplying the users with error information in a way I'm used to do with MVC.
Validation in WinForms however seems to work around the IDataErrorInfo interface, but I don't like to implement this interface on the objects I use for binding. I often can bind my command objects to the interface. Commands are DTOs that describe the business operation and are defined in the domain layer (the logic for executing those commands is defined in the business layer).
Since commands are part of the domain, I don't want to implement IDataErrorInfo on them, because this would couple them directly with the validation logic (since calling one of the IDataErrorInfo methods assumes validation). The only thing I want to do is mark my command properties with DataAnnotation attributes.
So my question is: how do I enable validation in WinForms (using the ErrorProvider) but without having to implement IDataErrorInfo on the classes that I use to bind?
For instance, is there a way to hook onto the ErrorProvider and delegate the validation of to DataAnnotations' Validate class?
The trick is to look at the control's DataBindings to determine the type and property that the control is bound to. With this information the validation can be hooked.
public static void RegisterBindingSourceValidations(Form form,
ErrorProvider errorProvider)
{
Requires.IsNotNull(form, "form");
Requires.IsNotNull(errorProvider, "errorProvider");
RegisterBindingSourceValidationsRecursive(form, errorProvider);
}
private static void RegisterBindingSourceValidationsRecursive(
Control control, ErrorProvider provider)
{
foreach (Control childControl in control.Controls)
{
RegisterBindingSourceValidationsForControl(childControl, provider);
RegisterBindingSourceValidationsRecursive(childControl, provider);
}
}
private static void RegisterBindingSourceValidationsForControl(
Control control, ErrorProvider errorProvider)
{
AddMaximumStringLengthToDataViewBoundTextBox(control);
AddDataAnnotationsValidations(control, errorProvider);
}
private static void AddMaximumStringLengthToDataViewBoundTextBox(Control control)
{
TextBox textBox = control as TextBox;
if (textBox == null)
{
return;
}
int maximumTextLength = (
from dataBinding in textBox.DataBindings.Cast<Binding>()
where StringComparer.OrdinalIgnoreCase.Equals(dataBinding.PropertyName, "Text")
let bindingSource = (BindingSource)dataBinding.DataSource
where bindingSource.SyncRoot is DataView
let view = (DataView)bindingSource.SyncRoot
let bindingField = dataBinding.BindingMemberInfo.BindingField
let maxLength = view.Table.Columns[bindingField].MaxLength
where maxLength > 0
select maxLength)
.SingleOrDefault();
if (maximumTextLength > 0)
{
textBox.MaxLength = maximumTextLength;
}
}
private static void AddDataAnnotationsValidations(Control control,
ErrorProvider errorProvider)
{
var binding = (
from dataBinding in control.DataBindings.Cast<Binding>()
where dataBinding.DataSource is BindingSource
let bindingSource = (BindingSource)dataBinding.DataSource
where !string.IsNullOrEmpty(dataBinding.BindingMemberInfo.BindingMember)
let modelType = bindingSource.GetEnumerableElementType()
where modelType != null
let controlProperty = control.GetType().GetProperty(dataBinding.PropertyName)
let boundPropertyName = dataBinding.BindingMemberInfo.BindingMember
select new { bindingSource, modelType, controlProperty, boundPropertyName })
.FirstOrDefault();
if (binding != null)
{
RegisterValidator(control, binding.controlProperty,
binding.modelType, binding.boundPropertyName,
() => binding.bindingSource.Current, errorProvider);
if (control is TextBox)
{
SetMaximumTextLength((TextBox)control, binding.modelType,
binding.boundPropertyName);
}
}
}
private static void SetMaximumTextLength(TextBox textBoxToValidate,
Type modelType, string modelPropertyName)
{
var propertyChain = GetPropertyChain(modelType, modelPropertyName).ToArray();
ApplyMaximumStringLength(textBoxToValidate, propertyChain.Last());
}
private static void ApplyMaximumStringLength(TextBox textBoxToValidate,
PropertyInfo property)
{
var maximumLength = (
from attribute in property.GetCustomAttributes(
typeof(StringLengthAttribute), true)
.OfType<StringLengthAttribute>()
select attribute.MaximumLength)
.FirstOrDefault();
if (maximumLength > 0)
{
textBoxToValidate.MaxLength = maximumLength;
}
}
private static Type GetEnumerableElementType(
this BindingSource bindingSource)
{
return (
from intf in bindingSource.DataSource.GetType()
.GetInterfaces()
where intf.IsGenericType
where intf.GetGenericTypeDefinition() == typeof(IEnumerable<>)
let type = intf.GetGenericArguments().Single()
where type != typeof(object)
select type)
.SingleOrDefault();
}
public static void RegisterValidator(Control controlToValidate,
PropertyInfo controlProperty,
Type modelType, string modelPropertyName,
Func<object> instanceSelector, ErrorProvider errorProvider)
{
Requires.IsNotNull(controlToValidate, "controlToValidate");
Requires.IsNotNull(controlProperty, "controlProperty");
Requires.IsNotNull(modelType, "modelType");
Requires.IsNotNull(instanceSelector, "instanceSelector");
Requires.IsNotNull(errorProvider, "errorProvider");
controlToValidate.CausesValidation = true;
var propertyChain = GetPropertyChain(modelType, modelPropertyName).ToArray();
PropertyInfo targetProperty = propertyChain.Last();
var validator = new ControlValidator
{
ControlToValidate = controlToValidate,
ControlProperty = controlProperty,
PropertyChain = propertyChain,
InstanceSelector = instanceSelector,
ErrorProvider = errorProvider,
ValidationAttributes =
targetProperty.GetCustomAttributes<ValidationAttribute>().ToArray(),
Converter = TypeDescriptor.GetConverter(targetProperty.PropertyType),
};
if (validator.ValidationAttributes.Any())
{
controlToValidate.CausesValidation = true;
// This check seems redundant, since WinForms doesn't allow you to
// leave a form field when the value can't be converted, which
// means the validator will not go off.
if (validator.Converter == null)
{
throw GetTypeConverterMissingExcpetion(targetProperty);
}
controlToValidate.Validating += (s, e) => validator.Validate();
}
}
private static Exception GetTypeConverterMissingExcpetion(
PropertyInfo modelProperty)
{
return new InvalidOperationException(string.Format(
"Property '{0}' declared on type {1} cannot be used for validation. " +
"There is no TypeConverter for type {2}.",
modelProperty.Name,
modelProperty.DeclaringType,
modelProperty.PropertyType));
}
private static IEnumerable<PropertyInfo> GetPropertyChain(
Type modelType, string modelPropertyName)
{
foreach (string propertyName in modelPropertyName.Split('.'))
{
var property = modelType.GetProperty(propertyName);
if (property == null)
{
throw new InvalidOperationException(string.Format(
"Property with name '{0}' could not be found on type {1}.",
propertyName, modelType.FullName));
}
modelType = property.PropertyType;
yield return property;
}
}
private class ControlValidator
{
public PropertyInfo[] PropertyChain { get; set; }
public ValidationAttribute[] ValidationAttributes { get; set; }
public TypeConverter Converter { get; set; }
public Func<object> InstanceSelector { get; set; }
public ErrorProvider ErrorProvider { get; set; }
public Control ControlToValidate { get; set; }
public PropertyInfo ControlProperty { get; set; }
public void Validate()
{
ModelPropertyPair pair = this.GetModelPropertyChain().Last();
object value = this.GetValueToValidate();
object convertedValue;
if (!this.TryConvertValue(value, out convertedValue))
{
this.ErrorProvider.SetError(this.ControlToValidate,
"Value is invalid.");
return;
}
string errorMessage = this.GetValidationErrorOrNull(pair, convertedValue);
this.ErrorProvider.SetError(this.ControlToValidate, errorMessage);
}
private IEnumerable<ModelPropertyPair> GetModelPropertyChain()
{
var model = this.InstanceSelector();
foreach (var property in this.PropertyChain)
{
yield return new ModelPropertyPair(model, property);
model = model == null ? null : property.GetValue(model);
}
}
private object GetValueToValidate()
{
return this.ControlProperty.GetValue(this.ControlToValidate);
}
[DebuggerStepThrough]
private string GetValidationErrorOrNull(ModelPropertyPair pair, object value)
{
var context = new ValidationContext(pair.Model) { MemberName = pair.Property.Name };
try
{
Validator.ValidateValue(value, context, this.ValidationAttributes);
return null;
}
catch (ValidationException ex)
{
return ex.Message;
}
}
[DebuggerStepThrough]
private bool TryConvertValue(object rawValue, out object convertedValue)
{
if (rawValue != null &&
rawValue.GetType() == this.PropertyChain.Last().PropertyType)
{
convertedValue = rawValue;
return true;
}
try
{
convertedValue = this.Converter.ConvertFrom(rawValue);
return true;
}
catch (Exception ex)
{
// HACK: There is a bug in the .NET framework BaseNumberConverter class.
// The class throws an Exception base class, and therefore we must catch
// the 'Exception' base class :-(.
convertedValue = null;
return false;
}
}
private class ModelPropertyPair
{
public readonly object Model;
public readonly PropertyInfo Property;
public ModelPropertyPair(object model, PropertyInfo property)
{
this.Model = model;
this.Property = property;
}
}
}
I think the way to go is to hook the Validating event for each of the controls on your forms. Then, in those handlers, implement your custom validation such as calling the DataAnnotations Validator.
Raising error flags would then be as simple as calling the ErrorProvider's SetError method if validation returns a failure.
Also, I'm sure with some clever coding on your part, you could funnel all your controls to a single Validating event handler so you would probably be able to avoid creating a separate event handler for each and every control you have.
Related
I am working setting up INotifyDataErrorInfo on a view model, to handle validation with attributes.
I have it working fine in the UI, the text box gets a nice red boarder and the mouse over event says what is wrong.
But I can not work out how in the ViewModel to work out the view model is valid. I am guessing I have to set up the HasErrors. In the examples I have seen they have a variable
private Dictionary<string, List<string>> _PropertyErrors = new Dictionary<string, List<string>>();
But then do nothing to set it.
I would like to check in the Save() method if the view model is valid.
public class CustomerViewModel : EntityBase, INotifyPropertyChanged, INotifyDataErrorInfo
{
public CustomerViewModel ()
{
//SET UP
}
private string _HomePhone;
[Required]
public string HomePhone
{
get { return _HomePhone; }
set
{
if (_HomePhone != value)
{
_HomePhone = value;
PropertyChanged(this, new PropertyChangedEventArgs("HomePhone"));
}
}
}
private void Save()
{
//Break point here
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public bool HasErrors
{
get { return true; }
}
public IEnumerable GetErrors(string propertyName)
{
return null;
}
You can check the HasErrors property.
This is an example implementation of INotifyDataErrorInfo with ValidationAttribute support and providing an example TrySave(), which checks if the view model has any validation errors:
public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
// Usage example property which validates its value
// before applying it using a Lambda expression.
// Example uses System.ValueTuple.
private string userInput;
public string UserInput
{
get => this.userInput;
set
{
// Use Lambda
if (ValidateProperty(value, newValue => newValue.StartsWith("#") ? (true, string.Empty) : (false, "Value must start with '#'.")))
{
this.userInput = value;
OnPropertyChanged();
}
}
}
// Alternative usage example property which validates its value
// before applying it using a Method group.
// Example uses System.ValueTuple.
private string userInputAlternativeValidation;
public string UserInputAlternativeValidation
{
get => this.userInputAlternativeValidation;
set
{
// Use Method group
if (ValidateProperty(value, AlternativeValidation))
{
this.userInputAlternativeValidation = value;
OnPropertyChanged();
}
}
}
private (bool IsValid, string ErrorMessage) AlternativeValidation(string value)
{
return value.StartsWith("#")
? (true, string.Empty)
: (false, "Value must start with '#'.");
}
// Alternative usage example property which validates its value
// before applying it using a ValidationAttribute.
private string userInputAttributeValidation;
[Required(ErrorMessage = "Value is required.")]
public string UserInputAttributeValidation
{
get => this.userInputAttributeValidation;
set
{
// Use only the attribute (can be combined with a Lambda or Method group)
if (ValidateProperty(value))
{
this.userInputAttributeValidation = value;
OnPropertyChanged();
}
}
}
private bool TrySave()
{
if (this.HasErrors)
{
return false;
}
// View model has no errors. Save data.
return true;
}
// Constructor
public ViewModel()
{
this.Errors = new Dictionary<string, List<string>>();
}
// Example uses System.ValueTuple
public bool ValidateProperty(object value, Func<object, (bool IsValid, string ErrorMessage)> validationDelegate = null, [CallerMemberName] string propertyName = null)
{
// Clear previous errors of the current property to be validated
this.Errors.Remove(propertyName);
OnErrorsChanged(propertyName);
// First validate using the delegate
(bool IsValid, string ErrorMessage) validationResult = validationDelegate?.Invoke(value) ?? (true, string.Empty);
if (!validationResult.IsValid)
{
AddError(propertyName, validationResult.ErrorMessage);
}
// Check if property is decorated with validation attributes
// using reflection
IEnumerable<Attribute> validationAttributes = GetType()
.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
?.GetCustomAttributes(typeof(ValidationAttribute)) ?? new List<Attribute>();
// Validate attributes if present
if (validationAttributes.Any())
{
var validationResults = new List<ValidationResult>();
if (!Validator.TryValidateProperty(value, new ValidationContext(this, null, null) { MemberName = propertyName }, validationResults))
{
foreach (ValidationResult attributeValidationResult in validationResults)
{
AddError(propertyName, attributeValidationResult.ErrorMessage);
}
validationResult = (false, string.Empty);
}
}
return validationResult.IsValid;
}
// Adds the specified error to the errors collection if it is not
// already present, inserting it in the first position if 'isWarning' is
// false. Raises the ErrorsChanged event if the Errors collection changes.
public void AddError(string propertyName, string errorMessage, bool isWarning = false)
{
if (!this.Errors.TryGetValue(propertyName, out List<string> propertyErrors))
{
propertyErrors = new List<string>();
this.Errors[propertyName] = propertyErrors;
}
if (!propertyErrors.Contains(errorMessage))
{
if (isWarning)
{
// Move warnings to the end
propertyErrors.Add(errorMessage);
}
else
{
propertyErrors.Insert(0, errorMessage);
}
OnErrorsChanged(propertyName);
}
}
public bool PropertyHasErrors(string propertyName) => this.Errors.TryGetValue(propertyName, out List<string> propertyErrors) && propertyErrors.Any();
#region INotifyDataErrorInfo implementation
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
// Returns all errors of a property. If the argument is 'null' instead of the property's name,
// then the method will return all errors of all properties.
public System.Collections.IEnumerable GetErrors(string propertyName)
=> string.IsNullOrWhiteSpace(propertyName)
? this.Errors.SelectMany(entry => entry.Value)
: this.Errors.TryGetValue(propertyName, out IEnumerable<string> errors)
? errors
: new List<string>();
// Returns if the view model has any invalid property
public bool HasErrors => this.Errors.Any();
#endregion
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
#endregion
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnErrorsChanged(string propertyName)
{
this.ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
// Maps a property name to a list of errors that belong to this property
private Dictionary<String, List<String>> Errors { get; }
}
This link contains an explanation and links to more examples: How to add validation to view model properties or how to implement INotifyDataErrorInfo
I need binding between two similar objects (C#):
public class TypeA
{
public int I;
public string S;
}
public class TypeB
{
public IntField I;
public StringField S;
}
When a field in TypeA changes I need to update the matching field in TypeB.
IntField is an object that has a Value field of int type, so that updating TypeB can be written as:
bInstance.I.Value = aInstance.I;
If I understand correctly, if I use INotifyPropertyChanged in order to bind TypeB to TypeA, it'll cause boilerplate:
aInstance.PropertyChanged += (sender, args) =>
{
if (args.PropertyName == "I")
this.I.Value = sender.I;
if (args.PropertyName == "S")
this.S.Value = sender.S;
};
Also:
I have access to the code in both types, and I'd rather not change TypeB.
I have ~15 pairs of types like TypeA and TypeB - I'd like to avoid boilerplate.
Performance is very important, so reflection is not a preferred option.
Perhaps static reflection is an option? I've heard about it, but I'm not sure about:
How to use it without boilerplate.
Its performance.
Using it for different instances of pairs of the same type (i.e. a1Instance->b1Instance, a2Intance->b2Instance, etc.).
Edit:
IntField is a class.
It's used for another type of data binding that exists in the system (complex, and the entire system relies on this). It inherits from a class that represents a general bindable field. here's part of it:
public class IntField : GeneralField
{
private int _value;
public int Value
{
get { return _value; }
set
{
IsDirty = true;
_value = value;
}
}
// ... a couple of abstract method implementations go here (setting _value, and getting value in a non-type specific way)
}
If you don't want lots of manual coding, something reflection-based or meta-programming-based is going to be your best bet. For example:
static void Entwine(INotifyPropertyChanged source, object target)
{
source.PropertyChanged += (sender,args) =>
{
var prop = target.GetType().GetProperty(args.PropertyName);
if(prop != null)
{
var field = prop.GetValue(target) as GeneralField;
if(field != null)
{
var newVal = source.GetType().GetProperty(args.PropertyName)
.GetValue(source);
field.SetValue(newVal); // <=== some method on GeneralField
}
}
};
}
In many cases this will be fine, but if the reflection is genuinely a problem, tools like FastMember can help:
static void Entwine(INotifyPropertyChanged source, object target)
{
var sourceAccessor = ObjectAccessor.Create(source);
var targetAccessor = ObjectAccessor.Create(target);
source.PropertyChanged += (sender, args) =>
{
var field = targetAccessor[args.PropertyName] as GeneralField;
if (field != null)
{
var newVal = sourceAccessor[args.PropertyName];
field.SetValue(newVal);
}
};
}
This is significantly faster than reflection - it uses a lot of tricks to avoid pain. That just leaves the need for something like:
abstract class GeneralField
{
// ...
public abstract void SetValue(object value);
}
class Int32Field : GeneralField
{
// ...
public override void SetValue(object value)
{
Value = (int)value;
}
}
And of course your INotifyPropertyChanged implementation, for example:
public class TypeA : INotifyPropertyChanged
{
private int i;
private string s;
public int I
{
get { return i; }
set { SetField(ref i, value); }
}
public string S
{
get { return s; }
set { SetField(ref s, value); }
}
private void SetField<T>(ref T field, T value,
[CallerMemberName]string propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
var handler = PropertyChanged;
if (handler != null) handler(
this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
I have a DataTable with complex objects.
For example,
class ComplexDataWrapper
{
public string Name{ get; set; }
public ComplexData Data{ get; set; }
public ComplexDataWrapper(ComplexData data)
{
this.Data = data;
this.Name = "Something";
}
public override string ToString()
{
return Name;
}
}
And now I want to bind cells from DataTable to objects of ComplexDataWrapper
So, I try something like this :
...
var column = new DataColumn() { ColumnName = columnName, DataType = typeof(ComplexDataWrapper)};
row[column] = new ComplexDataWrapper(data);
But, I want to bind for only one property, for example, Name.
And in the gridview (DataTable is a data source for this view) I want to edit this property(Name).
var complexDataWrapper = row[column] as ComplexDataWrapper;
complexDataWrapper always equals to NULL.
I know that I miss something.
So my questions : How I can bind my cell of DataTable to complex object? Plus in grid view I want to edit exactly one property of complex object.
Thanks. Hopefully, everything is clear.
So my questions : How I can bind my cell of DataTable to complex object? Plus in grid view I want to edit exactly one property of complex object.
What you need here is the ability to bind to a so called property path (e.g. obj.Prop1.Prop2). Unfortunately WinForms has limited support for that - it's supported for simple data binding (like control.DataBindings.Add(...)) but not for list data binding which is used by DataGridView control and similar.
Fortunately it's still doable with some (most of the time trivial) coding, because the data binding is build around an abstraction called PropertyDescriptor. By default it is implemented via reflection, but nothing prevents you to create your own implementation and do whatever you like inside it. That allows you to do many things that are not possible with reflection, in particular to simulate "properties" that actually do not exist.
Here we will utilize that possibility to create a "property" that actually gets/sets its value from a child property of the original property, while from outside it still looks like a single property, thus allowing to data bind to it:
public class ChildPropertyDescriptor : PropertyDescriptor
{
public static PropertyDescriptor Create(PropertyDescriptor sourceProperty, string childPropertyPath, string displayName = null)
{
var propertyNames = childPropertyPath.Split('.');
var propertyPath = new PropertyDescriptor[1 + propertyNames.Length];
propertyPath[0] = sourceProperty;
for (int i = 0; i < propertyNames.Length; i++)
propertyPath[i + 1] = propertyPath[i].GetChildProperties()[propertyNames[i]];
return new ChildPropertyDescriptor(propertyPath, displayName);
}
private ChildPropertyDescriptor(PropertyDescriptor[] propertyPath, string displayName)
: base(propertyPath[0].Name, null)
{
this.propertyPath = propertyPath;
this.displayName = displayName;
}
private PropertyDescriptor[] propertyPath;
private string displayName;
private PropertyDescriptor RootProperty { get { return propertyPath[0]; } }
private PropertyDescriptor ValueProperty { get { return propertyPath[propertyPath.Length - 1]; } }
public override Type ComponentType { get { return RootProperty.ComponentType; } }
public override bool IsReadOnly { get { return ValueProperty.IsReadOnly; } }
public override Type PropertyType { get { return ValueProperty.PropertyType; } }
public override bool CanResetValue(object component) { var target = GetTarget(component); return target != null && ValueProperty.CanResetValue(target); }
public override object GetValue(object component) { var target = GetTarget(component); return target != null ? ValueProperty.GetValue(target) : null; }
public override void ResetValue(object component) { ValueProperty.ResetValue(GetTarget(component)); }
public override void SetValue(object component, object value) { ValueProperty.SetValue(GetTarget(component), value); }
public override bool ShouldSerializeValue(object component) { var target = GetTarget(component); return target != null && ValueProperty.ShouldSerializeValue(target); }
public override AttributeCollection Attributes { get { return ValueProperty.Attributes; } }
public override string Category { get { return ValueProperty.Category; } }
public override TypeConverter Converter { get { return ValueProperty.Converter; } }
public override string Description { get { return ValueProperty.Description; } }
public override bool IsBrowsable { get { return ValueProperty.IsBrowsable; } }
public override bool IsLocalizable { get { return ValueProperty.IsLocalizable; } }
public override string DisplayName { get { return displayName ?? RootProperty.DisplayName; } }
public override object GetEditor(Type editorBaseType) { return ValueProperty.GetEditor(editorBaseType); }
public override PropertyDescriptorCollection GetChildProperties(object instance, Attribute[] filter) { return ValueProperty.GetChildProperties(GetTarget(instance), filter); }
public override bool SupportsChangeEvents { get { return ValueProperty.SupportsChangeEvents; } }
public override void AddValueChanged(object component, EventHandler handler)
{
var target = GetTarget(component);
if (target != null)
ValueProperty.AddValueChanged(target, handler);
}
public override void RemoveValueChanged(object component, EventHandler handler)
{
var target = GetTarget(component);
if (target != null)
ValueProperty.RemoveValueChanged(target, handler);
}
private object GetTarget(object source)
{
var target = source;
for (int i = 0; target != null && target != DBNull.Value && i < propertyPath.Length - 1; i++)
target = propertyPath[i].GetValue(target);
return target != DBNull.Value ? target : null;
}
}
The code is not so small, but all it does is basically delegating the calls to the corresponding methods of the property descriptor chain representing the path from the original property to the child property. Also please note that many methods of the PropertyDescriptor are used only during the design time, so creating a custom concrete runtime property descriptor usually needs only to implement ComponentType, PropertyType, GetValue and SetValue (if supported).
So far so good. This is just the first part of the puzzle. We can create a "property", now we need a way to let data binding use it.
In order to do that, we'll utilize another data binding related interface called ITypedList:
Provides functionality to discover the schema for a bindable list, where the properties available for binding differ from the public properties of the object to bind to.
In other words, it allows us to provide "properties" for the list elements. But how? If we were implementing the data source list, it would be easy. But here we want to do that for a list that we don't know in advance (I'm trying the keep the solution generic).
The solutions is to wrap the original list in onother one that will implement IList (the minimum requirement for list data binding) by delegating all the calls to the underlying list, but by implementing ITypedList will control the properties used for binding:
public static class ListDataView
{
public static IList Create(object dataSource, string dataMember, Func<PropertyDescriptor, PropertyDescriptor> propertyMapper)
{
var source = (IList)ListBindingHelper.GetList(dataSource, dataMember);
if (source == null) return null;
if (source is IBindingListView) return new BindingListView((IBindingListView)source, propertyMapper);
if (source is IBindingList) return new BindingList((IBindingList)source, propertyMapper);
return new List(source, propertyMapper);
}
private class List : IList, ITypedList
{
private readonly IList source;
private readonly Func<PropertyDescriptor, PropertyDescriptor> propertyMapper;
public List(IList source, Func<PropertyDescriptor, PropertyDescriptor> propertyMapper) { this.source = source; this.propertyMapper = propertyMapper; }
// IList
public object this[int index] { get { return source[index]; } set { source[index] = value; } }
public int Count { get { return source.Count; } }
public bool IsFixedSize { get { return source.IsFixedSize; } }
public bool IsReadOnly { get { return source.IsReadOnly; } }
public bool IsSynchronized { get { return source.IsSynchronized; } }
public object SyncRoot { get { return source.SyncRoot; } }
public int Add(object value) { return source.Add(value); }
public void Clear() { source.Clear(); }
public bool Contains(object value) { return source.Contains(value); }
public void CopyTo(Array array, int index) { source.CopyTo(array, index); }
public IEnumerator GetEnumerator() { return source.GetEnumerator(); }
public int IndexOf(object value) { return source.IndexOf(value); }
public void Insert(int index, object value) { source.Insert(index, value); }
public void Remove(object value) { source.Remove(value); }
public void RemoveAt(int index) { source.RemoveAt(index); }
// ITypedList
public string GetListName(PropertyDescriptor[] listAccessors) { return ListBindingHelper.GetListName(source, listAccessors); }
public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
{
var properties = ListBindingHelper.GetListItemProperties(source, listAccessors);
if (propertyMapper != null)
properties = new PropertyDescriptorCollection(properties.Cast<PropertyDescriptor>()
.Select(propertyMapper).Where(p => p != null).ToArray());
return properties;
}
}
private class BindingList : List, IBindingList
{
private IBindingList source;
public BindingList(IBindingList source, Func<PropertyDescriptor, PropertyDescriptor> propertyMapper) : base(source, propertyMapper) { this.source = source; }
private ListChangedEventHandler listChanged;
public event ListChangedEventHandler ListChanged
{
add
{
var oldHandler = listChanged;
if ((listChanged = oldHandler + value) != null && oldHandler == null)
source.ListChanged += OnListChanged;
}
remove
{
var oldHandler = listChanged;
if ((listChanged = oldHandler - value) == null && oldHandler != null)
source.ListChanged -= OnListChanged;
}
}
private void OnListChanged(object sender, ListChangedEventArgs e)
{
var handler = listChanged;
if (handler != null)
handler(this, e);
}
public bool AllowNew { get { return source.AllowNew; } }
public bool AllowEdit { get { return source.AllowEdit; } }
public bool AllowRemove { get { return source.AllowRemove; } }
public bool SupportsChangeNotification { get { return source.SupportsChangeNotification; } }
public bool SupportsSearching { get { return source.SupportsSearching; } }
public bool SupportsSorting { get { return source.SupportsSorting; } }
public bool IsSorted { get { return source.IsSorted; } }
public PropertyDescriptor SortProperty { get { return source.SortProperty; } }
public ListSortDirection SortDirection { get { return source.SortDirection; } }
public object AddNew() { return source.AddNew(); }
public void AddIndex(PropertyDescriptor property) { source.AddIndex(property); }
public void ApplySort(PropertyDescriptor property, ListSortDirection direction) { source.ApplySort(property, direction); }
public int Find(PropertyDescriptor property, object key) { return source.Find(property, key); }
public void RemoveIndex(PropertyDescriptor property) { source.RemoveIndex(property); }
public void RemoveSort() { source.RemoveSort(); }
}
private class BindingListView : BindingList, IBindingListView
{
private IBindingListView source;
public BindingListView(IBindingListView source, Func<PropertyDescriptor, PropertyDescriptor> propertyMapper) : base(source, propertyMapper) { this.source = source; }
public string Filter { get { return source.Filter; } set { source.Filter = value; } }
public ListSortDescriptionCollection SortDescriptions { get { return source.SortDescriptions; } }
public bool SupportsAdvancedSorting { get { return source.SupportsAdvancedSorting; } }
public bool SupportsFiltering { get { return source.SupportsFiltering; } }
public void ApplySort(ListSortDescriptionCollection sorts) { source.ApplySort(sorts); }
public void RemoveFilter() { source.RemoveFilter(); }
}
}
Actually as you can see, I've added wrappers for other data source interfaces like IBindingList and IBindingListView. Again, the code is not so small, but it's just delegating the calls to the underlying objects (when creating one for your concrete data, you usually would inherit from List<T> or BiundingList<T> and implement only the two ITypedList members). The essential part is the GetItemProperties method implementation which along with the propertyMapper lambda allows you to replace one property with another.
With all that in place, solving the specific problem from the post is simple a matter of wrapping the DataTable and mapping the Complex property to the Complex.Name property:
class ComplexData
{
public int Value { get; set; }
}
class ComplexDataWrapper
{
public string Name { get; set; }
public ComplexData Data { get; set; } = new ComplexData();
}
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var form = new Form();
var gridView = new DataGridView { Dock = DockStyle.Fill, Parent = form };
gridView.DataSource = ListDataView.Create(GetData(), null, p =>
{
if (p.PropertyType == typeof(ComplexDataWrapper))
return ChildPropertyDescriptor.Create(p, "Name", "Complex Name");
return p;
});
Application.Run(form);
}
static DataTable GetData()
{
var dt = new DataTable();
dt.Columns.Add("Id", typeof(int));
dt.Columns.Add("Complex", typeof(ComplexDataWrapper));
for (int i = 1; i <= 10; i++)
dt.Rows.Add(i, new ComplexDataWrapper { Name = "Name#" + i, Data = new ComplexData { Value = i } });
return dt;
}
}
To recap, custom PropertyDescriptor and ITypedList allow you to create unlimited types of views of your data, which then can be used by any data bound aware control.
I believe your architecture is flawed for what you're trying to achieve.
If you are using the gridView to edit a single property on your complex type, then there is no need to bind the entire type into the datatable the is the datasource of your grid.
Instead you should bind only the property you wish to edit, and when the the data comes back, simply assign it to the complex type in the right place.
(.NET 4.0/WebForms/EF 4.1 POCO)
Hi,
I´m using a Custom Validator to use DataAnnotations with WebForms (source code is bellow).
Everything goes fine when I use DataAnnotations directly in the generated classes. But when I use the DataAnnotations in a Metadata class with a partial class, the DataAnnotations attributes seems to be bypassed in the validation. I know that the metadata was properly recognized, because when I save the data in the DbContext it is being validated and EntityValidationErrors returns the validated errors.
I did some searches and found this: (http://stackoverflow.com/questions/2657358/net-4-rtm-metadatatype-attribute-ignored-when-using-validator/2657644#2657644). Unfortunately my implementation did not worked. May be I don´t know where to call it. I´ve tried to call it in the constructor of the Metadata class but it didn´t work.
public static class MetadataTypesRegister
{
static bool installed = false;
static object installedLock = new object();
public static void Install()
{
if (installed)
{
return;
}
lock (installedLock)
{
if (installed)
{
return;
}
// TODO: for debug purposes only (please remove in production)
Assembly assembly = Assembly.GetExecutingAssembly();
Type[] types = assembly.GetTypes();
//------
foreach (Type type in Assembly.GetExecutingAssembly().GetTypes())
{
foreach (MetadataTypeAttribute attrib in type.GetCustomAttributes(typeof(MetadataTypeAttribute), true))
{
TypeDescriptor.AddProviderTransparent(
new AssociatedMetadataTypeTypeDescriptionProvider(type, attrib.MetadataClassType), type);
}
}
installed = true;
}
}
}
The model to be validated is located in DataLayer.dll and the DataAnnotationsValidator class is in Common.dll.
This is my DataAnnotationsValidator class:
[ToolboxData("<{0}:DataAnnotationsValidator runat=server></{0}:DataAnnotationsValidator>")]
public class DataAnnotationsValidator : BaseValidator
{
private string _propertyName = string.Empty;
public string PropertyName
{
get { return _propertyName; }
set { _propertyName = value; }
}
public string _sourceType = string.Empty;
public string SourceType
{
get { return _sourceType; }
set { _sourceType = value; }
}
public ValidationDataType _type = ValidationDataType.String;
public ValidationDataType Type
{
get { return _type; }
set { _type = value; }
}
public string _cssError = string.Empty;
public string CssError
{
get { return _cssError; }
set { _cssError = value; }
}
protected override bool EvaluateIsValid()
{
// get specified type for reflection
Type objectType = System.Type.GetType(_sourceType, true, true);
// get a property to validate
PropertyInfo prop = objectType.GetProperty(_propertyName);
// get the control to validate
TextBox control = this.FindControl(this.ControlToValidate) as TextBox;
object valueToValidate = null;
if (control.Text != String.Empty)
{
if (Type == ValidationDataType.Double)
valueToValidate = double.Parse(control.Text);
else if (Type == ValidationDataType.Integer)
valueToValidate = int.Parse(control.Text);
else if (Type == ValidationDataType.Date)
valueToValidate = DateTime.Parse(control.Text);
else if (Type == ValidationDataType.Currency)
valueToValidate = decimal.Parse(control.Text);
else
valueToValidate = control.Text;
}
CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture;
bool result = true;
try
{
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
// The custom validator can return only one error message. Because the field model being validated can have more than
// one DataAnnotation validation (Required, Range, RegularExpression, etc.) the DataAnnotationsValidator will return only the first
// error message that it evaluates.
foreach (ValidationAttribute attr in prop.GetCustomAttributes(typeof(ValidationAttribute), true).OfType<ValidationAttribute>())
{
Thread.CurrentThread.CurrentCulture = currentCulture;
if (!attr.IsValid(valueToValidate))
{
result = false;
var displayNameAttr = prop.GetCustomAttributes(typeof(DisplayNameAttribute), true).OfType<DisplayNameAttribute>().FirstOrDefault();
string displayName = displayNameAttr == null ? prop.Name : displayNameAttr.DisplayName;
ErrorMessage = attr.FormatErrorMessage(displayName);
break;
}
}
}
finally
{
Thread.CurrentThread.CurrentCulture = currentCulture;
if (result)
{
if (!string.IsNullOrEmpty(CssError))
control.RemoveCssClass(CssError);
}
else
{
if (!string.IsNullOrEmpty(CssError))
control.AddCssClass(CssError);
}
}
return result;
}
}
Thanks!!
I´ve found a solution here (http://stackoverflow.com/questions/5600707/how-do-you-do-web-forms-model-validation).
I modify the code of EvaluateIsValid method to include the code bellow to looking for Metadata Attributes:
// get specified type for reflection
Type objectType = System.Type.GetType(_sourceType, true, true);
// check for the types that have MetadataType attribute because
// it is they who have the DataAnnotations attributes
IEnumerable<MetadataTypeAttribute> mt = objectType.GetCustomAttributes(typeof(MetadataTypeAttribute), false).OfType<MetadataTypeAttribute>();
if (mt.Count() > 0)
{
objectType = mt.First().MetadataClassType;
}
And everything goes fine!
I need to call SetSettings() and using the 3 elements in splitSettings, set EncodeAudio to False.
How would I go about doing that? Convert the property of a object to who's name I have in a string.
I realize I could do with with a switch statement of all my settings but there has to be a more dynamic way to go about doing this.
namespace SettingsLib
{
public class Settings
{
public Boolean EncodeAudio { get; set; }
}
}
namespace Service
{
void SetSettings()
{
string[] splitSettings = { "SettingsLib.Settings", "EncodeAudio", "False" };
// Need to set EncodeAudio to False in SettingsLib.Settings
}
}
Yes I have a instance of Settings
Say:
Settings settingManager = new Settings();
I am trying to do is dynamically set EncodeAudo to False by using elements of splitSettings
settingManager.EncodeAudio = False;
Thanks to the help of TBohnen.jnr
I came to this answer:
public void setProperty(object containingObject, string propertyName, object newValue)
{
foreach (PropertyInfo p in containingObject.GetType().GetProperties())
{
if (p.Name == propertyName)
{
p.SetValue(containingObject, Convert.ChangeType(newValue, p.PropertyType), null);
}
}
}
EDIT Tested it with int, bool, double and string and it worked, also added a check to make sure that the property exists and throws an exception of it doesn't (Might want to change Exception type)
EDIT 2: Temporary solution, will add more typenames to the convert method or alternatively if somebody can suggest a more dynamic way of casting it (If not then I assume you will have to know all of the types that will be used)?
EDIT3 Stole the convert method from another answer in question (Chris Taylor ), thanks :-)
public void setProperty(object containingObject, string propertyName, object newValue)
{
if (containingObject.GetType().GetProperties().Count(c => c.Name == propertyName) > 0)
{
var type = containingObject.GetType().GetProperties().First(c => c.Name == propertyName).PropertyType;
object val = Convert(type,(string)newValue);
containingObject.GetType().InvokeMember(propertyName, BindingFlags.SetProperty, null, containingObject, new object[] { val });
}
else
{
throw new KeyNotFoundException("The property: " + propertyName + " was not found in: " + containingObject.GetType().Name);
}
}
public object convert(System.Type type, string value)
{
return Convert.ChangeType(value, type);
}
Taken from http://www.haslo.ch/blog/setproperty-and-getproperty-with-c-reflection/
Was interested to see if this works, create a quick test:
class testSettings
{
public bool SetBool { get; set; }
public void setProperty(object containingObject, string propertyName, object newValue)
{
if (containingObject.GetType().GetProperties().Count(c => c.Name == propertyName) > 0)
{
containingObject.GetType().InvokeMember(propertyName, BindingFlags.SetProperty, null, containingObject, new object[] { newValue });
}
else
{
throw new KeyNotFoundException("The property: " + propertyName + " was not found in: " + containingObject.GetType().Name);
}
}
}
static void Main(string[] args)
{
testSettings ts = new testSettings();
ts.SetBool = false;
ts.setProperty(ts, "SetBool", true);
Console.WriteLine(ts.SetBool.ToString());
Console.Read();
}
The output is true, not entirely sure if it will convert all types correctly though.
As others have mentioned, you should consider making your SettingsLib class static. And you might also need to handle the conversion of values from strings to the target types. Here is a simple example how this would work.
namespace Service
{
class Program
{
static void Main(string[] args)
{
string[] splitSettings = { "SettingsLib.Settings", "EncodeAudio", "False" };
SetProperty(splitSettings[0], splitSettings[1], splitSettings[2]);
}
static void SetProperty(string typeName, string propertyName, object value)
{
var type = Type.GetType(typeName);
if (type == null)
{
throw new ArgumentException("Unable to get type", "typeName");
}
var pi = type.GetProperty(propertyName);
if (pi == null)
{
throw new ArgumentException("Unable to find property on type", "propertyName");
}
object propertyValue = value;
if (propertyValue != null)
{
// You might need more elaborate testing here to ensure that you can handle
// all the various types, you might need to special case some types here
// but this will work for the basics.
if (pi.PropertyType != propertyValue.GetType())
{
propertyValue = Convert.ChangeType(propertyValue, pi.PropertyType);
}
}
pi.SetValue(null, propertyValue, null);
}
}
}
namespace SettingsLib
{
public static class Settings
{
public static bool EncodeAudio { get; set; }
}
}
Maybe you should mark your settable properties as static and then try to set the values using Reflection:
namespace SettingsLib
{
public static class Settings
{
public static bool EncodeAudio { get; set; }
}
}
namespace Service
{
void SetSettings()
{
string[] splitSettings = { "SettingsLib.Settings", "EncodeAudio", "False" };
dynamic property = Type.GetType(splitSettings[0]).GetProperty(splitSettings[1]);
property = splitSettings[2];
}
}