views:

590

answers:

3

I believe there is no human way to change any attribute or field inside an Attribute apart from doing it in the constructor. That is, short of redesigning and recompiling Visual Studio yourself. There is already a similar question posted here: http://stackoverflow.com/questions/51269/change-attributes-parameter-at-runtime but I believe the peculiarities of my problem are different enough to require a new post.

I use an enumeration to keep track of the different columns of a DataTable. I use attributes in each enumeration element to indicate the underlying type and the description -in case the .ToString() would give an "ugly" result due to the rigid set of characters that are allowed to name an enumeration element, such as "Tomato_Field" when you want "Tomato Field", and the like. This allows me to place all the related information in the same object, which is, I believe, what it should be. This way I can later create all the columns with a simple and clean foreach that cycles through the elements of the enumeration and extracts the metedata (description and type) to create each column.

Now, some of the columns are autocalculated, which means that during their creation -via DataTable Identifier.Columns.Add.(NameOfColumn,underlyingType,optional: autocalculatedString)- I need to specify a string that determines how it should be calculated. That string must use the names of other columns, which might be in the Description Attribute. The approach that looks logical is to use another attribute that holds the string, which should be built using the names of the other columns, requiring access to the metadata. Now that seems impossible in the constructor: you are forced to provide a constant string. You can't use a method or anything.

This problem could be solved if there were a way to change a property inside the attribute (lets call it AutocalculatedStringAttribute) at runtime. If you access the metadata you can retrieve the string you used at the constructor of the Attribute, and you can of course change that string. However, if you later access the metadata again that change is ignored, I believe the constructor is called every time the metadata is accessed at runtime, thus ignoring any changes.

There are, of course, dirty ways to achive what I am trying to do, but my question is specifically if there is a way to properly use attributes for this. Short of resorting to CodeDOM to recompile the whole assembly with the constructor of the AutocalculatedStringAttribute changed, a certain overkill.

+1  A: 

Right, the metadata that's used to initialize the attribute is immutable. But you can add properties and methods to an attribute class that can run code and return relevant info after the attribute object is constructed. The data they rely on doesn't have to be stored in metadata, it can be persisted anywhere.

Of course, such code wouldn't have to be part of the attribute class implementation, it could just as well be part of the code that instantiates the attribute. Which is where it belongs.

Hans Passant
Could you post a quick sample of what you mean. I am not sure I understand your approach. I have included a fragment of the relevant code (answer 3) in case it helps to illustrate what I am trying to do.
Carlos Hugo
A: 

It isn't entirely clear to me what code is consuming this attribute, and it matters...

You cannot change an attribute that is burned into the code - you can query it with reflection, but that is about it. However, in many cases you can still do interesting things - I don't know if they apply to your scenario, though:

  • you can subclass many attributes like [Description], [DisplayName], etc - and while you pass in a constant string (typically a key) to the .ctor, it can return (through regular C#) more flexible values - perhaps looking up the description from a resx to implement i18n
  • if the caller respects System.ComponentModel, you can attach attributes at runtime to types etc very easily - but much harder on individual properties, especially in the case of DataTable etc (since that has a custom descriptor model via DataView)
  • you can wrap things and provide your own model via ICustomTypeDescriptor / TypeDescriptionProvider / PropertyDescriptor - lots of work, but provides access to set your own attributes, or return a description (etc) outside of attributes

I don't know how much of this is suitable for your environment (perhaps show some code of what you have and what you want), but it highlights that (re the question title) yes: there are things you can do to tweak how attributes are perceived at runtime.

Marc Gravell
I have posted a fragment of the code just below following your suggestion. Subclassing, while ingenious, is not a clean solution in this case, I believe.
Carlos Hugo
A: 

I wanted to post this as a comment but since I wanted to include some code I couldn't, given the 600 characters limit. This is the cleanest solution I have managed to find, although it does not include all the info to create the columns on the enum, which is my goal. I have translated every field to make it easier to follow. I am not showing some code which has an obvious use (in particular the implementations of the other custom attributes and their static methods to retrieve the metadata, assume that it works).

This gets the job done, but I would ideally like to include the information stored in the strings "instancesXExpString " and "totalInstancesString" in the Autocalculated attribute, which currently only marks the columns that have such a string. This is what I have been unable to do and what, I believe, cannot be easily accomplished via subclassing -although it is an ingenious approach, I must say. Thanks for the two prompt replies, btw.

And without any further ado, lets get to the code:

// Form in which the DataGridView, its underlying DataTable and hence the enumeration are:
public partial class MainMenu : Form {
(...)

    DataTable dt_expTable;

//Enum that should have all the info on its own... but does not:
public enum e_columns {
        [TypeAttribute(typeof(int))]
        Experiments = 0,

        [TypeAttribute(typeof(decimal))]
        Probability,

        [DescriptionAttribute("Samples / Exp.")]
        [TypeAttribute(typeof(int))]
        SamplesXExperiment,

        [DescriptionAttribute("Instances / Sample")]
        [TypeAttribute(typeof(int))]
        InstancesXSample,

        [DescriptionAttribute("Instances / Exp.")]
        [TypeAttribute(typeof(int))]
        [Autocalculated()]
        InstancesXExp,

        [DescriptionAttribute("Total Instances")]
        [TypeAttribute(typeof(long))]
        [Autocalculated()]
        Total_Instances
    };

//These are the two strings
string instancesXExpString = "[" + DescriptionAttribute.obtain(e_columns.SamplesXExperiment) + "] * [" + DescriptionAttribute.obtain(e_columns.InstancesXMuestra) + "]";
    string totalInstancesString = "[" + DescriptionAttribute.obtain(e_columns.InstancesXExp) + "] * [" + DescriptionAttribute.obtain(e_columns.Experiments) + "]";

public MainMenu() {
        InitializeComponent();
 (...)        
    }

private void MainMenu_Load(object sender, EventArgs e) {
 (...)
 // This is the neat foreach I refered to:
 foreach (e_columns en in Enum.GetValues(typeof(e_columnas))) {
                addColumnDT(en);
        }
}

private void addColumnDT(Enum en) {
 //*This is a custom static method for a custom attrib. that simply retrieves the description string or
 //the standard .ToString() if there is no such attribute.*/
            string s_columnName = DescriptionAttribute.obtain(en);
            bool b_typeExists;
            string s_calculusString;
            Type TypeAttribute = TypeAttribute.obtain(en, out b_typeExists);
            if (!b_typeExists) throw (new ArgumentNullException("Type has not been defined for one of the columns."));
            if (isCalculatedColumn(DescriptionAttribute.obtain(en))) {
                s_calculusString = calcString(en);
                dt_expTable.Columns.Add(s_columnName, TypeAttribute, s_calculusString);
            } else {
                dt_expTable.Columns.Add(s_columnName, TypeAttribute);
            }
    }

private string calcString(Enum en) {
        if (en.ToString() == e_columns.InstancessXExp.ToString()) {
            return instancesXExpString;
        } else if (en.ToString() == e_columns.Total_Samples.ToString()) {
            return totalInstancesString;
        } else throw (new ArgumentException("There is a column with the autocalculated attribute whose calculus string has not been considered."));
    }
(...)
}

I hope this piece of code clarifies the situation and what I am trying to do.

Carlos Hugo
Maybe a TypeConverter?
Marc Gravell