Сообщений 1    Оценка 30 [+0/-1]         Оценить  
Система Orphus

Редактирование объекта с псевдо свойствами в PropertyGrid

Автор: Андрей Никулин
Net2S

Источник: RSDN Magazine #1-2008
Опубликовано: 17.07.2008
Исправлено: 10.12.2016
Версия текста: 1.0
Предисловие
Реализация
Использование
Дополнение
Заключение

Тестовый проект

Предисловие

В одном из проектов мне необходимо было реализовать редактор свойств объекта. Загвоздка заключалась в том что в проекте не используются средства ORM, и ко мне данные, которые надо было отредактировать и отослать обратно, попадали просто как двумерный массив object[,]. Ужас! То есть бизнес-классы отсутствовали вообще, и данные просто загружались из базы данных, изменялись и снова сохранялись в БД.

Известным редактором свойств объекта является PropertyGrid. Данный элемент управления используется практически повсеместно. Он просто очень удобен и предоставляет интуитивно понятный интерфейс для редактирования различных типов данных (int, string, double, массивов и коллекций). Он, конечно, предоставляет огромные возможности для расширения функциональности и редактирования сложных типов данных.

Но проблема была в том что PropertyGrid может редактировать объекты с уже существующими свойствами. В моем же случае все данные хранятся в БД, и не существует соответствующих бизнес-классов, куда отображаются данные из БД. Следовательно, на первый взгляд, использовать PropertyGrid (со всеми его встроенными редакторами простых типов, массивов, коллекций и автоматической проверкой корректности введенного значения) не получится. Но это только на первый взгляд...

Реализация

После некоторых изысканий элегантное решение было найдено. Использовать PropertyGrid все-таки можно. Просто необходимо создать некий класс DynamicObject, реализующий интерфейс ICustomTypeDescriptor:

      public
      interface ICustomTypeDescriptor
{
  AttributeCollection          GetAttributes();
  string                       GetClassName();
  string                       GetComponentName();
  TypeConverter                GetConverter();
  EventDescriptor              GetDefaultEvent();
  PropertyDescriptor           GetDefaultProperty();
  object                       GetEditor(Type editorBaseType);
  EventDescriptorCollection    GetEvents();
  EventDescriptorCollection    GetEvents(Attribute[] attributes);
  PropertyDescriptorCollection GetProperties();
  PropertyDescriptorCollection GetProperties(Attribute[] attributes);
  object                       GetPropertyOwner(PropertyDescriptor pd);
}

В этом классе нужно реализовать методы, которые предоставляют динамическую информацию об объекте и, в частности, о его свойствах. Вся работа будет сконцентрирована в основном вокруг реализации одного перегруженного метода – GetProperties. Данный метод будет вызван элементом управления PropertyGrid, когда вы присвоите экземпляр класса свойству SelectedObject:

      var dynamicObject = new DynamicObject();
propertyGrid1.SelectedObject = dynamicObject;

PropertyGrid вызовет метод GetProperties и передаст туда массив атрибутов Attribute[] с одним элементом BrowsableAttribute. То есть PropertyGrid запрашивает у объекта список свойств объекта, которые можно отобразить.

Метод GetProperties возвращает PropertyDescriptorCollection – коллекцию объектов PropertyDescriptor. PropertyDescriptor – это абстрактный класс, описывающий свойство объекта. В данном случае необходимо реализовать собственный класс-наследник PropertyDescriptor, который будет использоваться элементом управления PropertyGrid для получения информации о конкретном свойстве объекта. Так как тип свойства может быть любым типом из .Net Framework, то я решил реализовать generic-класс и назвал его просто – GenericPropertyDescriptor<T>, где T – тип свойства.

      public
      class GenericPropertyDescriptor<T> : PropertyDescriptor
{
  private T _value;

  public GenericPropertyDescriptor(string name, Attribute[] attrs)
    : base(name, attrs)
  {
  }

  public GenericPropertyDescriptor(string name, T value, Attribute[] attrs)
    : base(name, attrs)
  {
    _value = value;
  }

  publicoverridebool CanResetValue(object component)
  {
    returnfalse;
  }

  publicoverride System.Type ComponentType
  {
    get
    {
      returntypeof(GenericPropertyDescriptor<T>);
    }
  }

  publicoverrideobject GetValue(object component)
  {
    return _value;
  }

  publicoverridebool IsReadOnly
  {
    get
    {
      return Array.Exists(this.AttributeArray, 
         attr => attr is ReadOnlyAttribute);
    }
  }

  publicoverride System.Type PropertyType
  {
    get
    {
      returntypeof(T);
    }
  }

  publicoverridevoid ResetValue(object component)
  {
  }

  publicoverridevoid SetValue(object component, object value)
  {
    _value = (T)value;
  }

  publicoverridebool ShouldSerializeValue(object component)
  {
    returnfalse;
  }
}

Вернемся к реализации класса-обертки DynamicObject. Как я уже говорил, метод GetProperties возвращает PropertyDescriptorCollection – коллекцию объектов PropertyDescriptor. Соответственно, в классе DynamicObject нужно где-то хранить описатели свойств. Для этого создадим private-поле типа PropertyDescriptorCollection.

      private PropertyDescriptorCollection propertyDescriptors = 
  new PropertyDescriptorCollection(null);

По умолчанию коллекция объектов PropertyDescriptor пуста, и необходимо реализовать методы для изменения её содержимого. Для корректного отображения свойства в PropertyGrid и для инициализации класса GenericPropertyDescriptor<T> реализуем метод (назовем его AddProperty<T>), добавляющий новое свойство. Метод должен принимать имя свойства, его текущее значение, описание свойства для отображения в PropertyGrid, имя категории (если мы хотим, чтобы свойства в PropertyGrid были разбиты по категориям), флаг readOnly для указания, должно ли свойство быть доступным только для чтения, и массив атрибутов, на случай, если нужно пометить данное свойство дополнительными атрибутами.

ПРИМЕЧАНИЕ

Дополнительные атрибуты могут понадобиться, например, для указания PropertyGrid специфического редактора или специального конвертора типа данных.

Реализация перегруженного метода AddProperty<T> представлена ниже:

      public
      void AddProperty<T>(
  string name,
  T value,
  string displayName,
  string description,
  string category,
  bool readOnly,
  IEnumerable<Attribute> attributes)
{
  var attrs = attributes == null ? new List<Attribute>()
                                 : new List<Attribute>(attributes);

  if (!String.IsNullOrEmpty(displayName))
    attrs.Add(new DisplayNameAttribute(displayName));

  if (!String.IsNullOrEmpty(description))
    attrs.Add(new DescriptionAttribute(description));

  if (!String.IsNullOrEmpty(category))
    attrs.Add(new CategoryAttribute(category));

  if (readOnly)
    attrs.Add(new ReadOnlyAttribute(true));

  propertyDescriptors.Add(new GenericPropertyDescriptor<T>(
    name, value, attrs.ToArray()));
}

publicvoid AddProperty<T>(
  string name,
  T value,
  string description,
  string category,
  bool readOnly)
{
  AddProperty<T>(name, value, name, description, category, readOnly, null);
}

Также можно предусмотреть метод удаления свойств из коллекции свойств объекта. Для этого служит метод RemoveProperty:

      public
      void RemoveProperty(string propertyName)
{
  var descriptor = propertyDescriptors.Find(propertyName, true);
  if (descriptor != null)
    propertyDescriptors.Remove(descriptor);
  elsethrownew Exception("Property is not found");
}

Кроме того, необходима возможность прочитать/изменить значение того или иного свойства извне элемента управления PropertyGrid. Для этого у класса DynamicObject нужно реализовать методы GetPropertyValue и SetPropertyValue:

      private
      object GetPropertyValue(string propertyName)
{
  var descriptor = propertyDescriptors.Find(propertyName, true);
  if (descriptor != null)
    return descriptor.GetValue(null);
  elsethrownew Exception("Property is not found");
}

privatevoid SetPropertyValue(string propertyName, object value)
{
  var descriptor = propertyDescriptors.Find(propertyName, true);
  if (descriptor != null)
    descriptor.SetValue(null, value);
  elsethrownew Exception("Property is not found");
}

Ну и для удобства работы с классом DynamicObject добавим indexer, чтобы можно было обращаться к свойствам как к элементам коллекции:

      public
      object
      this[string propertyName]
{
  get { return GetPropertyValue(propertyName); }
  set { SetPropertyValue(propertyName, value); }
}

Использование

А теперь – пример использования, конечно. Для начала – инициализация объекта и добавление свойств. Предположим у нас есть простейшая форма и элемент управления PropertyGrid на форме. Здесь представлен код события Form1_Load:

      private
      void Form1_Load(object sender, EventArgs e)
{
  var dynamicObject = new DynamicObject();

  dynamicObject.AddProperty<Int32>("Int32 Param", 0, 
    "Int32 Param Description", "Simple types", false);
  dynamicObject.AddProperty<String>("String Param", "", 
    "String Param Description", "Simple types", false);
  dynamicObject.AddProperty<Double>("Double Param", 0, 
    "Double Param Description", "Simple types", false);

  dynamicObject.AddProperty<Int32[]>("Int32[] Param", new Int32[] { },
    "Int32[] Param Description", "Array types", false);
  dynamicObject.AddProperty<String[]>("String[] Param", new String[] { },
    "String[] Param Description", "Array types", false);
  dynamicObject.AddProperty<Double[]>("Double[] Param", new Double[] { },
    "Double[] Param Description", "Array types", false);

  dynamicObject.AddProperty<List<Int32>>("List<Int32> Param",
    new List<Int32>(), "List<Int32> Param Description", 
    "Collection types", false);
  dynamicObject.AddProperty<List<Double>>("List<Double> Param", 
    new List<Double>(), "List<Double> Param Description", 
    "Collection types", false);

  propertyGrid1.SelectedObject = dynamicObject;
}

А прочитать значения свойств объекта можно так:

      private
      void btnRead_Click(object sender, EventArgs e)
{
  var dynamicObject = propertyGrid1.SelectedObject as DynamicObject;
  if (dynamicObject != null)
  {
    var intValue = (Int32)dynamicObject["Int32 Param"];
    var strValue = (String)dynamicObject["String Param"];
    var dblValue = (Double)dynamicObject["Double Param"];

    var intArray = (Int32[])dynamicObject["Int32[] Param"];
    var strArray = (String[])dynamicObject["String[] Param"];
    var dblArray = (Double[])dynamicObject["Double[] Param"];

    var intList = (List<Int32>)dynamicObject["List<Int32> Param"];
    var dblList = (List<Double>)dynamicObject["List<Double> Param"];
  }
}

Дополнение

В тестовом проекте вы также найдете пример реализации фильтра свойств oбъекта DynamicObject, отображаемых в элементе управления PropertyGrid. Идея была мной позаимствована из новой WPF-версии элемента управления PropertyGrid, использованного в Visual Studio 2008. Подробно останавливаться на этом я не буду. Скажу просто, что если объект DynamicObject имеет более 20-30 свойств, очень удобно иметь возможность отфильтровать свойства по имени.


Заключение

Данная статья описывает еще одну возможность (но далеко не последнюю в списке) из огромнейшего списка возможностей замечательного элемента управления PropertyGrid. Если вас заинтересовала данная статья, очень советую обратить внимание на статьюАлексея Кирюшкина, опубликованную в одном из номеров журнала RSDN Magazine, и на очень полезный сайт, специально посвященный PropertyGrid - Microsoft PropertyGrid Resource List.


Эта статья опубликована в журнале RSDN Magazine #1-2008. Информацию о журнале можно найти здесь
    Сообщений 1    Оценка 30 [+0/-1]         Оценить