dani herrera
dani herrera

Reputation: 51645

How can I dynamically make entity properties read-only?

I'm working with EF 4.5 and DbContext. At business rules layer level, I should implement checks to avoid change entity value properties in some entity scenarios. Sample: StartProjecteDate should be readonly if ProjectIsStarted but not in other status.

I follow DRY principle, for this reason, I should be able to inspect readonly properties list from context and also from UI.

My question:

Is there a DataAnnotation validator that can dynamically set properties as readonly?

(and if not, is there a different / better solution to this problem?)

Notice than I'm working with Web Forms (and Telerik) architecture, a clean and elegant pattern will be welcome.

I'm trying to set and get at run time EditableAttribute as Jesse Webb explains, but I'm not able to get dataannotation attributes from property, my code:

<EditableAttribute(False)>
<MaxLength(400, ErrorMessage:="Màxim 400 caracters")>
Public Property NomInvertebrat As String

enter image description here

Edited Nov 8 2013 after digging docs, it seems that dataanottions if for class but for instance object itself. Perhaps an iReadonlyableProperties interface may be a way.

Upvotes: 2

Views: 1262

Answers (3)

Chris Wu
Chris Wu

Reputation: 57

I think what you are looking for is a custom Annotation Attribute like this:

<DisableEditAttribute(this.IsProjectStarted)>
Public Property NomInvertebrat As String

public override bool IsValid(bool value)
{
    bool result = true;
    // Add validation logic here.
    if(value)
    {
         //Compare Current Value Against DB Value.
    }
    return result;
}

See MSDN: http://msdn.microsoft.com/en-us/library/cc668224(v=vs.98).aspx

Upvotes: 1

Akash Kava
Akash Kava

Reputation: 39916

.NET allows you to dynamically change structure of Class by implementing System.ComponentModel.ICustomTypeDescriptor. Most serializers support this interface.

// Sample Serialization

foreach(PropertyDescriptor p in TypeDescriptor.GetProperties(obj)){
    string name = p.PropertyName;
    object value = p.GetValue(obj);
}

Internally TypeDescriptor uses Reflection, but the implementation allows us to override reflection attributes easily.

Here are three steps of implementation,

// Implement System.ComponentModel.ICustomTypeDescriptor Interface on
// your Entity

public class MyEntity: System.ComponentModel.ICustomTypeDescriptor
{
     ....
     // most methods needs only call to default implementation as shown below

    System.ComponentModel.AttributeCollection      
    System.ComponentModel.ICustomTypeDescriptor.GetAttributes()
    {
        return TypeDescriptor.GetAttributes(this, true);
    }

    string System.ComponentModel.ICustomTypeDescriptor.GetClassName()
    {
        return TypeDescriptor.GetClassName(this, true);
    }

    string System.ComponentModel.ICustomTypeDescriptor.GetComponentName()
    {
        return TypeDescriptor.GetComponentName(this, true);
    }

    System.ComponentModel.TypeConverter System.ComponentModel.ICustomTypeDescriptor.GetConverter()
    {
        return TypeDescriptor.GetConverter(this, true);
    }

    System.ComponentModel.EventDescriptor System.ComponentModel.ICustomTypeDescriptor.GetDefaultEvent()
    {
        return TypeDescriptor.GetDefaultEvent(this, true);
    }

    System.ComponentModel.PropertyDescriptor System.ComponentModel.ICustomTypeDescriptor.GetDefaultProperty()
    {
        return TypeDescriptor.GetDefaultProperty(this, true);
    }

    object System.ComponentModel.ICustomTypeDescriptor.GetEditor(Type editorBaseType)
    {
        return TypeDescriptor.GetEditor(this, editorBaseType, true);
    }

    System.ComponentModel.EventDescriptorCollection System.ComponentModel.ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
    {
        return TypeDescriptor.GetEvents(this, attributes, true);
    }

    System.ComponentModel.EventDescriptorCollection System.ComponentModel.ICustomTypeDescriptor.GetEvents()
    {
        return TypeDescriptor.GetEvents(this, true);
    }

    System.ComponentModel.PropertyDescriptorCollection System.ComponentModel.ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
    {
        return TypeDescriptor.GetProperties(this, attributes, true);
    }

    object System.ComponentModel.ICustomTypeDescriptor.GetPropertyOwner(System.ComponentModel.PropertyDescriptor pd)
    {
        return this;
    }

    // The Only method that needs different implementation is below
    System.ComponentModel.PropertyDescriptorCollection 
    System.ComponentModel.ICustomTypeDescriptor.GetProperties()
    {
       // ... you are supposed to create new instance of 
       // PropertyDescriptorCollection with PropertyDescriptor

       PropertyDescriptorCollection pdc = new PropertyDescriptorCollection();

       foreach(PropertyDescriptor p in TypeDescriptor.GetProperties(this,true)){
            // if readonly..

            AtomPropertyDescriptor ap = new AtomPropertyDescriptor(p, p.Name);
            // or
            AtomPropertyDescriptor ap = new AtomPropertyDescriptor(p, p.Name, 
                true,
                new XmlIgnoreAttribute(),
                new ScriptIgnoreAttribute(),
                new ReadOnlyAttribute());
            pdc.Add(ap);
       }

       return pdc;
    }
}


// And here is the AtomPropertyDescriptorClass

public class AtomPropertyDescriptor : PropertyDescriptor
{
    PropertyDescriptor desc;

    bool? readOnly = null;

    public AtomPropertyDescriptor(PropertyDescriptor pd, string name, 
        bool? readOnly, params Attribute[] attrs) :
        base(name, attrs)
    {
        desc = pd;
        this.readOnly = readOnly;
    }

    public override bool CanResetValue(object component)
    {
        return desc.CanResetValue(component);
    }

    public override Type ComponentType
    {
        get
        {
            return desc.ComponentType;
        }
    }

    public override object GetValue(object component)
    {
        return desc.GetValue(component);
    }

    public override bool IsReadOnly
    {
        get
        {
            if (readOnly.HasValue)
                return readOnly.Value;
            return desc.IsReadOnly;
        }
    }

    public override Type PropertyType
    {
        get { return desc.PropertyType; }
    }

    public override void ResetValue(object component)
    {
        desc.ResetValue(component);
    }

    public override void SetValue(object component, object value)
    {
        desc.SetValue(component, value);
    }

    public override bool ShouldSerializeValue(object component)
    {
        return desc.ShouldSerializeValue(component);
    }
}

Upvotes: 1

Colin
Colin

Reputation: 22595

I have a class containing extension methods that lets me read data annotations like this:

int maxRefLen = ReflectionAPI.GetProperty<Organisation, String>(x => x.Name)
                             .GetAttribute<StringLengthAttribute>()
                             .GetValueOrDefault(x => x.MaximumLength, 256);

So if you use it you should be able to do get the value of the EditableAttribute like this:

bool isEditable = ReflectionAPI.GetProperty<Foo, String>(x => x.NomInvertebrat)
                               .GetAttribute<EditableAttribute>()
                               .GetValueOrDefault(x => x.AllowEdit, true);

As for setting the data annotations at run-time, I haven't done it myself but I have read that there is a solution here: Setting data-annotations at runtime

Getting a list of all data annotations of a particular type I think would entail reading the entity framework metadata. Again I haven't tried this.

If you add that together I personally think it feels clunky rather than elegant, but you have asked for a solution using DataAnnotations and something more elegant would probably mean getting into your architecture.

I would be inclined to do this:

public bool StartDateIsReadOnly
{
   //use this property client-side to disable the input
   get{ return Project.IsStarted;}
}

//Implement IValidatable object to do server side validation
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext
{
   bool startdateIsChanged = // I'll leave you to work out this bit
   var results = new List<ValidationResult>();
   if(StartDateIsReadOnly && startdateIsChanged)
   results.Add(new ValidationResult("Start Date cannot be changed after project is started");
}

Here is the ReflectionAPI class:

Please note that the class includes part of a hack that @JonSkeet posted and described as "evil". I personally think this bit ain't so bad, but you should read the following references:

Override a generic method for value types and reference types.

Evil code - overload resolution workaround

public static class ReflectionAPI
{

    public static int GetValueOrDefault<TInput>(this TInput a, Func<TInput, int> func, int defaultValue)
        where TInput : Attribute
    //Have to restrict to struct or you get the error:
    //The type 'R' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'System.Nullable<T>'
    {
        if (a == null)
            return defaultValue;

        return func(a);
    }

    public static Nullable<TResult> GetValueOrDefault<TInput, TResult>(this TInput a, Func<TInput, TResult> func, Nullable<TResult> defaultValue)
        where TInput : Attribute
        where TResult : struct
    //Have to restrict to struct or you get the error:
    //The type 'R' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'System.Nullable<T>'
    {
        if (a == null)
            return defaultValue;

        return func(a);
    }

    //In order to constrain to a class without interfering with the overload that has a generic struct constraint
    //we need to add a parameter to the signature that is a reference type restricted to a class
    public class ClassConstraintHack<T> where T : class { }

    //The hack means we have an unused parameter in the signature
    //http://msmvps.com/blogs/jon_skeet/archive/2010/11/02/evil-code-overload-resolution-workaround.aspx
    public static TResult GetValueOrDefault<TInput, TResult>(this TInput a, Func<TInput, TResult> func, TResult defaultValue, ClassConstraintHack<TResult> ignored = default(ClassConstraintHack<TResult>))
        where TInput : Attribute
        where TResult : class
    {
        if (a == null)
            return defaultValue;

        return func(a);
    }

    //I don't go so far as to use the inheritance trick decribed in the evil code overload resolution blog, 
    //just create some overloads that take nullable types - and I will just keep adding overloads for other nullable type
    public static bool? GetValueOrDefault<TInput>(this TInput a, Func<TInput, bool?> func, bool? defaultValue)
where TInput : Attribute
    {
        if (a == null)
            return defaultValue;

        return func(a);
    }

    public static int? GetValueOrDefault<TInput>(this TInput a, Func<TInput, int?> func, int? defaultValue)
where TInput : Attribute
    {
        if (a == null)
            return defaultValue;

        return func(a);
    }

    public static T GetAttribute<T>(this PropertyInfo p) where T : Attribute
    {
        if (p == null)
            return null;

        return p.GetCustomAttributes(false).OfType<T>().LastOrDefault();
    }

    public static PropertyInfo GetProperty<T, R>(Expression<Func<T, R>> expression)
    {
        if (expression == null)
            return null;

        MemberExpression memberExpression = expression.Body as MemberExpression;
        if (memberExpression == null)
            return null;

        return memberExpression.Member as PropertyInfo;
    }
}

Upvotes: 2

Related Questions