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/// <summary> /// TypeConverter для Enum, преобразовывающий Enum к строке с /// учетом атрибута Description /// </summary> class EnumTypeConverter : EnumConverter { private Type _enumType; /// <summary>Инициализирует экземпляр</summary>/// <param name="type">тип Enum</param>public EnumTypeConverter(Type type) : base(type) { _enumType = type; } publicoverridebool CanConvertTo(ITypeDescriptorContext context, Type destType) { return destType == typeof(string); } publicoverrideobject ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destType) { FieldInfo fi = _enumType.GetField(Enum.GetName(_enumType, value)); DescriptionAttribute dna = (DescriptionAttribute) Attribute.GetCustomAttribute( fi, typeof(DescriptionAttribute)); if (dna != null) return dna.Description; elsereturn value.ToString(); } publicoverridebool CanConvertFrom(ITypeDescriptorContext context, Type srcType) { return srcType == typeof (string); } publicoverrideobject ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { foreach (FieldInfo fi in _enumType.GetFields()) { DescriptionAttribute dna = (DescriptionAttribute) Attribute.GetCustomAttribute( fi, typeof(DescriptionAttribute)); if ((dna != null) && ((string)value == dna.Description)) return Enum.Parse(_enumType, fi.Name); } return Enum.Parse(_enumType, (string) value); } } |
и задать атрибут 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> /// Атрибут для поддержки динамически показываемых свойств /// </summary> [AttributeUsage(AttributeTargets.Property, Inherited = true)] classDynamicPropertyFilterAttribute : Attribute { string _propertyName; /// <summary>/// Название свойства, от которого будет зависить видимость/// </summary>publicstring PropertyName { get { return _propertyName; } } string _showOn; /// <summary>/// Значения свойства, от которого зависит видимость /// (через запятую, если несколько), при котором свойство, к/// которому применен атрибут, будет видимо. /// </summary>publicstring ShowOn { get { return _showOn; } } /// <summary>/// Конструктор /// </summary>/// <param name="propName">Название свойства, от которого будет /// зависеть видимость</param>/// <param name="value">Значения свойства (через запятую, если несколько), /// при котором свойство, к которому применен атрибут, будет видимо.</param>public DynamicPropertyFilterAttribute(string propertyName, string value) { _propertyName = propertyName; _showOn = value; } } /// <summary>/// Базовый класс для объектов, поддерживающих динамическое /// отображение свойств в PropertyGrid/// </summary>publicclassFilterablePropertyBase : ICustomTypeDescriptor { protected PropertyDescriptorCollection GetFilteredProperties(Attribute[] attributes) { PropertyDescriptorCollection pdc = TypeDescriptor.GetProperties(this, attributes, true); PropertyDescriptorCollection finalProps = new PropertyDescriptorCollection(new PropertyDescriptor[0]); foreach (PropertyDescriptor pd in pdc) { bool include = false; bool dynamic = false; foreach (Attribute a in pd.Attributes) { if (a is DynamicPropertyFilterAttribute) { dynamic = true; DynamicPropertyFilterAttribute dpf = (DynamicPropertyFilterAttribute)a; PropertyDescriptor temp = pdc[dpf.PropertyName]; if (dpf.ShowOn.IndexOf(temp.GetValue(this).ToString()) > -1) include = true; } } if (!dynamic || include) finalProps.Add(pd); } return finalProps; } #region ICustomTypeDescriptor Members public TypeConverter GetConverter() { return TypeDescriptor.GetConverter(this, true); } public EventDescriptorCollection GetEvents(Attribute[] attributes) { return TypeDescriptor.GetEvents(this, attributes, true); } EventDescriptorCollection ICustomTypeDescriptor.GetEvents() { return TypeDescriptor.GetEvents(this, true); } publicstring GetComponentName() { return TypeDescriptor.GetComponentName(this, true); } publicobject GetPropertyOwner(PropertyDescriptor pd) { returnthis; } public AttributeCollection GetAttributes() { return TypeDescriptor.GetAttributes(this, true); } public PropertyDescriptorCollection GetProperties( Attribute[] attributes) { return GetFilteredProperties(attributes); } PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties() { return GetFilteredProperties(new Attribute[0]); } publicobject GetEditor(Type editorBaseType) { return TypeDescriptor.GetEditor(this, editorBaseType, true); } public PropertyDescriptor GetDefaultProperty() { return TypeDescriptor.GetDefaultProperty(this, true); } public EventDescriptor GetDefaultEvent() { return TypeDescriptor.GetDefaultEvent(this, true); } publicstring GetClassName() { return TypeDescriptor.GetClassName(this, true); } #endregion } |
Указываем базовый класс для класса настраиваемого объекта:
/// <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/// <summary> /// Свой CollectionEditor для редактирования списка телефонов абонента - /// для задания заголовка и запоминания положения окна /// </summary> class PhoneNumbersCollectionEditor : CollectionEditor { /// <summary>/// Конструктор/// </summary>public PhoneNumbersCollectionEditor(Type t) : base(t) { Trace.WriteLine("PhoneNumbersCollectionEditor() ctor"); } /// <summary>/// Перекрытый метод создания формы редактора - /// для сохранения/восстановления размеров и положения окна/// </summary>protectedoverride CollectionForm CreateCollectionForm() { CollectionForm collform = base.CreateCollectionForm(); // подключаем свой обработчик открытия и в нем // восстанавливаем положение и размер окна collform.Load += delegate(object sender, EventArgs e) { System.Diagnostics.Trace.WriteLine("collform.Load()"); collform.HelpButton = false; #region Восстановление положения окна и смена заголовка // пока это положение первый раз не сохранено, при // попытке чтения будет кирдыкtry { collform.Size = Settings.Default.PhoneNumbersCollectFormSize; collform.Location = Settings.Default.PhoneNumbersCollectFormLocation; collform.WindowState = Settings.Default.PhoneNumbersCollectFormState; } catch { Trace.WriteLine( "PhoneNumbersCollectionEditor::CreateCollectionForm() exception"); } collform.Text = "Номера телефонов абонента"; #endregion#region Русификация и изменение надписей на форме Label BlaBlaProperties = new Label(); stringMembersText = "&Телефоны:"; stringPropertiesText = "&Номера телефонов:"; // перебираем все control-ы на форме и заменяем// неправильные надписиforeach (Control ctrl in collform.Controls) { foreach (Control ctrl1 in ctrl.Controls) { if (ctrl1.GetType().ToString()== "System.Windows.Forms.Label" && (ctrl1.Text == "&Members:" || ctrl1.Text == "&Члены:")) { ctrl1.Text = MembersText; } if (ctrl1.GetType().ToString() == "System.Windows.Forms.Label" && (ctrl1.Text.Contains("&properties") || ctrl1.Text.Contains("&Cвойства"))) { BlaBlaProperties = (Label)ctrl1; BlaBlaProperties.Text = PropertiesText; } if (ctrl1.GetType().ToString() == "System.ComponentModel.Design.CollectionEditor+FilterListBox") { // это самый правильный обработчик, но после него// срабатывает обработчик формы и меняет надпись на свою//((ListBox)ctrl1).SelectedValueChanged +=// delegate(object sndr, EventArgs ea)// {// Trace.WriteLine("SelectedValueChanged()");// BlaBlaProperties.Text = PropertiesText ;// };// вместо одного правильного - два обходных - // на смену индекса в листбоксе ((ListBox)ctrl1).SelectedIndexChanged += delegate(object sndr, EventArgs ea) { BlaBlaProperties.Text = PropertiesText; }; } if (ctrl1.GetType().ToString() == "System.Windows.Forms.Design.VsPropertyGrid") { // и на редактирование в PropertyGrid ((PropertyGrid)ctrl1).SelectedGridItemChanged += delegate(Object sndr, SelectedGridItemChangedEventArgs segichd) { BlaBlaProperties.Text = PropertiesText; }; // также сделать доступным окно с подсказками по параметрам // в нижней части ((PropertyGrid)ctrl1).HelpVisible = true; ((PropertyGrid)ctrl1).HelpBackColor = System.Drawing.SystemColors.Info; } } } #endregion }; // подключаем свой обработчик закрытия // и в нем сохраняем положение и размер окна collform.FormClosing += delegate(Object sender, FormClosingEventArgs e) { System.Diagnostics.Trace.WriteLine("collform.FormClosing()"); #region Сохранение положения окна Settings.Default.PhoneNumbersCollectFormState = collform.WindowState; if (collform.WindowState == FormWindowState.Normal) { Settings.Default.PhoneNumbersCollectFormSize = collform.Size; Settings.Default.PhoneNumbersCollectFormLocation = collform.Location; } else { Settings.Default.PhoneNumbersCollectFormSize = collform.RestoreBounds.Size; Settings.Default.PhoneNumbersCollectFormLocation = collform.RestoreBounds.Location; } #endregion }; return collform; } } |
Результат что называется, налицо:
Рисунок 22.
Нужно реализовать новый атрибут для задания порядка сортировки – PropertyOrderAttribute, и наследника ExpandableObjectConverter (PropertySorter), возвращающего список свойств, упорядоченный согласно значениям, заданным для них в атрибуте PropertyOrder:
PropertySorterpublic class PropertySorter : ExpandableObjectConverter { #regionМетодыpublicoverridebool GetPropertiesSupported(ITypeDescriptorContext context) { returntrue; } /// <summary>/// Возвращает упорядоченный список свойств/// </summary>publicoverride PropertyDescriptorCollection GetProperties( ITypeDescriptorContext context, object value, Attribute[] attributes) { PropertyDescriptorCollection pdc = TypeDescriptor.GetProperties(value, attributes); ArrayList orderedProperties = new ArrayList(); foreach (PropertyDescriptor pd in pdc) { Attribute attribute = pd.Attributes[typeof(PropertyOrderAttribute)]; if (attribute != null) { // атрибут есть - используем номер п/п из него PropertyOrderAttribute poa = (PropertyOrderAttribute)attribute; orderedProperties.Add(new PropertyOrderPair(pd.Name, poa.Order)); } else { // атрибута нет – считаем, что 0 orderedProperties.Add(new PropertyOrderPair(pd.Name, 0)); } } // сортируем по Order-у orderedProperties.Sort(); // формируем список имен свойств ArrayList propertyNames = new ArrayList(); foreach (PropertyOrderPair pop in orderedProperties) propertyNames.Add(pop.Name); // возвращаемreturn pdc.Sort((string[])propertyNames.ToArray(typeof(string))); } #endregion } #regionPropertyOrder Attribute /// <summary>/// Атрибут для задания сортировки/// </summary> [AttributeUsage(AttributeTargets.Property)] publicclassPropertyOrderAttribute : Attribute { privateint _order; public PropertyOrderAttribute(int order) { _order = order; } publicint Order { get { return _order; } } } #endregion#regionPropertyOrderPair /// <summary>/// Пара имя/номер п/п с сортировкой по номеру/// </summary>publicclass PropertyOrderPair : IComparable { privateint _order; privatestring _name; publicstring Name { get { return _name; } } public PropertyOrderPair(string name, int order) { _order = order; _name = name; } /// <summary>/// Собственно метод сравнения/// </summary>publicint CompareTo(object obj) { int otherOrder = ((PropertyOrderPair)obj)._order; if (otherOrder == _order) { // если Order одинаковый - сортируем по именамstring otherName = ((PropertyOrderPair)obj)._name; returnstring.Compare(_name, otherName); } elseif (otherOrder > _order) return -1; return 1; } } #endregion |
Задаем атрибут 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-адреса вымышлены. Любые совпадения случайны. В ходе экспериментов ни один Иван Иванович из г.Бобруйска не пострадал. |