views:

637

answers:

4

The title pretty much says it all. When I'm doing some reflection through my classes, will the MemberInfo.GetCustomAttributes() method preserve the order of attributes on a member, or not? The official documentation does not say anything one way or the other.


In case you're wondering why I would need this, here's the full explanation. It's lengthy and not needed for the question as it is posed now, but perhaps someone can come up with an alternative solution to the greater problem which does not involve relying on attribute enumeration order.

I'm trying to make a flexible framework for an (ASP.NET) application that is expected to have a pretty long lifetime. Over the course it will gain many forms that will have to be accessible from the menu. To make life easier for the developers to come I've made a MenuItemAttribute which you can apply to a form's class. This attribute has an unlimited amount of string parameters in its constructor which allows a developer to specify where exactly will the form reside in the menu. A typical usage example would be something like [MenuItem("Company", "Clients", "Orders")] which would then mean that the menu should have an item "Company" under which there would be an item "Clients" under which there would be an item "Orders" - which would then open the form. A single form can have several of these attributes if needed - it will then be acessible from several places in the menu.

Obviously the whole menu is built in runtime by enumerating through all the classes in my assemblies and searching for this attribute. However recently I've gotten a request that the menu items should be sorted in a predefined way. Forms that have related functionality should be next to each other in the menu. Note that this is NOT alphabetical sorting, but a predefined order that the developers specify.

This then brings the problem - how do I specify the order in these attributes? Since one MenuItemAttribute describes a whole hierarchy, the order specification should also include order numbers for the whole (or at least a part) of the hierarchy. An order number for just the lower level of the hierarchy is not sufficient.

I could make another attribute - MenuItemOrderHintAttribute, but that would then bring problems with cases when there is more than one MenuItemAttribute. Hence the original question.

I could also extend the MenuItemAttribute to take either two arrays or an array of pairs, but that then would complicate the syntax a lot. The last idea is that I could make the strings have special format, but that would be rather messy IMHO.


OK, I've got another idea. Let's use the ordering that Jon Skeet suggested. This will allow to specify the order for the last level of the hierarchy, not the higher ones. But I could modify the attribute so that it applies not only to classes, but also to the assembly itself. In this case the menu item would not have an associated form. At the assembly level these attributes could then be used to specify the ordering among higher levels of the hierarchy.

This is then a tradeoff between a centralized and decentralized menu system. Any thoughts why this would be a bad idea?

A: 

Unfortunately no, you cannot guarantee that the order will be the same as the order in which you specified them.

Edit: a workaround

Disclaimer: I apologize ahead of time if I am misunderstanding your ultimate problem.

It sounds as if you need to be able to pass n number of strings to MenuItemAttribute and have MenuItemAttribute maintain the order of these strings as entered by the developer in the attributes constructor.

Here is a possible solution:

Have MenuItemAttribute use a LinkedList<String> to maintain its state. Then you can iterate the params String[] in your constructor and add them to the LinkedList<String> which would maintain the order.

Andrew Hare
Yup, you've misunderstood. :) The problem is ordering the strings when there are multiple MenuItem attributes applied. I have to order the first strings in all the items, then second strings in all the items, etc.
Vilx-
+2  A: 

I'd put a single extra (optional) value in the MenuItemAttribute constructor, which is "order" or "priority":

[MenuItem(0, "Company", "Clients", "Orders")]
[MenuItem(1, "Foo", "Bar", "Baz")]

I'm not saying it would be pretty, but it would effectively allow you to specify the ordering.

Jon Skeet
This only specifies the order for the last level of hierarchy. This way I cannot specify the relationships between "Clients" and "Bar".
Vilx-
Okay. I suspect I haven't fully understood the problem then. Given the complexities involved, perhaps attributes aren't the best way of doing this? Could you not just have a file embedded in the assembly with the whole tree in an appropriate order?
Jon Skeet
Well, that's what I wanted to avoid. I wanted to make menu modifications as easy as possible. Decentralized attributes looked good. In fact, I've even made a form template for VS2008 that includes this attribute by default.
Vilx-
If I make the menu centralized, then adding a new form (which in 99% cases will need a single menu item) will become more difficult.
Vilx-
Well, it's only modifying a single file. It also means that it's *much* easier to get an idea of what your overall menu structure is just by looking at the file rather than looking for the attribute in *every* form.
Jon Skeet
Well, that's true. Then again - you could just run the application and see. :)
Vilx-
+3  A: 

The lexical ordering of elements in a file is absolutely not guaranteed to be persisted in anyway in the resulting CIL assemblies nor to be respected in the results returned from Reflection. This ordering is not even guaranteed to be the same over repeated calls within the same app domain!

Note that MS have broken this ordering in other parts of reflection in the past (Noting as they did it that this actually caused some issues for some of their code), thus even if it happens to work at the moment there is nothing stopping this breaking in future or on different platforms.

Consider changing your attribute model to allow expressing the semantic information directly rather than relying on ordering.

ShuggyCoUk
A: 

It seems to me that your problem really stems from the fact that the forms are specifying where they appear in your menu, which means you're trying to build a menu by combining all the forms in an assembly. It might be easier if you specify the structure of the menu separately from any forms, and resolve the forms from various propeties/attributes defined on the class corresponding to a menu item.

e.g.

public class MenuItem
{
    string Text { get; }
    Type FormType { get; }
    ICollection<MenuItem> SubItems { get; }
}

Then when a menu-item is selected, resolve the form somehow and display it. This approach has the disadvantage that any new forms need require a change to the code specifying the menu structure along with the form itself, but that will be quite minor...

Lee
This is what I wanted to avoid. Right now adding a new form is like three clicks of the mouse when using a template I've made (which includes this attribute).
Vilx-