简体   繁体   中英

How do I update the Order property on a DisplayAttribute programmatically?

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.)

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
            }
        }
    }
}

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; }
}

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);

        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM