Alterar o parâmetro do atributo em tempo de execução

Não tenho certeza se é possível alterar o parâmetro do atributo durante o tempo de execução? Por exemplo, dentro de uma assembly eu tenho a seguinte class

public class UserInfo { [Category("change me!")] public int Age { get; set; } [Category("change me!")] public string Name { get; set; } } 

Esta é uma class fornecida por um fornecedor de terceiros e não posso alterar o código . Mas agora descobri que as descrições acima não são precisas e quero alterar o nome da categoria “alterar-me” para outra coisa quando eu vincular uma instância da class acima a uma grade de propriedade.

Posso saber como fazer isso?

Bem, você aprende algo novo todos os dias, aparentemente eu menti:

O que geralmente não é percebido é que você pode alterar os valores das instâncias de atributos com bastante facilidade no tempo de execução. A razão é, obviamente, que as instâncias das classs de atributos criadas são objects perfeitamente normais e podem ser usadas sem restrições. Por exemplo, podemos obter o object:

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

… Mude o valor de sua variável pública e mostre que mudou:

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

… E finalmente criar outra instância e mostrar que seu valor é inalterado:

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

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

Caso alguém mais ande por essa avenida, a resposta é que você pode fazê-lo, com reflection, exceto que você não pode, porque há um bug no framework. Veja como você faria isso:

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

Tudo bem, exceto que o atributo de categoria é alterado para todas as propriedades, não apenas para ‘Idade’.

Você pode subclassificar a maioria dos atributos comuns com bastante facilidade para fornecer essa extensibilidade:

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

Existem opções mais complexas que envolvem escrever PropertyDescriptor customizados, expostos via TypeConverter , ICustomTypeDescriptor ou TypeDescriptionProvider – mas isso geralmente é um exagero.

Infelizmente os atributos não devem mudar em tempo de execução. Você basicamente tem duas opções:

  1. Recrie um tipo similar na hora usando System.Reflection.Emit como mostrado abaixo.

  2. Peça ao seu fornecedor para adicionar essa funcionalidade. Se você estiver usando Xceed.WpfToolkit.Extended, você pode fazer o download do código-fonte a partir daqui e implementar facilmente uma interface como IResolveCategoryName que resolveria o atributo em tempo de execução. Eu fiz um pouco mais do que isso, foi muito fácil adicionar mais funcionalidades como limites ao editar um valor numérico em um DoubleUpDown dentro do 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); } } 

Para a primeira opção: No entanto, esta falta de vinculação de propriedade adequada para refletir o resultado de volta ao object real que está sendo editado.

  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().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; } } 

Você resolveu o problema?

Aqui estão os passos possíveis para conseguir uma solução aceitável.

  1. Tente criar uma class filha, redefina todas as propriedades que você precisa para alterar o atributo [Category] (marque-as com o new ). Exemplo:
 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); ... } 

Edição posterior: essa parte da solução não é viável se você tiver um grande número de propriedades que talvez precise rewrite os atributos. É aí que entra a parte dois:

  1. Naturalmente, isso não ajudará se a class não for herdável ou se você tiver muitos objects (e propriedades). Você precisaria criar uma class de proxy automática completa que obtém sua class e cria uma class dinâmica, aplica atributos e, é claro, faz uma conexão entre as duas classs. Isso é um pouco mais complicado, mas também viável. Basta usar a reflection e você está no caminho certo.

Dado que o item selecionado do PropertyGrid é “Idade”:

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

Onde SetCategoryLabelViaReflection() é definido da seguinte maneira:

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

No que diz respeito à programação do item selecionado, a categoria pai da qual você deseja mudar; Há várias soluções simples. Google “Defina o foco para uma propriedade PropertyGrid específica”.

Eu realmente não penso assim, a menos que haja alguma reflection funky que pode tirá-lo. As decorações da propriedade são definidas em tempo de compilation e, até onde sei, são fixas

Nesse meio tempo, cheguei a uma solução parcial, derivada dos seguintes artigos:

  1. ICustomTypeDescriptor, parte 1
  2. ICustomTypeDescriptor, parte 2
  3. Adicionar (remover) itens para (de) PropertyGrid em tempo de execução

Basicamente, você criaria uma class genérica CustomTypeDescriptorWithResources , que obteria as propriedades por meio da reflection e carregar Description e Category de um arquivo (suponho que você precise exibir texto localizado para poder usar um arquivo de resources ( .resx ))

Aqui está uma maneira “barata” de fazer isso:

Se você tiver um número fixo de valores de potencial constantes para o parâmetro de atributo, poderá definir uma propriedade separada para cada valor potencial do parâmetro (e fornecer a cada propriedade um atributo ligeiramente diferente) e, em seguida, alternar qual propriedade você faz referência dinamicamente.

Em VB.NET, pode parecer com isso:

 Property Time As Date  ReadOnly Property TimeMonthly As Date Get Return Time End Get End Property  ReadOnly Property TimeQuarterly As Date Get Return Time End Get End Property  ReadOnly Property TimeYearly As Date Get Return Time End Get End Property 

Você pode alterar os valores de Atributo no tempo de execução no nível de Classe (não 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);