C# 在运行时更改属性的参数

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/51269/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-03 10:04:19  来源:igfitidea点击:

Change Attribute's parameter at runtime

提问by Graviton

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.

这是由第三方供应商提供的类,我无法更改代码。但是现在发现上面的描述不准确,想把上面的类的实例绑定到属性网格时,把“change me”类名改成别的东西。

May I know how to do this?

我可以知道怎么做吗?

回答by Glenn Slaven

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

我真的不这么认为,除非有一些时髦的反射可以把它拉下来。属性装饰是在编译时设置的,据我所知是固定的

回答by Glenn Slaven

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

好吧,您每天都在学习新东西,显然我撒了谎:

What isn't generally realised is that you can change attribute instancevalues 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);

通常没有意识到的是,您可以在运行时相当容易地更改属性实例值。原因当然是创建的属性类的实例是完全正常的对象,可以不受限制地使用。例如,我们可以获取对象:

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

...更改其公共变量的值并显示它已更改:

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

...最后创建另一个实例并显示其值不变:

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

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

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

回答by Bogdan Maxim

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:
  1. 尝试创建一个子类,重新定义您需要更改[Category]属性的所有属性(用 标记它们new)。例子:
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);

...

}
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.
  1. 当然,如果类不可继承,或者您有很多对象(和属性),这将无济于事。您需要创建一个全自动代理类来获取您的类并创建一个动态类,应用属性,当然还需要在两个类之间建立连接。这有点复杂,但也是可以实现的。只需使用反射,你就在正确的道路上。

回答by Bogdan Maxim

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
  1. ICustomTypeDescriptor,第 1 部分
  2. ICustomTypeDescriptor,第 2 部分
  3. 在运行时向(从)PropertyGrid 添加(删除)项目

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

基本上,您将创建一个通用类CustomTypeDescriptorWithResources<T>,它将通过反射和加载Description以及Category从文件中获取属性(我想您需要显示本地化文本,以便您可以使用资源文件 ( .resx))

回答by Marc Gravell

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, ICustomTypeDescriptoror TypeDescriptionProvider- but that is usually overkill.

还有更复杂的选项,涉及编写自定义PropertyDescriptors、通过或-公开TypeConverter,但这通常是矫枉过正。ICustomTypeDescriptorTypeDescriptionProvider

回答by Jules

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

一切都很好,除了更改所有属性的类别属性,而不仅仅是“年龄”。

回答by David MacDermot

Given that the PropertyGrid's selected item is "Age":

鉴于 PropertyGrid 的选定项是“年龄”:

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

Where SetCategoryLabelViaReflection()is defined as follows:

其中SetCategoryLabelViaReflection()定义如下:

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

至于以编程方式设置所选项目,您希望更改的父类别;有许多简单的解决方案。谷歌“将焦点设置为特定的 PropertyGrid 属性”。

回答by mmm

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

回答by Darien Pardinas

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.Emitas shown below.

  2. Ask your vendor to add this functionality. If you are using Xceed.WpfToolkit.Extended you can download the source code from hereand easily implement an interface like IResolveCategoryNamethat 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 DoubleUpDowninside 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);
        }
    }
    
  1. 使用System.Reflection.Emit如下所示动态重新创建一个类似的类型。

  2. 请让您的供应商添加此功能。如果您正在使用 Xceed.WpfToolkit.Extended,您可以从这里下载源代码,并轻松实现这样的接口IResolveCategoryName,该接口将在运行时解析属性。我做了更多的工作,在 .DoubleUpDown内部编辑数值时添加更多功能(如限制)非常容易PropertyGrid

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

回答by mbomb007

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:

在 VB.NET 中,它可能如下所示:

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