Оценка 2272
[+4/-0]
Оценить ![]() ![]() ![]() ![]() ![]() ![]()
|
PropertyGrid – удобный компонент для визуального редактирования свойств объектов. Объект для редактирования задается в дизайнере WinForms, либо непосредственно в коде:
private PersonData _personData = new PersonData();
propertyGrid1.SelectedObject = _personData;
|
Хотя многие стандартные типы PropertyGrid редактировать умеет (см. рисунок 1), любое практическое применение требует все же ручной доводки.

Рисунок 1.
В данном FAQ собраны ответы на некоторые вопросы, возникающие при использовании PropertyGrid.
Для этого предназначен атрибут DisplayName:
using System.ComponentModel;
...
[DisplayName("День рождения")]
public DateTime Birthday
{
get { return _birthday; }
set { _birthday = value; }
}
|

Рисунок 2.
Для этого предназначен атрибут Description:
[DisplayName("День рождения")]
[Description("День рождения он день рождения и есть")]
public DateTime Birthday
{
get { return _birthday; }
set { _birthday = value; }
}
|

Рисунок 3.
Используйте атрибут Category:
[DisplayName("ФИО")] [Description("Фамилия Имя Отчество")] [Category("1. Идентификация")] publicstring Name { get { return _name; } set { _name = value; } } /// <summary>/// День рождения/// </summary> [DisplayName("День рождения")] [Description("День рождения он день рождения и есть")] [Category("2. Общие")] public DateTime Birthday { get { return _birthday; } set { _birthday = value; } } |

Рисунок 4.
Можно либо само свойство сделать read-only (оставив только get), либо использовать атрибут ReadOnly:
[DisplayName("ID")] [Description("Идентификатор")] [Category("1. Идентификация")] [ReadOnly(true)] publicint Id { get { return _id; } set { _id = value; } } |

Рисунок 5.
Используйте атрибут TypeConverter:
[DisplayName("Наличие страховки")] [Description("Наличие страховки")] [Category("3. Дополнительно")] [TypeConverter(typeof(BooleanTypeConverter))] publicbool Insurance { get { return _insurance; } set { _insurance = value; } } |
Реализация BooleanTypeConverter проста:
class
BooleanTypeConverter : BooleanConverter
{
publicoverrideobject ConvertTo(ITypeDescriptorContext context,
CultureInfo culture,
object value,
Type destType)
{
return (bool)value ?
"Есть" : "Нет";
}
publicoverrideobject ConvertFrom(ITypeDescriptorContext context,
CultureInfo culture,
object value)
{
return (string)value == "Есть";
}
}
|

Рисунок 6.
Необходимо задать атрибут Description с нужным именем для каждого члена перечисления:
enum SEX
{
[Description("Муж.")]
Man,
[Description("Жен.")]
Woman,
[Description("Неизв.")]
Unknown
}
|
Реализовать EnumTypeConverter, осуществляющий преобразование к строке с учетом атрибута Description:
и задать атрибут TypeConverter для отображаемого свойства:
[DisplayName("Пол")] [Description("Пол")] [Category("2. Общие")] [TypeConverter(typeof(EnumTypeConverter))] public SEX Sex { get {return _sex;} set {_sex = value;} } |

Рисунок 7.
Необходимо реализвать своего наследника от UITypeEditor с кодом отрисовки (в данном случае изображения хранятся в ресурсах с именами, соответствующими именам членов перечисления):
/// <summary>
/// Добавляет картинки, соответствующие каждому члену перечисления
/// </summary>
public
class
SexEditor : UITypeEditor
{
publicoverridebool GetPaintValueSupported(ITypeDescriptorContext context)
{
returntrue;
}
publicoverridevoid PaintValue(PaintValueEventArgs e)
{
// картинки хранятся в ресурсах с именами, соответствующими// именам каждого члена перечисления SEX
string resourcename = ((SEX)e.Value).ToString();
// достаем картинку из ресурсов
Bitmap sexImage =
(Bitmap)Resources.ResourceManager.GetObject(resourcename);
Rectangle destRect = e.Bounds;
sexImage.MakeTransparent();
// и отрисовываем
e.Graphics.DrawImage(sexImage, destRect);
}
}
|
и привязать его с помощью атрибута Editor к редактируемому свойству:
[DisplayName("Пол")] [Description("Пол")] [Category("2. Общие")] [TypeConverter(typeof(EnumTypeConverter))] [Editor(typeof(SexEditor), typeof(UITypeEditor))] public SEX Sex { get { return _sex; } set { _sex = value; } } |

Рисунок 8.
Необходимо реализовать TypeConverter, предоставляющий список, из которого можно будет делать выбор:
/// <summary>
/// TypeConverter для списка должностей
/// </summary>
class
PostTypeConverter : StringConverter
{
/// <summary>/// Будем предоставлять выбор из списка/// </summary>publicoverridebool GetStandardValuesSupported(
ITypeDescriptorContext context)
{
returntrue;
}
/// <summary>/// ... и только из списка/// </summary>publicoverridebool GetStandardValuesExclusive(
ITypeDescriptorContext context)
{
// false - можно вводить вручную// true - только выбор из спискаreturntrue;
}
/// <summary>/// А вот и список/// </summary>publicoverride StandardValuesCollection GetStandardValues(
ITypeDescriptorContext context)
{
// возвращаем список строк из настроек программы// (базы данных, интернет и т.д.)returnnew StandardValuesCollection(Settings.Default.PostList);
}
}
|
В данном случае возвращается список строк из настроек программы. Затем нужно задать этот класс в качестве параметра атрибута TypeConverter для редактируемого свойства:
/// <summary>
/// Должность
/// </summary>
[DisplayName("Должность")]
[Description("Занимаемая должность согласно штатного расписания")]
[Category("3. Дополнительно")]
[TypeConverter(typeof(PostTypeConverter))]
publicstring Post
{
get { return _post; }
set { _post = value; }
}
|

Рисунок 9.
Аналогично реализуется и выбор из списка фиксированных значений например double – см. класс PossibleValuesTypeConverter в тестовом проекте.
Свойства типа StringCollection
[DisplayName("Дети")]
[Description("Дети")]
[Category("2. Общие")]
[PropertyOrder(30)]
publicStringCollection Children
{
get { return _children; }
set { _children = value; }
}
|
открываются для редактирования в PropertyGrid, но при попытке добавить строку к списку выдается сообщение об ошибке “Конструктор для типа "System.String" не найден” (Constructor on type 'System.String' not found):
Рисунок 10.
Исправить положение можно добавив атрибут Editor со следующими параметрами:
[DisplayName("Дети")]
[Description("Дети")]
[Category("2. Общие")]
[PropertyOrder(30)]
[Editor(
"System.Windows.Forms.Design.StringCollectionEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
"System.Drawing.Design.UITypeEditor,System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
)]
public StringCollection Children
{
get { return _children; }
set { _children = value; }
}
|
Окно редактирования StringCollection примет при этом следующий вид:
Рисунок 11.
Бывает так, что тип свойства является сложным объектом, также имеющим свойства. Хотелось бы иметь возможность редактировать свойства этого объекта в раскрывающемся списке. К классу, используемому в качестве типа составного свойства, необходимо применить атрибут TypeConverter с ExpandableObjectConverter в качестве параметра:
/// <summary>
/// Данные, входящие в адрес
/// </summary>
[TypeConverter(typeof(ExpandableObjectConverter))]
class AddressData
{
/// <summary>/// Конструктор/// </summary>public AddressData(string town, string street, uint house)
{
_town = town;
_street = street;
_house = house;
}
privatestring _town;
/// <summary>/// Город/// </summary>
[DisplayName("Город")]
[Description("Наименование населенного пункта")]
publicstring Town
{
get { return _town; }
set { _town = value; }
}
privatestring _street;
/// <summary>/// Улица/// </summary>
[DisplayName("Улица")]
[Description("Название улицы")]
publicstring Street
{
get { return _street; }
set { _street = value; }
}
privateuint _house;
/// <summary>/// Номер дома/// </summary>
[DisplayName("Дом")]
[Description("Номер дома")]
publicuint House
{
get { return _house; }
set { _house = value; }
}
/// <summary>/// Представление в виде строки/// </summary>publicoverridestring ToString()
{
return _town + ", " + _street + " - " + _house;
}
}
|
Задавать дополнительные атрибуты для редактируемого свойства не нужно:
[DisplayName("Место жительства")] [Description("Адрес")] [Category("3. Дополнительно")] publicAddressData Address { get { return _address; } set { _address = value; } } |

Рисунок 12.
Задайте атрибут Editor:
[DisplayName("Личное дело")] [Description("Имя файла личного дела")] [Category("3. Дополнительно")] [Editor(typeof(DocFileEditor), typeof(UITypeEditor))] publicstring PersonalFileName { get { return _personalfilename; } set { _personalfilename = value; } } |
Собственно фильтр расширений задается в DocFileEditor:
class
DocFileEditor : FileNameEditor
{
/// <summary>/// Настройка фильтра расширений /// </summary>protectedoverridevoid InitializeDialog(OpenFileDialog ofd)
{
ofd.CheckFileExists = false;
ofd.Filter = "Doc files (*.doc)|*.doc|All files (*.*)|*.*";
}
}
|

Рисунок 13.

Рисунок 14.
Необходимо реализовать наследника UITypeEditor, обеспечивающего вызов нужной формы (в данном случае IPAddressEditorForm), передачу ей редактируемого значения и получение результата:
public
class
IPAddressEditor : UITypeEditor
{
/// <summary>/// Реализация метода редактирования/// </summary>publicoverride Object EditValue(
ITypeDescriptorContext context,
IServiceProvider provider,
Object value)
{
if((context != null) && (provider != null))
{
IWindowsFormsEditorService svc =
(IWindowsFormsEditorService)
provider.GetService(typeof(IWindowsFormsEditorService));
if(svc!= null)
{
using (IPAddressEditorForm ipfrm =
new IPAddressEditorForm((IPAddress)value))
{
if (svc.ShowDialog(ipfrm) == DialogResult.OK)
{
value = ipfrm.IP;
}
}
}
}
returnbase.EditValue(context, provider, value);
}
/// <summary>/// Возвращаем стиль редактора - модальное окно/// </summary>publicoverride UITypeEditorEditStyle GetEditStyle(
ITypeDescriptorContext context)
{
if (context != null)
returnUITypeEditorEditStyle.Modal;
elsereturnbase.GetEditStyle(context);
}
}
|
а затем привязать его к редактируемому свойству при помощи атрибута Editor:
[DisplayName("IP адрес")] [Description("IP адрес компьютера рабочего места")] [Category("3. Дополнительно")] [Editor(typeof(IPAddressEditor), typeof(UITypeEditor))] public IPAddress IPaddress { get { return _ipAddress; } set { _ipAddress = value; } } |

Рисунок 15.
Опять же, реализовать своего наследника UITypeEditor, отображающего выпадающий список с нужным control-ом, передать ему исходное значение редактируемого свойства и принять результат по окончании редактирования (ForeignLangsControl – составной UserControl c элементами, необходимыми для редактирования свойства):
public
class
ForeignLangsDropDownEditor : UITypeEditor
{
/// <summary>/// Реализация метода редактирования/// </summary>publicoverride Object EditValue(
ITypeDescriptorContext context,
IServiceProvider provider,
Object value)
{
if((context != null) && (provider != null))
{
IWindowsFormsEditorService svc =
(IWindowsFormsEditorService)
provider.GetService(typeof(IWindowsFormsEditorService));
if(svc!= null)
{
ForeignLangsControl flctrl =
new ForeignLangsControl((ForeignLangs)value);
flctrl.Tag = svc;
svc.DropDownControl(flctrl);
value = flctrl.Foreignlangs;
}
}
returnbase.EditValue(context, provider, value);
}
/// <summary>/// Возвращаем стиль редактора - выпадающее окно/// </summary>publicoverride UITypeEditorEditStyle GetEditStyle(
ITypeDescriptorContext context)
{
if (context != null)
returnUITypeEditorEditStyle.DropDown;
elsereturnbase.GetEditStyle(context);
}
}
|
и, соответственно, привязать полученный редактор к редактируемому свойству:
[DisplayName("Иностанные языки")] [Description("Какими иностранными языками владеет")] [Category("3. Дополнительно")] [Editor(typeof(ForeignLangsDropDownEditor), typeof(UITypeEditor))] public ForeignLangs Foreignlangs { get { return _fl; } set { _fl = value; } } |

Рисунок 16.
Используйте атрибут PasswordPropertyText:
[DisplayName("Пароль")]
[Description("Пароль для доступа на сервер компании")]
[Category("3. Дополнительно")]
[PropertyOrder(80)]
[PasswordPropertyText(true)]
publicstring Password
{
get { return _password; }
set { _password = value; }
}
|

Рисунок 17.
Стандартный атрибут Browsable позволяет задавать видимость свойства в PropertyGrid только на этапе написания кода. Чтобы управлять видимостью свойства в зависимости от значения другого свойства настраиваемого объекта, понадобятся новый атрибут – DynamicPropertyFilter и базовый класс – FilterablePropertyBase:
Указываем базовый класс для класса настраиваемого объекта:
/// <summary>
/// Данные для редактирования в PropertyGrid
/// </summary>
class PersonData : FilterablePropertyBase
{
public PersonData()
... |
| ПРИМЕЧАНИЕ Решение не очень красивое, так как делает данные зависимыми от средств отображения и редактирования. – прим. ред. |
А для свойства, видимость которого зависит от другого свойства – атрибут DynamicPropertyFilter:
/// <summary>
/// Должность
/// </summary>
[DisplayName("Должность")]
[Description("Занимаемая должность согласно штатному расписанию")]
[Category("3. Дополнительно")]
[TypeConverter(typeof(PostTypeConverter))]
publicstring Post
{
get { return _post; }
set { _post = value; }
}
/// <summary>/// Имя файла личного дела/// </summary>
[DisplayName("Личное дело")]
[Description("Имя файла личного дела")]
[Category("3. Дополнительно")]
[Editor(typeof(DocFileEditor), typeof(UITypeEditor))]
[DynamicPropertyFilter("Post", "Уборщик, Инженер, Начальник отдела, Начальник сектора, Секретарь")]
publicstring PersonalFileName
{
get { return _personalfilename; }
set { _personalfilename = value; }
}
|
Также нужно добавить обработчик события PropertyGrid – PropertyValueChanged:
private
void propertyGrid1_PropertyValueChanged(
object s, PropertyValueChangedEventArgs e)
{
propertyGrid1.Refresh();
}
|
В данном случае свойство PersonalFileName (Имя файла личного дела) будет показано в PropertyGrid только тогда, когда свойство Post (Должность) будет иметь любое из указанных в атрибуте значений:

Рисунок 18.
Если переключить свойство Должность в значение, отсутствующее в параметре атрибута DynamicPropertyFilter, свойство Личное дело исчезнет из списка отображаемых свойств:

Рисунок 19.
Аналогично, вторым параметром атрибута DynamicPropertyFilter можно передавать значения параметров других типов, например, перечисления:
[DynamicPropertyFilter("Sex", "Woman,Unknown")] |
или bool:
[DynamicPropertyFilter("Insurance", "True")] |
Используйте атрибут TypeConverter:
[DisplayName("Телефоны")] [Description("Список номеров телефонов")] [Category("3. Дополнительно")] [TypeConverter(typeof(CollectionTypeConverter))] public List<PhoneNumber> Phones { get { return _phones; } set { _phones = value; } } |
Реализация CollectionTypeConverter проста:
class
CollectionTypeConverter : TypeConverter
{
/// <summary>/// Только в строку/// </summary>publicoverridebool CanConvertTo(
ITypeDescriptorContext context, Type destType)
{
return destType == typeof (string);
}
/// <summary>/// И только так/// </summary>publicoverrideobject ConvertTo(
ITypeDescriptorContext context, CultureInfo culture,
object value, Type destType)
{
return"< Список... >";
}
}
|

Рисунок 20.
При переходе к редактированию коллекции отобразится стандартное окно Collection Editor с данными редактируемой коллекции:

Рисунок 21.
“Members”, “properties”, “Add” и “Remove” можно заменить русскими аналогами (при наличии установленного .NET Framework 2.0 Russian Language Pack) переключением CurrentUICulture,как это сделано в методе Main() тестовой программы:
Thread.CurrentThread.CurrentUICulture = new CultureInfo("ru-RU", false); |
Однако, если надпись “Члены” над списком телефонов вас тоже не устраивает, можно применить и более радикальный способ. Заодно решим и следующую проблему.
Добавим к свойству-коллекции атрибут Editor со специализированной версией CollectionEditor:
[DisplayName("Телефоны")] [Description("Список номеров телефонов")] [Category("3. Дополнительно")] [TypeConverter(typeof(CollectionTypeConverter))] [Editor(typeof(PhoneNumbersCollectionEditor), typeof(UITypeEditor))] public List<PhoneNumber> Phones { get { return _phones; } set { _phones = value; } } |
Новая реализация CollectionEditor, PhoneNumbersCollectionEditor, умеет запоминать положение и размеры своего окна, меняет стандартные подписи на соответствующие редактируемым данным и добавляет окно с расширенной подсказкой по свойствам:
Результат что называется, налицо:

Рисунок 22.
Нужно реализовать новый атрибут для задания порядка сортировки – PropertyOrderAttribute, и наследника ExpandableObjectConverter (PropertySorter), возвращающего список свойств, упорядоченный согласно значениям, заданным для них в атрибуте PropertyOrder:
Задаем атрибут TypeConverter с параметром PropertySorter для всего класса с настраиваемыми свойствами:
/// <summary>
/// Данные для редактирования в PropertyGrid
/// </summary>
[TypeConverter(typeof(PropertySorter))]
class PersonData : FilterablePropertyBase
{
|
и указываем атрибут PropertyOrder для упорядочиваемых свойств:
[DisplayName("Место жительства")] [Description("Адрес")] [PropertyOrder(30)] [Category("3. Дополнительно")] public AddressData Address ... [DisplayName("Наличие страховки")] [Description("Наличие страховки")] [PropertyOrder(40)] [Category("3. Дополнительно")] [TypeConverter(typeof(BooleanTypeConverter))] publicbool Insurance ... [DisplayName("Телефоны")] [Description("Список номеров телефонов")] [PropertyOrder(50)] [Category("3. Дополнительно")] [TypeConverter(typeof(CollectionTypeConverter))] [Editor(typeof(PhoneNumbersCollectionEditor), typeof(UITypeEditor))] public List<PhoneNumber> Phones ... [DisplayName("Личное дело")] [Description("Имя файла личного дела")] [Category("3. Дополнительно")] [PropertyOrder(20)] [Editor(typeof(DocFileEditor), typeof(UITypeEditor))] [DynamicPropertyFilter("Post", "Уборщик, Инженер, Начальник отдела, Начальник сектора, Секретарь")] publicstring PersonalFileName ... [DisplayName("IP-адрес")] [Description("IP-адрес компьютера рабочего места")] [PropertyOrder(70)] [Category("3. Дополнительно")] [Editor(typeof(IPAddressEditor), typeof(UITypeEditor))] public IPAddress IPaddress ... [DisplayName("Иностранные языки")] [Description("Какими иностранными языками владеет")] [PropertyOrder(60)] [Category("3. Дополнительно")] [Editor(typeof(ForeignLangsDropDownEditor), typeof(UITypeEditor))] public ForeignLangs Foreignlangs ... [DisplayName("Должность")] [Description("Занимаемая должность согласно штатного расписания")] [Category("3. Дополнительно")] [PropertyOrder(10)] [TypeConverter(typeof(PostTypeConverter))] publicstring Post ... |
результат – свойства в заданном порядке:

Рисунок 23.
Это можно сделать при помощи следующих функций:
/// <summary>
/// Сохранение положения разделителя в гриде
/// </summary>
private
void
SaveGridSplitterPos()
{
Type type = propertyGrid1.GetType();
FieldInfo field = type.GetField("gridView",
BindingFlags.NonPublic | BindingFlags.Instance);
object valGrid = field.GetValue(propertyGrid1);
Type gridType = valGrid.GetType();
Settings.Default.GridSplitterPos = (int)gridType.InvokeMember(
"GetLabelWidth",
BindingFlags.Public | BindingFlags.InvokeMethod | BindingFlags.Instance,
null,
valGrid, newobject[] { });
Trace.WriteLine("SaveGridSplitterPos(): "
+ Settings.Default.GridSplitterPos);
}
/// <summary>/// Восстановление положения разделителя в гриде/// </summary>privatevoidRestoreGridSplitterPos()
{
try
{
Type type = propertyGrid1.GetType();
FieldInfo field = type.GetField("gridView",
BindingFlags.NonPublic | BindingFlags.Instance);
object valGrid = field.GetValue(propertyGrid1);
Type gridType = valGrid.GetType();
gridType.InvokeMember("MoveSplitterTo",
BindingFlags.NonPublic | BindingFlags.InvokeMethod
| BindingFlags.Instance,
null,
valGrid, newobject[] { Settings.Default.GridSplitterPos });
Trace.WriteLine("RestoreGridSplitterPos(): "
+ Settings.Default.GridSplitterPos);
}
catch
{
Trace.WriteLine("MainForm::RestoreGridSplitterPos() exception");
}
}
|
Вызываются они соответственно перед закрытием и перед загрузкой окна формы, содержащей PropertyGrid:
private
void
MainForm_Load(object sender, EventArgs e)
{
Trace.WriteLine("MainForm_Load()");
//устанавливаем редактируемый объект
propertyGrid1.SelectedObject = _personData;
// восстанавливаем положение окна
RestorePos();
// и разделителя колонок в гридеRestoreGridSplitterPos();
}
privatevoidMainForm_FormClosing(object sender, FormClosingEventArgs e)
{
Trace.WriteLine("MainForm_FormClosing()");
// запоминаем положение окна
SavePos();
// и разделителя в гридеSaveGridSplitterPos();
// сохраним возможно измененные значения параметров
Settings.Default.Save();
}
|
| ПРЕДУПРЕЖДЕНИЕ Все имена, фамилии и ip-адреса вымышлены. Любые совпадения случайны. В ходе экспериментов ни один Иван Иванович из г.Бобруйска не пострадал. |
Оценка 2272
[+4/-0]
Оценить ![]() ![]() ![]() ![]() ![]() ![]()
|