Does anybody know of a good algorithm to mutually exclusively check two properties using a ModelValidator?
Something like:
[EitherPropertyRequired("BuildingNumber","BuildingName"]
public class Address{
public int BuildingNumber { get; set; }
public string BuildingName { get; set; }
}
I ended up creating an attribute and manually checking it with a custom ModelValidator. This custom model validator is checked using an AssociatedValidatorProvider which is registered in Application_Start().
protected void Application_Start()
{
ModelValidatorProviders.Providers.Add(new ZipValidationProvider());
}
public class ZipValidationProvider:AssociatedValidatorProvider
{
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
{
foreach (var attribute in attributes.OfType<EitherPropertyRequiredAttribute>())
{
yield return new EitherPropertyRequiredValidator(metadata,
context, attribute.FirstProperty, attribute.SecondProperty, attribute.Message);
}
}
}
[AttributeUsage(AttributeTargets.Class)]
public class EitherPropertyRequiredAttribute : Attribute
{
public readonly string FirstProperty;
public readonly string SecondProperty;
public readonly string Message;
public EitherPropertyRequiredAttribute(string firstProperty, string secondProperty,
string message)
{
FirstProperty = firstProperty;
SecondProperty = secondProperty;
Message = message;
}
}
public class EitherPropertyRequiredValidator:ModelValidator
{
private readonly string firstProperty;
private readonly string secondProperty;
private readonly string message;
public EitherPropertyRequiredValidator(ModelMetadata metadata,
ControllerContext context,
string firstProperty,
string secondProperty,
string message)
:base(metadata,context)
{
this.firstProperty = firstProperty;
this.secondProperty = secondProperty;
this.message = message;
}
private PropertyInfo GetPropertyInfoRecursive(Type type, string property)
{
var prop = type.GetProperty(property);
if (prop != null) return prop;
foreach (var p in type.GetProperties())
{
if (p.PropertyType.Assembly == typeof (object).Assembly)
continue;
return GetPropertyInfoRecursive(p.PropertyType, property);
}
return null;
}
private object GetPropertyValueRecursive(object obj, PropertyInfo propertyInfo)
{
Type objectType = obj.GetType();
if(objectType.GetProperty(propertyInfo.Name) != null)
return propertyInfo.GetValue(obj, null);
foreach (var p in objectType.GetProperties())
{
if (p.PropertyType.Assembly == typeof(object).Assembly)
continue;
var o = p.GetValue(obj,null);
return GetPropertyValueRecursive(o, propertyInfo);
}
return null;
}
public override IEnumerable<ModelValidationResult> Validate(object container)
{
if (Metadata.Model == null)
yield break;
var firstPropertyInfo = GetPropertyInfoRecursive(Metadata.Model.GetType(),firstProperty);
if(firstPropertyInfo == null)
throw new InvalidOperationException("Unknown property:" + firstProperty);
var secondPropertyInfo = GetPropertyInfoRecursive(Metadata.Model.GetType(),secondProperty);
if(secondPropertyInfo == null)
throw new InvalidOperationException("Unknown property:" + secondProperty);
var firstPropertyValue = GetPropertyValueRecursive(Metadata.Model, firstPropertyInfo);
var secondPropertyValue = GetPropertyValueRecursive(Metadata.Model, secondPropertyInfo);
bool firstPropertyIsEmpty = firstPropertyValue == null ||
firstPropertyValue.ToString().Length == 0;
bool secondPropertyIsEmpty = secondPropertyValue == null ||
secondPropertyValue.ToString().Length == 0;
if (firstPropertyIsEmpty && secondPropertyIsEmpty)
{
yield return new ModelValidationResult
{
MemberName = firstProperty,
Message = message
};
}
}
}
[AttributeUsage(AttributeTargets.Class)]
public class EitherPropertyRequiredAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
// value will be the model
Address address = (Address)value;
// TODO: Check the properties of address here and return true or false
return true;
}
}
You could make this more generic by avoiding it casting to Address and using attribute properties and reflection.
Related
I have multiple DTO class which require type converter. The following is one of the implementations. As you will see, I need ConvertFrom only.
public class EmployeeFilterTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (typeof(string) == sourceType)
return true;
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var strVal = value as String;
if (string.IsNullOrEmpty(strVal))
return new EmployeeFilter();
EmployeeFilter employeeFilter = new EmployeeFilter();
string[] filters = strVal.Split(';');
foreach (var filter in filters)
{
var filterSplit = filter.Split(':');
if (filterSplit.Length == 2)
{
var key = filterSplit[0];
var val = filterSplit[1];
SetPropertyValue(employeeFilter, key, val);
}
}
return employeeFilter;
}
private void SetPropertyValue(EmployeeFilter employeeFilter, string key, string val)
{
var t = typeof(EmployeeFilter);
PropertyInfo[] props = t.GetProperties(BindingFlags.Instance | BindingFlags.Public);
PropertyInfo prop = props.Where(p => p.Name.Equals(key, StringComparison.CurrentCultureIgnoreCase) == true && p.CanWrite).FirstOrDefault();
if (prop != null)
prop.SetValue(employeeFilter, val);
}
}
I want to make multiple DTOs sharing the same converter in hopes of reducing code duplication as well as tests and after some researches, I have 2 problems at hand
Get the type that I want to convert in ConvertFrom method
Using Type class to initialize new object
For the first one, I don't know how to get from ITypeDescriptorContext.
For the second one, I will use the following according to this post
Type employeeType = typeof(EmployeeFilter);
object objtype = Activator.CreateInstance(employeeType);
So, how to get the type that I want to convert to?
Test case
public class testConverter
{
[Theory]
[InlineData(typeof(string), true)]
[InlineData(typeof(int), false)]
public void testCanConvertFrom(Type sourceType, bool expected)
{
//Arrange
Type randomType = typeof(Book);
Type typeGenericConverter = typeof(EmployeeFilterTypeConverter<>);
Type typeActualConverter = typeGenericConverter.MakeGenericType(randomType);
/*
1. The way of creating EmployeeFilterTypeConverter<thattype>
https://stackoverflow.com/a/266282
*/
dynamic testConverter = Activator.CreateInstance(typeActualConverter);
Mock<ITypeDescriptorContext> mockDescContext = new Mock<ITypeDescriptorContext>();
//Act
bool actual = testConverter.CanConvertFrom(mockDescContext.Object, sourceType);
//Assert
Assert.Equal(expected, actual);
}
[Theory, ClassData(typeof(TestConvertFromType1))]
/*
1. All these classdata, propertydata stuff just for passing complex objects to test
https://stackoverflow.com/a/22093968
*/
public void testConverFromType1(object value, EmployeeFilter expected)
{
//api/employee?filter=firstName:Nikhil;lastName:Doomra
//Arrange
EmployeeFilterTypeConverter<EmployeeFilter> testConverter = new EmployeeFilterTypeConverter<EmployeeFilter>();
Mock<ITypeDescriptorContext> mockDescContext = new Mock<ITypeDescriptorContext>();
//Act
EmployeeFilter actual = testConverter.ConvertFrom(mockDescContext.Object, null, value) as EmployeeFilter;
//Assert
//public static void Equal<T>(T expected, T actual);
Assert.Equal(expected, actual);
}
[Theory, ClassData(typeof(TestConvertFromType2))]
public void testConverFromType2(object value, GeoPoint expected)
{
//api/employee?filter=firstName:Nikhil;lastName:Doomra
//Arrange
EmployeeFilterTypeConverter<GeoPoint> testConverter = new EmployeeFilterTypeConverter<GeoPoint>();
Mock<ITypeDescriptorContext> mockDescContext = new Mock<ITypeDescriptorContext>();
//Act
GeoPoint actual = testConverter.ConvertFrom(mockDescContext.Object, null, value) as GeoPoint;
//Assert
//public static void Equal<T>(T expected, T actual);
Assert.Equal(expected, actual);
}
}
Test Data Model
public class TestConvertFromType1: IEnumerable<object[]>
{
private readonly List<object[]> _data = new List<object[]>
{
new object[] { "firstName:Nikhil;lastName:Doomra",
new EmployeeFilter {
FirstName = "Nikhil", LastName = "Doomra"
}},
new object[] { "firstName:Nikhil",
new EmployeeFilter {
FirstName = "Nikhil"
}}
};
public IEnumerator<object[]> GetEnumerator()
{ return _data.GetEnumerator(); }
IEnumerator IEnumerable.GetEnumerator()
{ return GetEnumerator(); }
}
public class TestConvertFromType2 : IEnumerable<object[]>
{
private readonly List<object[]> _data = new List<object[]>
{
new object[] { "Latitude:12.345;Longitude:342.12",
new GeoPoint {
Latitude = 12.345, Longitude = 342.12
}},
new object[] { "Latitude:11.234;Longitude:345.12",
new GeoPoint {
Latitude = 11.234, Longitude = 345.12
}}
};
public IEnumerator<object[]> GetEnumerator()
{ return _data.GetEnumerator(); }
IEnumerator IEnumerable.GetEnumerator()
{ return GetEnumerator(); }
}
Generic Converter
public class EmployeeFilterTypeConverter<T> : TypeConverter where T: new()
/*
1. You can't declare T type = new T() without this constraint
Evidently it is because compiler can't say what is the type!
https://stackoverflow.com/a/29345294
*/
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (typeof(string) == sourceType)
return true;
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var strVal = value as String;
if (string.IsNullOrEmpty(strVal))
return new EmployeeFilter();
T converTo = new T();
string[] filters = strVal.Split(';');
foreach (var filter in filters)
{
string[] filterSplit = filter.Split(':');
if (filterSplit.Length == 2)
{
string key = filterSplit[0];
string val = filterSplit[1];
SetPropertyValue(converTo, key, val);
}
}
return converTo;
}
private void SetPropertyValue(T converTo, string key, string val)
{
Type t = typeof(T);
PropertyInfo[] props = t.GetProperties(BindingFlags.Instance | BindingFlags.Public);
PropertyInfo prop = props.Where(p => p.Name.Equals(key, StringComparison.CurrentCultureIgnoreCase) == true && p.CanWrite).FirstOrDefault();
if (prop is null) return;
prop.SetValue(converTo, TypeDescriptor.GetConverter(prop.PropertyType).ConvertFrom(val));
/*
1. Problem: val is a string and if your target property is non-string there is
a contradiction.
The following link offers a solution
https://stackoverflow.com/a/2380483
*/
}
}
EmployeeFilter
[TypeConverter(typeof(EmployeeFilterTypeConverter<EmployeeFilter>))]
public class EmployeeFilter: IEquatable<EmployeeFilter>
{
/*
1. As you can see, the DTO omitted the
a. ID
b. DOB
*/
public string FirstName { get; set; }
public string LastName { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
public DateTime? DOJ { get; set; }
public bool Equals(EmployeeFilter other)
{
/*
1. You need to put parenthesis around (this.FirstName == other.FirstName)
2. https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1?view=net-5.0
*/
return (this.FirstName == other.FirstName) &&
(this.LastName == other.LastName) &&
(this.Street == other.Street) &&
(this.City == other.City) &&
(this.State == other.State) &&
(this.ZipCode == other.ZipCode) &&
(this.DOJ == other.DOJ);
}
}
GeoPoint
public class GeoPoint: IEquatable<GeoPoint>
{
public double Latitude { get; set; }
public double Longitude { get; set; }
public static bool TryParse(string s, out GeoPoint result)
{
result = null;
var parts = s.Split(',');
if (parts.Length != 2)
{
return false;
}
double latitude, longitude;
if (double.TryParse(parts[0], out latitude) &&
double.TryParse(parts[1], out longitude))
{
result = new GeoPoint() { Longitude = longitude, Latitude = latitude };
return true;
}
return false;
}
public bool Equals(GeoPoint other)
{
return (this.Latitude == other.Latitude) && (this.Longitude == other.Longitude);
}
Edit: Added model classes
I create my own UniqueAttribute validation which check data in DB.
Problem is how to ignore own row.
So example:
Current email address: test#test.com Current address: Address1
now I change
Current address: Address2 Current email address: test#test.com ->
stays the same
I get message that this email already exist. How to remember old value?
Code:
#region
using Agado.Jobs.Infrastructure.ExtensionMethods;
using Agado.Jobs.StudentService.Web.Infrastructure.Utils;
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Reflection;
#endregion
namespace Agado.Jobs.StudentService.Web.Infrastructure.Validators
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class UniqueAttribute : ValidationAttribute
{
public UniqueAttribute(Type dataContextType, Type entityType, string propertyName, string methodName)
{
DataContextType = dataContextType;
EntityType = entityType;
PropertyName = propertyName;
MethodName = methodName;
}
public Type DataContextType { get; private set; }
public Type EntityType { get; private set; }
public string PropertyName { get; private set; }
public string MethodName { get; private set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null) return ValidationResult.Success;
var idValue = ValidationUtils.GetValue<int>(validationContext.ObjectInstance, "Id");
var repository = Jobs.Infrastructure.IoC.Resolve(DataContextType);
PropertyInfo propertyInfo = EntityType.GetProperty(PropertyName);
if (propertyInfo == null)
{
throw new Exception("PropertyInfo with name '{0}' is NULL".FormatWith(PropertyName));
}
var propertyType = propertyInfo.PropertyType;
if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
propertyType = Nullable.GetUnderlyingType(propertyType);
}
var args = new[] { Convert.ChangeType(value, propertyType) };
var data = repository.GetType().InvokeMember(MethodName, BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public, null, repository, args);
if (idValue != 0 && data != null)
{
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
else if (idValue == 0 && data != null)
{
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
else
{
return new RequiredAttribute().IsValid(value) ? ValidationResult.Success : new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
return ValidationResult.Success;
}
public override string FormatErrorMessage(string name)
{
if (String.IsNullOrEmpty(ErrorMessage))
{
var genericMessageWithPlaceHolder = (string)Resources.ResourceManager.GetObject("PropertyUniqueDefaultError");
if (!string.IsNullOrEmpty(genericMessageWithPlaceHolder))
{
ErrorMessage = genericMessageWithPlaceHolder;
}
}
return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString, name);
}
}
}
Use:
[Unique(typeof(ICustomerRepository), typeof(customers), "Email", "GetByEmail")]
[Display(Name = Translations.Global.EMAIL)]
public string Email { get; set; }
I have created a custom RequiredIf validator like this:
public class RequiredIfValidator : ValidationAttribute, IClientValidatable
{
RequiredAttribute _innerAttribute = new RequiredAttribute();
public string _dependentProperty { get; set; }
public object _targetValue { get; set; }
public RequiredIfValidator(string dependentProperty, object targetValue)
{
this._dependentProperty = dependentProperty;
this._targetValue = targetValue;
}
public override string FormatErrorMessage(string name)
{
return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, _dependentProperty);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var field = validationContext.ObjectInstance.GetType().GetProperty(_dependentProperty);
if (field != null)
{
var dependentValue = field.GetValue(validationContext.ObjectInstance, null);
if ((dependentValue == null && _targetValue == null) ||(dependentValue.Equals(_targetValue)))
{
if (!_innerAttribute.IsValid(value))
{
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
}
}
return ValidationResult.Success;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule();
rule.ErrorMessage = FormatErrorMessage(metadata.GetDisplayName());
rule.ValidationType = "requiredif";
rule.ValidationParameters["dependentproperty"] = _dependentProperty;
rule.ValidationParameters["targetvalue"] = _targetValue;
yield return rule;
}
}
I have an enum with various test types like this:
public enum TestTypes
{
Hair = 1,
Urine = 2
}
My ViewModel has some properties like this:
public class TestViewModel
{
public TestTypes TestTypeId {get; set;}
[RequiredIfValidator("TestTypeId", TestTypes.Hair)]
public string HairSpecimenId {get; set;}
}
My custom RequiredIfValidator is not working in this scinario. Is it because of the enum data type? Any way to achieve this with enums
You logic in the IsValid() does not appear to be correct. It should be
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null)
{
var otherProperty = validationContext.ObjectInstance.GetType().GetProperty(_dependentProperty);
var otherPropertyValue = otherProperty.GetValue(validationContext.ObjectInstance, null);
if (otherPropertyValue != null && otherPropertyValue.Equals(_targetValue ))
{
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
}
return ValidationResult.Success;
}
I'm looking for a way to get the name and value of a proptery in a POCO object. I've tried many solutions but can't seem to get them to work. I really liked this older solution but it causes a null ref error.
Here's kind of what I'm trying to do:
public class POCO
{
public int ID { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public string Description { get; set; }
}
public class POCOValidationResult
{
public bool IsValid { get; set; }
public string Error { get; set; }
}
public abstract class Validator<T> where T : class
{
public T Entity { get; set; }
public abstract POCOValidationResult Validate();
protected POCOValidationResult ValidateStringPropertyToLengthOf(Expression<Func<T, object>> expression, int maxLength)
{
var propertyName = getPropertyName(expression);
var propertyValue = getPropertyValue(expression);
if (propertyValue.Length > maxLength)
{
return new POCOValidationResult()
{
Error = string.Format("{0} value is too long. Must be less or equal to {1}", propertyName, maxLength.ToString())
};
}
return new POCOValidationResult() { IsValid = true };
}
internal string getPropertyName(Expression<Func<T, object>> expression)
{
var memberExpersion = (MemberExpression)expression.Body;
return memberExpersion.Member.Name;
}
internal string getPropertyValue<R>(Expression<Func<T, R>> expression)
{
//struggling to get this to work
var me = (MemberExpression)expression.Body; // (MemberExpression)((MemberExpression)expression.Body).Expression;
var ce = (ConstantExpression)me.Expression; // Error here!
var fieldInfo = ce.Value.GetType().GetField(me.Member.Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
var value = fieldInfo.GetValue(ce.Value);
}
}
public class POCOValidator : Validator<POCO>
{
public override POCOValidationResult Validate()
{
var surnameValidationResult = ValidateStringPropertyToLengthOf(p => p.Surname, 10);
if (!surnameValidationResult.IsValid)
return surnameValidationResult;
//var descriptionValidationResult = ValidateStringPropertyToLengthOf(p => p.Description, 100);
//if (!descriptionValidationResult.IsValid)
// return descriptionValidationResult;
//var nameValidationResult = ValidateStringPropertyToLengthOf(p => p.Name, 15);
//if (!nameValidationResult.IsValid)
// return nameValidationResult;
return new POCOValidationResult() { IsValid = true };
}
}
public class WorkerBee
{
public void ImDoingWorkReally()
{
var pocoVallidation = new POCOValidator()
{
Entity = new POCO()
{
ID = 1,
Name = "James",
Surname = "Dean",
Description = "I'm not 007!"
}
};
var vallidationResult = pocoVallidation.Validate();
if (!vallidationResult.IsValid)
{
return;
}
//continue to do work...
}
}
class Program
{
static void Main(string[] args)
{
var workerBee = new WorkerBee();
workerBee.ImDoingWorkReally();
}
}
So as you can see, I'm trying to get the Property's name and value [by using an Expression (p => p.Surname) as a parameter in the method ValidateStringPropertyToLengthOf(...)]. The problem is that I'm getting a null ref error in getPropertyValue(Expression<Func<T, object>> expression) when it calls var ce = (ConstantExpression)me.Expression;
So does anyone have ideas on how to get this to work?
Thanks for taking the time to look into this. I really appreciate it and hope that my question also is helpful for others as I think this can be rather useful if I can get this to work.
EDIT: I've made the change as mentioned below in the comments and still getting the error "Unable to cast object of type 'System.Linq.Expressions.TypedParameterExpression' to type 'System.Linq.Expressions.ConstantExpression" when I run my unit test.
I worked out a solution (unfortunately the comments were not helpful). Here's the code that works:
public class POCO
{
public int ID { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public string Description { get; set; }
}
public class POCOValidationResult
{
public bool IsValid { get; set; }
public string Error { get; set; }
}
public abstract class Validator<T> where T : class
{
public T Entity { get; set; }
public abstract POCOValidationResult Validate();
protected POCOValidationResult ValidateStringPropertyToLengthOf(Expression<Func<T, object>> expression, int maxLength)
{
var propertyName = getPropertyName(expression);
var propertyValue = getPropertyValue(expression);
if (propertyValue.Length > maxLength)
{
return new POCOValidationResult()
{
Error = string.Format("{0} value is too long. Must be less or equal to {1}", propertyName, maxLength.ToString())
};
}
return new POCOValidationResult() { IsValid = true };
}
internal string getPropertyName(Expression<Func<T, object>> expression)
{
var memberExpersion = (MemberExpression)expression.Body;
return memberExpersion.Member.Name;
}
internal string getPropertyValue(Expression<Func<T, object>> expression)
{
var memberExpression = expression.Body as MemberExpression;
var propertyInfo = memberExpression.Member as PropertyInfo;
return propertyInfo.GetValue(Entity, null).ToString();
}
}
public class POCOValidator : Validator<POCO>
{
public override POCOValidationResult Validate()
{
var surnameValidationResult = ValidateStringPropertyToLengthOf(p => p.Surname, 10);
if (!surnameValidationResult.IsValid)
return surnameValidationResult;
var descriptionValidationResult = ValidateStringPropertyToLengthOf(p => p.Description, 100);
if (!descriptionValidationResult.IsValid)
return descriptionValidationResult;
var nameValidationResult = ValidateStringPropertyToLengthOf(p => p.Name, 15);
if (!nameValidationResult.IsValid)
return nameValidationResult;
return new POCOValidationResult() { IsValid = true };
}
}
public class WorkerBee
{
public void ImDoingWorkReally()
{
var pocoVallidation = new POCOValidator()
{
Entity = new POCO()
{
ID = 1,
Name = "James",
Surname = "Dean",
Description = "I'm not 007!"
}
};
var vallidationResult = pocoVallidation.Validate();
if (!vallidationResult.IsValid)
{
return;
}
//continue to do work...
}
}
class Program
{
static void Main(string[] args)
{
var workerBee = new WorkerBee();
workerBee.ImDoingWorkReally();
}
}
Note the change for getPropertyValue:
internal string getPropertyValue(Expression<Func<T, object>> expression)
{
var memberExpression = expression.Body as MemberExpression;
var propertyInfo = memberExpression.Member as PropertyInfo;
return propertyInfo.GetValue(Entity, null).ToString();
}
I noticed that MaudDib's getPropertyValue method only works for properties directly on the object. e.g. it will work for myObj.Value, but not for myObj.Something.Value. The following works for both. (I don't know if there's a better way, though).
Usage: var myValue = GetPropertyValue(myObj, o => o.Something.Value);
public static object GetPropertyValue<T>(T obj, Expression<Func<T, object>> expression)
{
var members = new CompositeExpressionVisitor().GetMembers(expression);
object currentVal = obj;
foreach (var part in members)
{
currentVal = GetPropertyValue(currentVal, part);
}
return currentVal;
}
private static object GetPropertyValue(object obj, MemberInfo member)
{
var propertyInfo = (PropertyInfo)member;
return propertyInfo.GetValue(obj, null);
}
private class CompositeExpressionVisitor : ExpressionVisitor
{
private readonly List<MemberInfo> _members = new List<MemberInfo>();
protected override Expression VisitMember(MemberExpression node)
{
_members.Add(node.Member);
return base.VisitMember(node);
}
public IReadOnlyCollection<MemberInfo> GetMembers(Expression e)
{
Visit(e is LambdaExpression expression ? expression.Body : e);
_members.Reverse();
return _members;
}
}
And, if you want the full path of the expression, e.g. you don't just want the leaf... you can do this:
Usage: var myValue = NameOf(() => myObj.Something.Value);
Returns: myObj.Something.Value
Or: var myValue = NameOf(() => myObj.Something.Value, 1);
Returns: Something.Value
public static string NameOf(Expression<Func<object>> expression, int startIndex = 0)
{
return new CompositeExpressionVisitor().GetPath(expression, startIndex);
}
private class CompositeExpressionVisitor : ExpressionVisitor
{
private readonly List<string> _parts = new List<string>();
protected override Expression VisitMember(MemberExpression node)
{
_parts.Add(node.Member.Name);
return base.VisitMember(node);
}
protected override Expression VisitParameter(ParameterExpression node)
{
_parts.Add(node.Name);
return base.VisitParameter(node);
}
public string GetPath(Expression e, int startIndex = 0)
{
Visit(e is LambdaExpression expression ? expression.Body : e);
_parts.Reverse();
return string.Join(".", _parts.Skip(startIndex));
}
}
let's imagine i have the following classes that i am not allowed to change:
public class BaseType
{
public UInt32 m_baseMember = 1;
public bool m_baseMemberBool = false;
}
public class ComposedType
{
public ComposedType()
{
m_baseData = new BaseType();
}
public UInt32 m_newMember = 2;
public BaseType m_baseData;
}
Now i want to edit those data by putting it in an PropertyGrid. I created two Wrapper classes like those ( http://msdn.microsoft.com/en-us/magazine/cc163816.aspx )
public class FieldsToPropertiesProxyTypeDescriptor : ICustomTypeDescriptor
{
private object _target;
// object to be described
public FieldsToPropertiesProxyTypeDescriptor(object target)
{
if (target == null)
throw new ArgumentNullException("target");
_target = target;
}
public object GetProxiedObject()
{
return _target;
}
object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)
{
return _target;
// properties belong to the target object
}
AttributeCollection ICustomTypeDescriptor.GetAttributes()
{
// Gets the attributes of the target object
return TypeDescriptor.GetAttributes(_target, true);
}
string ICustomTypeDescriptor.GetClassName()
{
// Gets the class name of the target object
return TypeDescriptor.GetClassName(_target, true);
}
string ICustomTypeDescriptor.GetComponentName()
{
return TypeDescriptor.GetComponentName(_target, true);
}
TypeConverter ICustomTypeDescriptor.GetConverter()
{
return TypeDescriptor.GetConverter(_target, true);
}
EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()
{
return TypeDescriptor.GetDefaultEvent(_target, true);
}
PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()
{
return TypeDescriptor.GetDefaultProperty(_target, true);
}
object ICustomTypeDescriptor.GetEditor(Type editorBaseType)
{
return TypeDescriptor.GetEditor(_target, editorBaseType);
}
EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
{
return TypeDescriptor.GetEvents(_target, attributes);
}
EventDescriptorCollection ICustomTypeDescriptor.GetEvents()
{
return TypeDescriptor.GetEvents( _target );
}
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
{
return ((ICustomTypeDescriptor)this).GetProperties(null);
}
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties( Attribute[] attributes)
{
bool filtering = (attributes != null && attributes.Length > 0);
PropertyDescriptorCollection props = new PropertyDescriptorCollection(null);
foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(_target, attributes, true))
{
props.Add(prop);
}
foreach (FieldInfo field in _target.GetType().GetFields())
{
FieldPropertyDescriptor fieldDesc = new FieldPropertyDescriptor(field);
if (!filtering || fieldDesc.Attributes.Contains(attributes))
props.Add(fieldDesc);
}
return props;
}
}
public class FieldPropertyDescriptor : PropertyDescriptor
{
private FieldInfo _field;
public FieldPropertyDescriptor(FieldInfo field) : base(field.Name, (Attribute[])field.GetCustomAttributes(typeof(Attribute), true))
{
_field = field;
}
public FieldInfo Field
{
get { return _field; }
}
public override bool Equals(object obj)
{
FieldPropertyDescriptor other = obj as FieldPropertyDescriptor;
return other != null && other._field.Equals(_field);
}
public override int GetHashCode()
{
return _field.GetHashCode();
}
public override bool IsReadOnly
{
get { return false; }
}
public override AttributeCollection Attributes
{
get
{
if (_field.FieldType.IsClass || _field.FieldType.IsArray)
{
Attribute[] expandable = new Attribute[1];
expandable[0] = new ExpandableObjectAttribute();
return AttributeCollection.FromExisting(base.Attributes, expandable);
}
return base.Attributes;
}
}
public override void ResetValue(object component)
{
}
public override bool CanResetValue(object component)
{
return false;
}
public override bool ShouldSerializeValue(object component)
{
return true;
}
public override Type ComponentType
{
get { return _field.DeclaringType; }
}
public override Type PropertyType
{
get { return _field.FieldType; }
}
public override object GetValue(object component)
{
if (component is FieldsToPropertiesProxyTypeDescriptor)
{
FieldsToPropertiesProxyTypeDescriptor proxy = (FieldsToPropertiesProxyTypeDescriptor)component;
return _field.GetValue(proxy.GetProxiedObject());
}
return _field.GetValue(component);
}
public override void SetValue(object component, object value)
{
if (component is FieldsToPropertiesProxyTypeDescriptor)
{
FieldsToPropertiesProxyTypeDescriptor proxy = (FieldsToPropertiesProxyTypeDescriptor)component;
_field.SetValue(proxy.GetProxiedObject(), value);
OnValueChanged(proxy.GetProxiedObject(), EventArgs.Empty);
return;
}
_field.SetValue(component, value);
OnValueChanged(component, EventArgs.Empty);
}
}
I can see and edit the 'm_newMember' in the PropertyGrid but i need to wrap the access to 'm_baseData' via FieldsToPropertiesProxyTypeDescriptor. How could i achieve this. Or is there a better way to wrap fields into Properties?
You can change attributes of a given class at runtime without changing that class. So you could write a custom TypeConverter and set it to your classes, something like this:
TypeDescriptor.AddAttributes(typeof(ComposedType), new TypeConverterAttribute(typeof(FieldsExpandableObjectConverter)));
TypeDescriptor.AddAttributes(typeof(BaseType), new TypeConverterAttribute(typeof(FieldsExpandableObjectConverter)));
With the following TypeConverter (re-using your FieldDescriptor class):
public class FieldsExpandableObjectConverter : ExpandableObjectConverter
{
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
List<PropertyDescriptor> properties = new List<PropertyDescriptor>(base.GetProperties(context, value, attributes).OfType<PropertyDescriptor>());
if (value != null)
{
foreach (FieldInfo field in value.GetType().GetFields())
{
FieldPropertyDescriptor fieldDesc = new FieldPropertyDescriptor(field);
{
properties.Add(fieldDesc);
}
}
}
return new PropertyDescriptorCollection(properties.ToArray());
}
}