2

Given the following code:

using System.ComponentModel.DataAnnotations;
using System.Linq;
using Xunit;

namespace Company.Tests
{
    public class MyObject
    {
        [Display(Order = 1000)]
        public virtual string StringPropertyB { get; set; }

        [Display(Order = 2000)]
        public virtual string StringPropertyA { get; set; }
    }

    public class MyObjectTest
    {
        [Fact]
        public void X()
        {
            var properties = typeof(MyObject).GetProperties();
            var stringPropertyBPropertyInfo = properties[0];
            var stringPropertyAPropertyInfo = properties[1];

            // Debugger Display = "{[System.ComponentModel.DataAnnotations.DisplayAttribute(Order = 1000)]}"
            var bDisplayAttribute = stringPropertyBPropertyInfo.GetCustomAttributesData().FirstOrDefault();

            // Debugger Display = "{[System.ComponentModel.DataAnnotations.DisplayAttribute(Order = 2000)]}"
            var aDisplayAttribute = stringPropertyAPropertyInfo.GetCustomAttributesData().FirstOrDefault();
        }
    }
}

How do I update the Order property programmatically?

I want to update it after it's been set to a new value. (Use case being not having to specify the Order but instead assign the Order a value automatically to match the order that the properties on MyObject appear from top to bottom.)

vsdev
  • 33
  • 1
  • 5
  • If you need to change it, I'm thinking you might want to just store it in a property instead. – KSib Sep 14 '16 at 19:23
  • 2
    I might be wrong, but it seems to me as though modifying attributes at runtime would be paradigm breaking. If I were you, I'd reorder your properties some other way. – Sam I am says Reinstate Monica Sep 14 '16 at 19:24
  • But I'm so close... :) Been trying... `var property = cad.AttributeType.GetProperty("Order"); property.SetValue(whatToUseHere, 30000);` Just can't figure out what to use for whatToUseHere – vsdev Sep 14 '16 at 19:41
  • This may help point you in the right direction: http://stackoverflow.com/questions/2160476/how-to-set-attributes-values-using-reflection I also know you can access attributes at runtime, using reflection: public static object GetAttribute(object parent, Type attributeType) { var property = parent.GetType().GetProperties().Where(prop => Attribute.IsDefined(prop, attributeType)).Single(); var attribute = property.GetCustomAttribute(attributeType); return attribute; } – Thumper Sep 14 '16 at 20:43
  • Also, you can also check if the object has a private "set" value... If I have time later, today, and no one else has figured it out, I'll throw something together and see if I can POC it for you. – Thumper Sep 14 '16 at 20:47

3 Answers3

1

Just as Mr Skeet wrote in the answer that Thumper pointed to this can't be done as far as I can tell. You can update the Order value but it won't persist:

using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using Shouldly;
using Xunit;

namespace Tests
{
    public class ObjectWithDisplayOrder
    {
        [Display(Order = 0)]
        public virtual string StringPropertyB { get; set; }

        [Display(Order = 0)]
        public virtual string StringPropertyA { get; set; }
    }

    public class DisplayOrderTests
    {
        [Fact]
        public void ShouldUpdateDisplayOrderProperty()
        {
            const int updatedOrderValue = 1000;

            var properties = typeof(ObjectWithDisplayOrder).GetProperties();
            foreach (var property in properties)
            {
                var displayAttribute = (DisplayAttribute) property.GetCustomAttributes().First(a => a is DisplayAttribute);
                var props = displayAttribute.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public).ToList();
                props.Single(p => p.Name == "Order").SetValue(displayAttribute, updatedOrderValue);
                // displayAttribute Order is 1000 here, but it's not persisted...
            }

            foreach (var property in properties)
            {
                var displayAttribute = (DisplayAttribute) property.GetCustomAttributes().First(a => a is DisplayAttribute);
                displayAttribute.GetOrder().ShouldBe(updatedOrderValue); // Fails - Order is still 0
            }
        }
    }
}
vsdev
  • 33
  • 1
  • 5
0

Instead of what you have do this

public class MyObject
{
    [DefaultValue(1000)]
    public virtual int StringPropertyBOrder { get; set; }
    public virtual string StringPropertyB { get; set; }

    [DefaultValue(2000)]
    public virtual int StringPropertyAOrder { get; set; }
    public virtual string StringPropertyA { get; set; }
}
Paul Swetz
  • 2,234
  • 1
  • 11
  • 28
0

Updated Answer: Because attributes are "static metadata", they cannot be persistently changed after compilation. (Can attributes be added dynamically in C#?)

So, one way to accomplish OP's request to make persistent changes to Attribute values is to compile the code at runtime. If this is what's needed, it may be worth seriously considering a different approach to the problem you are trying to solve, since this means a lot more work for you (but it's super cool to me).

There is also the possiblity of using something called TypeBuilder (How Can I add properties to a class on runtime in C#?). I am not familiar with this, but it looks promising. It does also look like it uses runtime compilation.

Runtime Compiling:

Here is one way this can be accomplished, by compiling code at runtime. (Using NUnit, not XUnit, as OP has done):

namespace OpUnitTest {

    [TestClass]
    public class OpTest{

        //Use some web templating model so we can easily change it later (#=variable#)
        string myClassToCompile = @"

using System.ComponentModel.DataAnnotations;

namespace Test {
    public class ObjectWithDisplayOrder {
        [Display(Order = #=0#)]
        public virtual string StringPropertyB { get; set; }

        [Display(Order = #=1#)]
        public virtual string StringPropertyA { get; set; }
    }
}
";
        [TestMethod]
        public void AssignAtributeValuesDynamically() {

            const int order = 1000;     

            //Escape curly braces.
            myClassToCompile = myClassToCompile.Replace("{", "{{").Replace("}", "}}");

            //We could use Regex, or even a for loop, to make this more-elegant and scalable, but this is a Proof of Concept.
            myClassToCompile = myClassToCompile.Replace("#=0#", "{0}").Replace("#=1#", "{1}");

            myClassToCompile = string.Format(myClassToCompile, order, order);

            CSharpCodeProvider provider = new CSharpCodeProvider();
            CompilerParameters parameters = new CompilerParameters();
            parameters.ReferencedAssemblies.Add("System.ComponentModel.DataAnnotations.dll");
            parameters.GenerateInMemory = true;
            CompilerResults results = provider.CompileAssemblyFromSource(parameters, myClassToCompile);

            //You would normally check for compilation errors, here.

            Assembly assembly = results.CompiledAssembly;
            Type myCompiledObject = assembly.GetType("Test.ObjectWithDisplayOrder");
            PropertyInfo[] properties = myCompiledObject.GetProperties();

            foreach (var property in properties) {
                var displayAttribute = (DisplayAttribute)property.GetCustomAttributes().First(a => a is DisplayAttribute);
                Assert.AreEqual(order, displayAttribute.GetOrder());

            }
        }
}

A good ramp-up guide on runtime compiling in C# (something that is a bit of a passion of mine): http://www.codeproject.com/Tips/715891/Compiling-Csharp-Code-at-Runtime

Original Answer:

Technically, it demonstrates that you can, indeed, change the attribute's value, but as OP pointed out - this doesn't persist.

using System.ComponentModel.DataAnnotations;
using System.Linq;
using Xunit;

namespace Company.Tests
{
    public class MyObject
    {
        [Display(Order = 1000)]
        public virtual string StringPropertyB { get; set; }

        [Display(Order = 2000)]
        public virtual string StringPropertyA { get; set; }
    }

    public class MyObjectTest
    {
        [Fact]
        public void X()
        {
            var properties = typeof(MyObject).GetProperties();
            var stringPropertyBPropertyInfo = properties[0];
            var bDisplayAttribute = (DisplayAttribute)stringPropertyBPropertyInfo.GetCustomAttributes().First();
            var props = bDisplayAttribute.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public).ToList();
            props.Single(p => p.Name == "Order").SetValue(bDisplayAttribute, 5);

        }
    }
}
Community
  • 1
  • 1
Thumper
  • 525
  • 1
  • 6
  • 21
  • Thank you, but I can't get the values to persist, see my answer. But "A" for effort! :) – vsdev Sep 15 '16 at 07:03
  • I'm determined to make this work. I'll see what I can do with your answer (which you should have posted as an update to your post) – Thumper Sep 15 '16 at 16:24
  • @vsdev - check it. It, technically, answers your question - but it means you'd have a heck of a lot more work to do. It might be worth looking at a different way to do this - what is your use case for needing to use "Display"? – Thumper Sep 15 '16 at 16:57
  • Thanks, the framework that I use dynamically scans for the DisplayAttribute and sets up the order in the UI based on the Order of that attribute. So I can't use runtime compilation or anything like that. It's not that big of hassle to set the Order property manually so that the sort order is correct. I was just looking for a way to set defaults. Thanks, I'll accept your answer. :) – vsdev Sep 16 '16 at 10:14
  • Hmm... if it is dynamically scanning, you should be able to compile at runtime. Also, is the framework something under your control? Because if it is, you can definitely look into the model CsvReader uses - it allows decorating properties at runtime. You could use the mapping + set up the framework to support it. – Thumper Sep 16 '16 at 15:18
  • Funny thing is that with the ReadOnlyAttribute it does work and persist. I'm using it with the Xceed PropertyGrid. I've tried to do the same (setting the private filed _order) in the DisplayAttribute with no success. I'm still not sure where the difference is, as soon as i have some time i will compare both... – Marco Apr 27 '18 at 10:36