Home » c# » Change Attribute's parameter at runtime

Change Attribute's parameter at runtime

Posted by: admin November 30, 2017 Leave a comment

Questions:

I am not sure whether is it possible to change attribute’s parameter during runtime? For example, inside an assembly I have the following class

public class UserInfo
{
    [Category("change me!")]
    public int Age
    {
        get;
        set;
    }
    [Category("change me!")]
    public string Name
    {
        get;
        set;
    }
}

This is a class that is provided by a third party vendor and I can’t change the code. But now I found that the above descriptions are not accurate, and I want to change the “change me” category name to something else when i bind an instance of the above class to a property grid.

May I know how to do this?

Answers:

Well you learn something new every day, apparently I lied:

What isn’t generally realised is that
you can change attribute instance values fairly
easily at runtime. The reason is, of
course, that the instances of the
attribute classes that are created are
perfectly normal objects and can be
used without restriction. For example,
we can get the object:

ASCII[] attrs1=(ASCII[])
    typeof(MyClass).GetCustomAttributes(typeof(ASCII), false);

…change the value of its public variable and show that it has changed:

attrs1[0].MyData="A New String";
MessageBox.Show(attrs1[0].MyData);

…and finally create another instance
and show that its value is unchanged:

ASCII[] attrs3=(ASCII[])
    typeof(MyClass).GetCustomAttributes(typeof(ASCII), false);
 MessageBox.Show(attrs3[0].MyData);

http://www.vsj.co.uk/articles/display.asp?id=713

Questions:
Answers:

In case anyone else walks down this avenue, the answer is you can do it, with reflection, except you can’t because there’s a bug in the framework. Here’s how you would do it:

    Dim prop As PropertyDescriptor = TypeDescriptor.GetProperties(GetType(UserInfo))("Age")
    Dim att As CategoryAttribute = DirectCast(prop.Attributes(GetType(CategoryAttribute)), CategoryAttribute)
    Dim cat As FieldInfo = att.GetType.GetField("categoryValue", BindingFlags.NonPublic Or BindingFlags.Instance)
    cat.SetValue(att, "A better description")

All well and good, except that the category attribute is changed for all the properties, not just ‘Age’.

Questions:
Answers:

You can subclass most of the common attributes quite easily to provide this extensibility:

using System;
using System.ComponentModel;
using System.Windows.Forms;
class MyCategoryAttribute : CategoryAttribute {
    public MyCategoryAttribute(string categoryKey) : base(categoryKey) { }

    protected override string GetLocalizedString(string value) {
        return "Whad'ya know? " + value;
    }
}

class Person {
    [MyCategory("Personal"), DisplayName("Date of Birth")]
    public DateTime DateOfBirth { get; set; }
}

static class Program {
    [STAThread]
    static void Main() {
        Application.EnableVisualStyles();
        Application.Run(new Form { Controls = {
           new PropertyGrid { Dock = DockStyle.Fill,
               SelectedObject = new Person { DateOfBirth = DateTime.Today}
           }}});
    }
}

There are more complex options that involve writing custom PropertyDescriptors, exposed via TypeConverter, ICustomTypeDescriptor or TypeDescriptionProvider – but that is usually overkill.

Questions:
Answers:

Unfortunately attributes are not meant to change at runtime. You basically have two options:

  1. Recreate a similar type on the fly using System.Reflection.Emit as shown below.

  2. Ask your vendor to add this functionality. If you are using Xceed.WpfToolkit.Extended you can download the source code from here and easily implement an interface like IResolveCategoryName that would resolve the attribute at runtime. I did a bit more than that, it was pretty easy to add more functionality like limits when editing a numeric value in a DoubleUpDown inside the PropertyGrid, etc.

    namespace Xceed.Wpf.Toolkit.PropertyGrid
    {
        public interface IPropertyDescription
        {
            double MinimumFor(string propertyName);
            double MaximumFor(string propertyName);
            double IncrementFor(string propertyName);
            int DisplayOrderFor(string propertyName);
            string DisplayNameFor(string propertyName);
            string DescriptionFor(string propertyName);
            bool IsReadOnlyFor(string propertyName);
        }
    }
    

For first option: This however lack of proper property binding to reflect the result back to the actual object being edited.

    private static void CreatePropertyAttribute(PropertyBuilder propertyBuilder, Type attributeType, Array parameterValues)
    {
        var parameterTypes = (from object t in parameterValues select t.GetType()).ToArray();
        ConstructorInfo propertyAttributeInfo = typeof(RangeAttribute).GetConstructor(parameterTypes);
        if (propertyAttributeInfo != null)
        {
            var customAttributeBuilder = new CustomAttributeBuilder(propertyAttributeInfo,
                parameterValues.Cast<object>().ToArray());
            propertyBuilder.SetCustomAttribute(customAttributeBuilder);
        }
    }
    private static PropertyBuilder CreateAutomaticProperty(TypeBuilder typeBuilder, PropertyInfo propertyInfo)
    {
        string propertyName = propertyInfo.Name;
        Type propertyType = propertyInfo.PropertyType;

        // Generate a private field
        FieldBuilder field = typeBuilder.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);

        // Generate a public property
        PropertyBuilder property = typeBuilder.DefineProperty(propertyName, PropertyAttributes.None, propertyType,
            null);

        // The property set and property get methods require a special set of attributes:
        const MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.HideBySig;

        // Define the "get" accessor method for current private field.
        MethodBuilder currGetPropMthdBldr = typeBuilder.DefineMethod("get_" + propertyName, getSetAttr, propertyType, Type.EmptyTypes);

        // Intermediate Language stuff...
        ILGenerator currGetIl = currGetPropMthdBldr.GetILGenerator();
        currGetIl.Emit(OpCodes.Ldarg_0);
        currGetIl.Emit(OpCodes.Ldfld, field);
        currGetIl.Emit(OpCodes.Ret);

        // Define the "set" accessor method for current private field.
        MethodBuilder currSetPropMthdBldr = typeBuilder.DefineMethod("set_" + propertyName, getSetAttr, null, new[] { propertyType });

        // Again some Intermediate Language stuff...
        ILGenerator currSetIl = currSetPropMthdBldr.GetILGenerator();
        currSetIl.Emit(OpCodes.Ldarg_0);
        currSetIl.Emit(OpCodes.Ldarg_1);
        currSetIl.Emit(OpCodes.Stfld, field);
        currSetIl.Emit(OpCodes.Ret);

        // Last, we must map the two methods created above to our PropertyBuilder to 
        // their corresponding behaviors, "get" and "set" respectively. 
        property.SetGetMethod(currGetPropMthdBldr);
        property.SetSetMethod(currSetPropMthdBldr);

        return property;

    }

    public static object EditingObject(object obj)
    {
        // Create the typeBuilder
        AssemblyName assembly = new AssemblyName("EditingWrapper");
        AppDomain appDomain = System.Threading.Thread.GetDomain();
        AssemblyBuilder assemblyBuilder = appDomain.DefineDynamicAssembly(assembly, AssemblyBuilderAccess.Run);
        ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(assembly.Name);

        // Create the class
        TypeBuilder typeBuilder = moduleBuilder.DefineType("EditingWrapper",
            TypeAttributes.Public | TypeAttributes.AutoClass | TypeAttributes.AnsiClass |
            TypeAttributes.BeforeFieldInit, typeof(System.Object));

        Type objType = obj.GetType();
        foreach (var propertyInfo in objType.GetProperties())
        {
            string propertyName = propertyInfo.Name;
            Type propertyType = propertyInfo.PropertyType;

            // Create an automatic property
            PropertyBuilder propertyBuilder = CreateAutomaticProperty(typeBuilder, propertyInfo);

            // Set Range attribute
            CreatePropertyAttribute(propertyBuilder, typeof(Category), new[]{"My new category value"});

        }

        // Generate our type
        Type generetedType = typeBuilder.CreateType();

        // Now we have our type. Let's create an instance from it:
        object generetedObject = Activator.CreateInstance(generetedType);

        return generetedObject;
    }
}

Questions:
Answers:

Did you solve the problem?

Here are possible steps to achieve an acceptable solution.

  1. Try to create a child class, redefine all of the properties that you need to change the [Category] attribute (mark them with new). Example:
public class UserInfo
{
 [Category("Must change")]
 public string Name { get; set; }
}

public class NewUserInfo : UserInfo
{
 public NewUserInfo(UserInfo user)
 {
 // transfer all the properties from user to current object
 }

 [Category("Changed")]
 public new string Name {
get {return base.Name; }
set { base.Name = value; }
 }

public static NewUserInfo GetNewUser(UserInfo user)
{
return NewUserInfo(user);
}
}

void YourProgram()
{
UserInfo user = new UserInfo();
...

// Bind propertygrid to object

grid.DataObject = NewUserInfo.GetNewUser(user);

...

}

Later Edit: This part of the solution is not workable if you have a large number of properties that you might need to rewrite the attributes. This is where part two comes into place:

  1. Of course, this won’t help if the class is not inheritable, or if you have a lot of objects (and properties). You would need to create a full automatic proxy class that gets your class and creates a dynamic class, applies attributes and of course makes a connection between the two classes.. This is a little more complicated, but also achievable. Just use reflection and you’re on the right road.
Questions:
Answers:

Given that the PropertyGrid’s selected item is “Age”:

SetCategoryLabelViaReflection(MyPropertyGrid.SelectedGridItem.Parent,
    MyPropertyGrid.SelectedGridItem.Parent.Label, "New Category Label");

Where SetCategoryLabelViaReflection() is defined as follows:

private void SetCategoryLabelViaReflection(GridItem category,
                                           string oldCategoryName,
                                           string newCategoryName)
{
    try
    {
        Type t = category.GetType();
        FieldInfo f = t.GetField("name",
                                 BindingFlags.NonPublic | BindingFlags.Instance);
        if (f.GetValue(category).Equals(oldCategoryName))
        {
            f.SetValue(category, newCategoryName);
        }
    }
    catch (Exception ex)
    {
        System.Diagnostics.Trace.Write("Failed Renaming Category: " + ex.ToString());
    }
}

As far as programatically setting the selected item, the parent category of which you wish to change; there are a number of simple solutions. Google “Set Focus to a specific PropertyGrid property”.

Questions:
Answers:

I really don’t think so, unless there’s some funky reflection that can pull it off. The property decorations are set at compile time and to my knowledge are fixed

Questions:
Answers:

In the mean time I’ve come to a partial solution, derived from the following articles:

  1. ICustomTypeDescriptor, Part 1
  2. ICustomTypeDescriptor, Part 2
  3. Add (Remove) Items to (from) PropertyGrid at Runtime

Basically you would create a generic class CustomTypeDescriptorWithResources<T>, that would get the properties through reflection and load Description and Category from a file (I suppose you need to display localized text so you could use a resources file (.resx))

Questions:
Answers:

You can change Attribute values at runtime at Class level (not object):

var attr = TypeDescriptor.GetProperties(typeof(UserContact))["UserName"].Attributes[typeof(ReadOnlyAttribute)] as ReadOnlyAttribute;
attr.GetType().GetField("isReadOnly", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(attr, username_readonly);

Questions:
Answers:

Here’s a “cheaty” way to do it:

If you have a fixed number of constant potential values for the attribute parameter, you can define a separate property for each potential value of the parameter (and give each property that slightly different attribute), then switch which property you reference dynamically.

In VB.NET, it might look like this:

Property Time As Date

<Display(Name:="Month")>
ReadOnly Property TimeMonthly As Date
    Get
        Return Time
    End Get
End Property

<Display(Name:="Quarter")>
ReadOnly Property TimeQuarterly As Date
    Get
        Return Time
    End Get
End Property

<Display(Name:="Year")>
ReadOnly Property TimeYearly As Date
    Get
        Return Time
    End Get
End Property

Leave a Reply

Your email address will not be published. Required fields are marked *