Изменить параметр атрибута во время выполнения
Я не уверен, возможно ли изменить параметр атрибута во время выполнения? Например, внутри сборки у меня есть следующий класс
public class UserInfo
{
[Category("change me!")]
public int Age
{
get;
set;
}
[Category("change me!")]
public string Name
{
get;
set;
}
}
Это класс, предоставляемый сторонним поставщиком, и я не могу изменить код. Но теперь я обнаружил, что приведенные выше описания не являются точными, и я хочу изменить имя категории "изменить меня" на что-то другое, когда я привяжу экземпляр вышеупомянутого класса к сетке свойств.
Могу ли я знать, как это сделать?
10 ответов
Ну ты каждый день узнаешь что-то новое, видимо я солгал
Что обычно не осознается, так это то, что вы можете довольно легко изменять значения экземпляра атрибута во время выполнения. Причина, конечно, в том, что экземпляры создаваемых классов атрибутов являются совершенно нормальными объектами и могут использоваться без ограничений. Например, мы можем получить объект:
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);
В случае, если кто-то еще идет по этому пути, ответ таков: вы можете сделать это с отражением, за исключением того, что вы не можете этого сделать, потому что в фреймворке есть ошибка. Вот как бы вы это сделали:
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")
Все хорошо, за исключением того, что атрибут категории изменен для всех свойств, а не только для "Возраст".
Вы можете довольно легко создать подкласс для большинства общих атрибутов, чтобы обеспечить эту расширяемость:
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}
}}});
}
}
Есть более сложные варианты, которые включают в себя написание PropertyDescriptor
s, выставленный через TypeConverter
, ICustomTypeDescriptor
или же TypeDescriptionProvider
- но это обычно излишне.
К сожалению, атрибуты не предназначены для изменения во время выполнения. У вас есть два варианта:
Воссоздайте похожий тип на лету, используя
System.Reflection.Emit
как показано ниже.Попросите вашего поставщика добавить эту функцию. Если вы используете 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); } }
Для первого варианта: это, однако, отсутствие надлежащей привязки свойства, чтобы отразить результат обратно к фактическому редактируемому объекту.
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;
}
}
Вы решили проблему?
Вот возможные шаги для достижения приемлемого решения.
- Попробуйте создать дочерний класс, переопределите все свойства, необходимые для изменения
[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); ... }
Позднее редактирование: эта часть решения не работает, если у вас есть большое количество свойств, которые могут понадобиться для перезаписи атрибутов. Вот где начинается вторая часть:
- Конечно, это не поможет, если класс не наследуется или если у вас много объектов (и свойств). Вам необходимо создать полностью автоматический прокси-класс, который получает ваш класс и создает динамический класс, применяет атрибуты и, конечно, устанавливает связь между двумя классами. Это немного сложнее, но также достижимо. Просто используйте отражение и вы на правильном пути.
Вот "обманчивый" способ сделать это:
Если у вас есть фиксированное число постоянных потенциальных значений для параметра атрибута, вы можете определить отдельное свойство для каждого потенциального значения параметра (и присвоить каждому свойству этот слегка отличающийся атрибут), а затем переключить динамическое свойство, на которое вы ссылаетесь.
В 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
Учитывая, что выбранным элементом PropertyGrid является "Возраст":
SetCategoryLabelViaReflection(MyPropertyGrid.SelectedGridItem.Parent,
MyPropertyGrid.SelectedGridItem.Parent.Label, "New Category Label");
куда 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());
}
}
Что касается программной настройки выбранного элемента, родительскую категорию которого вы хотите изменить; Есть ряд простых решений. Google "Установить фокус на определенное свойство PropertyGrid".
Я действительно так не думаю, разве что есть какое-то странное отражение, способное справиться с этим. Украшения объекта устанавливаются во время компиляции, и, насколько мне известно, исправлены
Тем временем я пришел к частичному решению, основанному на следующих статьях:
- ICustomTypeDescriptor, часть 1
- ICustomTypeDescriptor, часть 2
- Добавить (удалить) элементы в (из) PropertyGrid во время выполнения
По сути, вы бы создали общий класс CustomTypeDescriptorWithResources<T>
, что бы получить свойства через отражение и нагрузку Description
а также Category
из файла (я полагаю, вам нужно отобразить локализованный текст, чтобы вы могли использовать файл ресурсов (.resx
))
Вы можете изменить значения атрибутов во время выполнения на уровне класса (не объект):
var attr = TypeDescriptor.GetProperties(typeof(UserContact))["UserName"].Attributes[typeof(ReadOnlyAttribute)] as ReadOnlyAttribute;
attr.GetType().GetField("isReadOnly", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(attr, username_readonly);