Pull to refresh

PropertyGrid в Visual Studio: отображение полей, связанных с коллекциями объектов

Reading time21 min
Views18K
PropertyGrid позволяет отображать разнообразные структуры классов в удобном для редактирования виде и для этого достаточно связать с ним объект вашего класса. Однако, не все конструкции можно сразу отобразить без написания дополнительного кода. В этой статье я хочу рассказать о своем опыте использования PropertyGrid в контексте:
1. Отображение выпадающего списка записей, который собой представляет коллекцию объектов.

image

2. Отображение поля, связанного с коллекцией объектов, вызывающего редактор коллекции.

image

В качестве примера я взял задачу для реализации управления информацией о сотруднике, которую реализует класс Employee.Класс содержит два свойства. Первое — свойство типа JobTitle (должность сотрудника для выпадающего списка), второе – свойство типа JobTitleCollection (коллекция должностей сотрудников для редактора коллекции). Задачей является отображение объекта класса Employee в PropertyGrid так, как показано на двух рисунках в начале статьи, а именно:1. Отобразить поле, в котором пользователь сможет выбрать одну из должностей сотрудника с помощью выпадающего списка как в элементе ComboBox.2. Отобразить поле, которое позволит вызвать редактор связанной коллекции должностей, а после завершения изменения коллекции отобразить должности в PropertyGrid в виде списка.
Рассмотрим подробнее.

Класс сотрудника
  1. /// <summary>
  2. /// Информация о сотруднике
  3. /// </summary>
  4. [DisplayName("Сотрудник"), Description("Информация о сотруднике"), Category("Информация")]
  5. public class Employee
  6. {
  7.     private JobTitle jt = new JobTitle();
  8.     /// <summary>
  9.     /// Должность, свойство отображается как выпадающий список должностей с возможностью выбора одной записи
  10.     /// (объекта JobTitle)
  11.     /// </summary>
  12.     [DisplayName("Должность"), Description("Должность сотрудника"), Category("Выбор из списка")]
  13.     [TypeConverter(typeof(JobTitleTypeConverter))]
  14.     public JobTitle jobtitle
  15.     {
  16.         get { return jt; }
  17.         set { jt = value; }
  18.     }
  19.  
  20.     private JobTitleCollection jobTitles = new JobTitleCollection();
  21.     /// <summary>
  22.     /// Коллекция должностей, отображается как поле вызова редактора коллекции объектов
  23.     /// с возможностью развернуть свойство в виде списка всех возможных должностей в PropertyGrid
  24.     /// </summary>        
  25.     [Description("Коллекция должностей сотрудников"), Category("Коллекции"), DisplayName("Должности")]
  26.     [TypeConverter(typeof(JobTitleCollectionConverter))]
  27.     public JobTitleCollection JobTitles
  28.     {
  29.         get { return jobTitles; }
  30.         set { jobTitles = value; }
  31.     }
  32. }
Класс должности JobTitle
  1. /// <summary>
  2. /// Должность сотрудника
  3. /// </summary>
  4. [DisplayName("Должность"), Description("Должность сотрудника"), Category("Информация о сотруднике")]
  5. [TypeConverter(typeof(JobTitleConverter))]
  6. public class JobTitle : MyItem
  7. {
  8.     /// <summary>
  9.     /// Конструктор
  10.     /// </summary>
  11.     public JobTitle() { }
  12.  
  13.     /// <summary>
  14.     /// Конструктор
  15.     /// </summary>
  16.     /// <param name="ID">ID записи</param>
  17.     /// <param name="ItemName">Название</param>
  18.     public JobTitle(int ID, string ItemName)
  19.     {
  20.         this.ID = ID;
  21.         this.ItemName = ItemName;
  22.     }
  23. }
Решение задачи
Нужно реализовать класс для преобразования типа JobTitle в такой вид, чтобы объект PropertyGrid смог корректно отобразить выпадающий список должностей. Предполагается, что в момент создания объекта класса Employee будет добавлено несколько должностей в коллекцию для отображения в списке, после чего коллекция сможет отобразиться в выпадающем списке с дальнейшим редактированием через поле JobTitleCollection объекта класса Employee.Сначала нужно реализовать класс преобразования типа должности в строковое отображение и обратно — JobTitleTypeConverter. Далее, помимо этого класса будут реализованы дополнительные классы, необходимые для решения задачи.
Для вас доступен исходный код примера. Ссылка продублирована в конце статьи.
Сразу хочу отметить, что управление информацией объекта, отображаемой в PropertyGrid, осуществляется с помощью атрибутов. Это позволяет задать требуемый стиль отображения информации. Далее атрибуты будут активно использоваться в реализации решения.
  1. /// <summary>
  2. /// Класс преобразования типа JobTitle
  3. /// Выполняет преобразование к типу String и обратно в JobTitle
  4. /// Также, позволяет отображать в виде выпадающего списка коллекцию должностей
  5. /// </summary>
  6. public class JobTitleTypeConverter : ExpandableObjectConverter
  7. {
  8.     /// <summary>
  9.     /// Возвращает значение, показывающее, поддерживает ли объект стандартный набор значений, 
  10.     /// которые можно выбрать из списка.
  11.     /// Если не установить принудительно значение в true, есть вероятность, 
  12.     /// что обработчик не будет воспринимать ваш метод GetStandardValues и вы не увидите 
  13.     /// ваш список значений для выбора
  14.     /// </summary>
  15.     /// <param name="context"></param>
  16.     /// <returns></returns>
  17.     public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
  18.     {
  19.         return true;
  20.     }
  21.  
  22.     /// <summary>
  23.     /// Этот метод можно исключить, но в перспективе он позволит использовать данный класс конвертора 
  24.     /// для вызова из разных классов.
  25.     /// Для этого достаточно добавить в секцию case имя другого класса, который должен содержать свойство 
  26.     /// JobTitle с функцией связи с выпадающим списком
  27.     /// </summary>
  28.     /// <param name="context"></param>
  29.     /// <returns></returns>
  30.     private JobTitleCollection GetCollection(System.ComponentModel.ITypeDescriptorContext context)
  31.     {
  32.         JobTitleCollection collection = new JobTitleCollection();
  33.         switch (context.Instance.GetType().Name)
  34.         {
  35.             case "Employee":
  36.                 collection = ((Employee)context.Instance).JobTitles;
  37.                 break;
  38.             default:
  39.                 collection = ((Employee)context.Instance).JobTitles;
  40.                 break;
  41.         }
  42.         return collection;
  43.     }
  44.  
  45.     /// <summary>
  46.     /// Метод возвращает список значений из коллекции должностей для отображения в выпадающем 
  47.     /// списке значений PropertyGrid для должности сотрудника
  48.     /// </summary>
  49.     /// <param name="context"></param>
  50.     /// <returns></returns>
  51.     public override TypeConverter.StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
  52.     {
  53.         return new StandardValuesCollection(GetCollection(context));
  54.     }
  55.  
  56.     /// <summary>
  57.     /// Метод проверяет, можно ли преобразовывать полученное значение свойства от пользователя 
  58.     /// в нужный нам тип
  59.     /// </summary>
  60.     /// <param name="context"></param>
  61.     /// <param name="destinationType"></param>
  62.     /// <returns></returns>
  63.     public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
  64.     {
  65.         if (destinationType.Equals(typeof(string)))
  66.         {
  67.             return true;
  68.         }
  69.         else
  70.         {
  71.             return false;
  72.         }
  73.     }
  74.  
  75.     /// <summary>
  76.     /// Преобразование типа JobTitle в строку для отображения значения в поле PropertyGrid
  77.     /// </summary>
  78.     /// <param name="context"></param>
  79.     /// <param name="culture"></param>
  80.     /// <param name="value"></param>
  81.     /// <param name="destinationType"></param>
  82.     /// <returns></returns>
  83.     public override object ConvertTo(ITypeDescriptorContext context,
  84.         System.Globalization.CultureInfo culture, object value, Type destinationType)
  85.     {
  86.         if (destinationType == typeof(string) && value is JobTitle)
  87.         {
  88.             JobTitle item = (JobTitle)value;
  89.             return item.FullName;
  90.         }
  91.         return base.ConvertTo(context, culture, value, destinationType);
  92.     }
  93.  
  94.     /// <summary>
  95.     /// Проверка на возможность обратного преобразования строкового представления в тип JobTitle
  96.     /// </summary>
  97.     /// <param name="context"></param>
  98.     /// <param name="sourceType"></param>
  99.     /// <returns></returns>
  100.     public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
  101.     {
  102.         if (sourceType.Equals(typeof(string)))
  103.         {
  104.             return true;
  105.         }
  106.         else
  107.         {
  108.             return false;
  109.         }
  110.     }
  111.  
  112.     /// <summary>
  113.     /// Преобразование строкового представления должности в тип JobTitle.
  114.     /// Недостаток данного преобразования - правильность работы приведения типа зависит от формата 
  115.     /// строкового представления типа JobTitle, а именно, от дублирующихся значений.
  116.     /// </summary>
  117.     /// <param name="context"></param>
  118.     /// <param name="culture"></param>
  119.     /// <param name="value"></param>
  120.     /// <returns></returns>
  121.     public override object ConvertFrom(ITypeDescriptorContext context,
  122.         System.Globalization.CultureInfo culture, object value)
  123.     {
  124.         if (value.GetType() == typeof(string))
  125.         {
  126.             JobTitle itemSelected = GetCollection(context).Count.Equals( 0) ?
  127.                 new JobTitle() : GetCollection(context)[ 0];
  128.  
  129.             foreach (JobTitle Item in GetCollection(context))
  130.             {
  131.                 string sCraftName = Item.FullName;
  132.                 if (sCraftName.Equals((string)value))
  133.                 {
  134.                     itemSelected = Item;
  135.                 }
  136.             }
  137.             return itemSelected;
  138.         }
  139.         else
  140.             return base.ConvertFrom(context, culture, value);
  141.     }
  142.  
  143.     /// <summary>
  144.     /// Возвращает значение, показывающее, является ли исчерпывающим списком коллекция стандартных значений, 
  145.     /// возвращаемая методом.
  146.     /// 
  147.     /// false - данные можно вводить вручную
  148.     /// true - только выбор из списка
  149.     /// </summary>
  150.     /// <param name="context"></param>
  151.     /// <returns></returns>
  152.     public override bool GetStandardValuesExclusive(ITypeDescriptorContext context)
  153.     {
  154.         return true;
  155.     }
  156.  
  157.     /// <summary>
  158.     /// Конструктор метода
  159.     /// </summary>
  160.     public JobTitleTypeConverter() { }
  161. }
  162.  
Класс коллекции должностей
Для реализации класса коллекции должностей нужно его унаследовать от класса CollectionBase и реализовать интерфейс ICustomTypeDescriptor. Это позволит использовать коллекцию для корректного отображения в PropertyGrid. Для этого используется реализация класса описателя свойств коллекции JobTitleCollectionPropertyDescriptor.
  1. /// <summary>
  2. /// Класс коллекции должностей
  3. /// Содержит методы, предоставляющие информацию для описания содержимого свойств коллекции 
  4. /// и отображения в PropertyGrid
  5. /// </summary>
  6. [DisplayName("Перечень должностей"), Description("Перечень должностей сотрудников"), Category("Информация о сотруднике")]
  7. public class JobTitleCollection : CollectionBase, ICustomTypeDescriptor
  8. {
  9.     #region Методы коллекции
  10.     public void Add(JobTitle Item)
  11.     {
  12.         this.List.Add(Item);
  13.     }
  14.  
  15.     public void Remove(JobTitle Item)
  16.     {
  17.         this.List.Remove(Item);
  18.     }
  19.  
  20.     public JobTitle this[int Index]
  21.     {
  22.         get { return (JobTitle)this.List[Index]; }
  23.     }
  24.     #endregion
  25.  
  26.     #region Реализация интерфейса ICustomTypeDescriptor
  27.     public String GetClassName()
  28.     {
  29.         return TypeDescriptor.GetClassName(this, true);
  30.     }
  31.  
  32.     public AttributeCollection GetAttributes()
  33.     {
  34.         return TypeDescriptor.GetAttributes(this, true);
  35.     }
  36.  
  37.     public String GetComponentName()
  38.     {
  39.         return TypeDescriptor.GetComponentName(this, true);
  40.     }
  41.  
  42.     public TypeConverter GetConverter()
  43.     {
  44.         return TypeDescriptor.GetConverter(this, true);
  45.     }
  46.  
  47.     public EventDescriptor GetDefaultEvent()
  48.     {
  49.         return TypeDescriptor.GetDefaultEvent(this, true);
  50.     }
  51.  
  52.     public PropertyDescriptor GetDefaultProperty()
  53.     {
  54.         return TypeDescriptor.GetDefaultProperty(this, true);
  55.     }
  56.  
  57.     public object GetEditor(Type editorBaseType)
  58.     {
  59.         return TypeDescriptor.GetEditor(this, editorBaseType, true);
  60.     }
  61.  
  62.     public EventDescriptorCollection GetEvents(Attribute[] attributes)
  63.     {
  64.         return TypeDescriptor.GetEvents(this, attributes, true);
  65.     }
  66.  
  67.     public EventDescriptorCollection GetEvents()
  68.     {
  69.         return TypeDescriptor.GetEvents(this, true);
  70.     }
  71.  
  72.     public object GetPropertyOwner(PropertyDescriptor pd)
  73.     {
  74.         return this;
  75.     }
  76.  
  77.     public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
  78.     {
  79.         return GetProperties();
  80.     }
  81.  
  82.     public PropertyDescriptorCollection GetProperties()
  83.     {
  84.         PropertyDescriptorCollection pds = new PropertyDescriptorCollection(null);
  85.  
  86.         for (int i =  0; i < this.List.Count; i++)
  87.         {
  88.             JobTitleCollectionPropertyDescriptor pd = new JobTitleCollectionPropertyDescriptor(this, i);
  89.             pds.Add(pd);
  90.         }
  91.         return pds;
  92.     }
  93.     #endregion
  94. }
Дополнительный класс описания свойств коллекции объектов JobTitle
  1. /// <summary>
  2. /// Класс описания свойств коллекции объектов JobTitle
  3. /// </summary>
  4. public class JobTitleCollectionPropertyDescriptor : PropertyDescriptor
  5. {
  6.     private JobTitleCollection collection = null;
  7.     private int index = -1;
  8.  
  9.     public JobTitleCollectionPropertyDescriptor(JobTitleCollection coll, int idx) :
  10.         base("#" + idx.ToString(), null)
  11.     {
  12.         this.collection = coll;
  13.         this.index = idx;
  14.     }
  15.  
  16.     public override AttributeCollection Attributes
  17.     {
  18.         get
  19.         {
  20.             return new AttributeCollection(null);
  21.         }
  22.     }
  23.  
  24.     public override bool CanResetValue(object component)
  25.     {
  26.         return true;
  27.     }
  28.  
  29.     public override Type ComponentType
  30.     {
  31.         get
  32.         {
  33.             return this.collection.GetType();
  34.         }
  35.     }
  36.  
  37.     public override string DisplayName
  38.     {
  39.         get
  40.         {
  41.             JobTitle Item = this.collection[index];
  42.             return Item.FullName;
  43.         }
  44.     }
  45.  
  46.     public override string Description
  47.     {
  48.         get
  49.         {
  50.             JobTitle Item = this.collection[index];
  51.             StringBuilder sb = new StringBuilder();
  52.             sb.Append(Item.ItemName);
  53.             sb.Append(", ");
  54.             sb.Append(index + 1);
  55.  
  56.             return sb.ToString();
  57.         }
  58.     }
  59.  
  60.     public override object GetValue(object component)
  61.     {
  62.         return this.collection[index];
  63.     }
  64.  
  65.     public override bool IsReadOnly
  66.     {
  67.         get { return false; }
  68.     }
  69.  
  70.     public override string Name
  71.     {
  72.         get { return "#" + index.ToString(); }
  73.     }
  74.  
  75.     public override Type PropertyType
  76.     {
  77.         get { return this.collection[index].GetType(); }
  78.     }
  79.  
  80.     public override void ResetValue(object component)
  81.     {
  82.     }
  83.  
  84.     public override bool ShouldSerializeValue(object component)
  85.     {
  86.         return true;
  87.     }
  88.  
  89.     public override void SetValue(object component, object value)
  90.     {
  91.  
  92.     }
  93. }
Дополнительный класс конвертора коллекции должностей
  1. /// <summary>
  2. /// Класс преобразователя стандартного отображения коллекции в PropertyGrid
  3. /// Заменяет обозначение "(Collection)" на "(Должности...)"
  4. /// </summary>
  5. public class JobTitleCollectionConverter : ExpandableObjectConverter
  6. {
  7.     public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture,
  8.         object value, Type destType)
  9.     {
  10.         if (destType == typeof(string) && value is JobTitleCollection)
  11.         {
  12.             return "(Должности...)";
  13.         }
  14.         return base.ConvertTo(context, culture, value, destType);
  15.     }
  16. }
Дополнительный класс конвертора типа должности
  1. /// <summary>
  2. /// Класс выполняющий преобразование типа JobTitle для корректного отображения в редакторе коллекций
  3. /// Если его не реализовать, то в окне редактора коллекции JobTitle будут отображаться внутреннее имя
  4. /// класса JobTitle
  5. /// </summary>
  6. public class JobTitleConverter : ExpandableObjectConverter
  7. {
  8.     public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture,
  9.         object value, Type destType)
  10.     {
  11.         if (destType == typeof(string) && value is JobTitle)
  12.         {
  13.             JobTitle jobtitle = (JobTitle)value;
  14.             return jobtitle.FullName;
  15.         }
  16.         return base.ConvertTo(context, culture, value, destType);
  17.     }
  18. }
Класс предок классов сотрудника и должности
  1. /// <summary>
  2. /// Класс-предок сущностей
  3. /// </summary>
  4. public class MyItem
  5. {
  6.     private int iID = -1;
  7.     /// <summary>
  8.     /// ID записи
  9.     /// </summary>
  10.     [Description("Номер элемента"), Category("Коллекции"), DisplayName("Номер")]
  11.     public int ID
  12.     {
  13.         get { return iID; }
  14.         set { iID = value; }
  15.     }
  16.  
  17.     private string sItemName = "";
  18.     /// <summary>
  19.     /// Название
  20.     /// </summary>
  21.     [Description("Название элемента"), Category("Коллекции"), DisplayName("Название")]
  22.     public string ItemName
  23.     {
  24.         get { return sItemName; }
  25.         set { sItemName = value; }
  26.     }
  27.  
  28.     /// <summary>
  29.     /// Полное наименование объекта
  30.     /// </summary>
  31.     [Browsable(false)]
  32.     public string FullName
  33.     {
  34.         get
  35.         {
  36.             return String.Format("{0} №{1}", ItemName, ID.ToString());
  37.         }
  38.     }
  39. }
Вы можете загрузить исходный код примера.
Статья создана по мотивам PropertyGrid FAQ и имеет своей целью его дополнение.
Tags:
Hubs:
Total votes 8: ↑6 and ↓2+4
Comments7

Articles