Сообщений 55 Оценка 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:
EnumTypeConverterи задать атрибут 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:
DynamicPropertyFilterУказываем базовый класс для класса настраиваемого объекта:
/// <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, умеет запоминать положение и размеры своего окна, меняет стандартные подписи на соответствующие редактируемым данным и добавляет окно с расширенной подсказкой по свойствам:
PhoneNumbersCollectionEditorРезультат что называется, налицо:
Рисунок 22.
Нужно реализовать новый атрибут для задания порядка сортировки – PropertyOrderAttribute, и наследника ExpandableObjectConverter (PropertySorter), возвращающего список свойств, упорядоченный согласно значениям, заданным для них в атрибуте PropertyOrder:
PropertySorterЗадаем атрибут 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-адреса вымышлены. Любые совпадения случайны. В ходе экспериментов ни один Иван Иванович из г.Бобруйска не пострадал. |
Сообщений 55 Оценка 2272 [+4/-0] Оценить |