вторник, 23 марта 2010 г.

Создание собственных секций (ConfigurationSection) в web.config/app.config

Конфигурационные файлы приложений (как веб, так и системных) предназначены для хранения настроек, причём любых. Но почему-то большинство программистов либо игнорирует эти возможности, либо пользуются только возможностью хранения строк соединения с базами данных (connection string) и пар «ключ-значение». Конфигурационный файл позволяет хранить данные любой вложенности и сложности, вопрос только в проектировании их структуры, а фактически – написание классов, которые будут сериализироваться.
Структура конфигурационных файлов условно делится на два раздела:
  • Метаописание конфигурационного файла – раздел configSections .
  • Непосредственно хранимые настройки.

Секция configSections – самая главная, так как она содержит в себе описание всех остальных секций, фактически это метаинформация о файле конфигурации. Именно этот раздел первым читает менеджер конфигураций, который считывает все описания секций и предназначенные для них структуры данных.
Если условно, то файл конфига – это база данных, а структуры данных – это объекты, поля которых содержат значения из базы данных. Роль менеджера конфигураций – своевременно заносить данные из БД в объект и обратно. С точки зрения программиста мы должны сделать две вещи:
  • Описать структуру данных (класс и атрибуты привязки полей класса к параметрам xml файла);
  • В конфигурационном файле сопоставить какое-либо название секции с нашей структурой.

Есть ещё и третий шаг, но он не обязательный – создать секцию и наполнить её данными. Необязательный этот шаг потому, что при описании структуры данных мы можем указать значения по умолчанию, что нам дасть возможность всегда иметь все настройки в структуре данных, независимо от того, есть ли они в конфиге.
К преимуществам конфигурационных файлов следует отнести то, что большую чатсь работы мы занимаемся декларационным программированием – фактически мы просто проектируем структуру, указываем, какие поля класса хранить в файле, совершенно не задумываясь о механизме сохранения и чтения, шифрования и т.п. Наличие в системе иерархии файлов конфигурации от самого низкого уровня машины(Machine.config) до конфигурационного файла приложения, и даже директории в веб-приложении, позволяет сделать систему наследования свойств и централизованного управления их значениями.
Лучше всего всё сказанное будет описано на примере простой секции, которая будет хранить некие данные, а также содержать в себе подсекции.
Начать лучше всего с создания главного класса секции, который должен быть унаследован от System.Configuration.ConfigurationSection (не забываем добавить в References System.Configuration.dll).

using System.Configuration;
<…>
///
       /// Настройки SORM
       ///
       public class SORMSettings: ConfigurationSection
       {
             ///
             /// Маска поиска
             ///
             [ConfigurationProperty("SearchMask ", DefaultValue = "*")]
             public String SearchMask
             {
                    get { return (String)this["SearchMask"]; }
                    set { this["SearchMask"] = value; }
             }

             ///
             /// Стандартный адаптер для работы
             ///
             [ConfigurationProperty("defaultAdapter", IsRequired = false)]
             public String DefaultAdapterName
             {
                    get { return (String)this["defaultAdapter"]; }
                    set { this["defaultAdapter"] = value; }
             }

              ///
              /// Список адаптеров
              ///
             [ConfigurationProperty("adapters")]
             public AdaptersCollection Adapters
             {
                    get { return (AdaptersCollection) this["adapters"]; }
             }
       }


У нас есть собственный класс SORMSettings и три его свойства. Унаследовав класс от ConfigurationSection мы дали понять, что будем читать данные из конфигурационного файла, а атрибутами пометили свойства, которые и будут читаться.
Атрибут ConfigurationProperty указывает менеджеру конфигурации, что в данное свойства класса необходимо «синхронизировать» с конкретным атрибутом из файла конфигурации. У ConfigurationProperty имеется одно обязательное поле – name, которое указывает название атрибута секции из файла конфигурации в котором будет храниться наше значение.

public class SORMSettings: ConfigurationSection
{
       [ConfigurationProperty("SearchMask")]
       public String SearchMask
       {
             get { return (String)this["SearchMask"]; }
             set { this["SearchMask"] = value; }
       }
}

<configuration>
       <…>
       <SORM
             SearchMask ="123_*.dat">
       <…>   
configuration>


Помимо свойства name у ConfigurationProperty есть парочка других полезных необязательных свойств:
  • DefaultValue – значение по умолчанию, если значение атрибута в файле конфигурации не определено.
  • IsRequired – указываем, что значение должно быть задано в файле конфигурации обязательно.
  • IsKey – позволяет отметить поле как ключевое, в случае, когда мы храним набор однотипных элементов.

Для хранения сложных вложенных структур данных в секции нужно создать класс-наследник уже от ConfigurationElement. В нашем примере это класс AdaptersCollection унаследованный от ConfigurationElementCollection (ConfigurationElementCollection – это наследник ConfigurationElement).

///
       /// Коллекция адаптеров
       ///
       public class AdaptersCollection: ConfigurationElementCollection
       {             
             ///
             /// Создание нового элемента коллекции нужно типа
             ///
             ///
             protected override ConfigurationElement CreateNewElement()
             {
                    return new AdapterElement();
             }

             ///
             /// Получение ключа элемента для его идентификации
             ///
             ///

             ///
             protected override object GetElementKey(ConfigurationElement element)
             {
                    return ((AdapterElement) element).Name;
             }

             ///
             /// Получение элемента по его индексу
             ///
             ///

             ///
             public AdapterElement this[Int32 index]
             {
                    get { return (AdapterElement) BaseGet(index); }
                    set
                    {
                           if (BaseGet(index)!= null)
                                  BaseRemoveAt(index);
                           BaseAdd(index,value);
                    }
             }

             ///
             /// Получение элемента по его имени
             ///
             ///

             ///
             public new AdapterElement this[String name]
             {
                    get { return (AdapterElement) BaseGet(name); }
             }
       }

Класс AdaptersCollection – описывает список адаптеров которые мы будет хранить в настройках. Минимально, что необходимо сделать унаследовавшись от ConfigurationElementCollection это переопределить метод CreateNewElement() которая вызывается при наполнении  коллекции элементами. Мы должны внутри этой функции инициализировать новый элемент коллекции и вернуть его. Помимо этого мы можем переопределить множество функций отвечающих за управление коллекцией (добавление, удаление, очистка, поиск). В данном случае ограничимся введением  двух индексаторов для доступа к элементам коллекции по имени и по индексу. Наша коллекция работает с элементами AdapterElement, вот код класса:

/// <summary>
       /// Элемент адаптера
       ///
       public class AdapterElement: ConfigurationElement
       {
             ///
             /// Название адаптера
             ///
             [ConfigurationProperty("name")]
             public String Name
             {
                    get { return (String) this["name"]; }
                    set { this["name"] = value;}
             }

             ///
             /// Строка с описанием типа адаптера
             ///
             [ConfigurationProperty("type")]
             public String Type
             {
                    get { return (String)this["type"]; }
                    set { this["type"] = value; }
             }

             ///
             /// Параметры для адаптера
             ///
             [ConfigurationProperty("parameters", IsDefaultCollection = true)]
             public NameValueConfigurationCollection Parameters
             {
                    get { return (NameValueConfigurationCollection) this["parameters"]; }
             }
       }

Элемент содержит два свойства – Name и Type, а также ещё одну вложенную коллекцию типа NameValueConfigurationCollection. NameValueConfigurationCollection – это коллекция элементов NameValueConfigurationElement, которые описываются двумя свойствами – Name (ключевое поле) и Value (значение). Есть аналогичная коллекция KeyValueConfigurationCollection – там поля Key и Value, видно из названия.
Таким образом, мы имеем адаптер, который хранит в себе набор (словарь) каких-то параметров (Parameters), имя (Name) и тип (Type). Адаптеры хранятся в коллекции (Adapters) нашей секции, где также указывается имя стандартного адаптера (DefaultAdapterName) и маска поиска (SearchMask). Вот так выглядит код секции:

<SORM defaultAdapter=" My Simple XML Adapter">
             <adapters>
                    <add name="My Simple XML Adapter" type="SimpleXMLAdapter">
                           <parameters>
                                  <add name="DataBasePath" value="DataBase" />
                                  <add name="LogLevel" value="3" />
                           parameters>
                    add>
             adapters>
       SORM>

Как видите, структура хранения данных идентична структуре классов, что позволяет довольно быстро делать удобные конфигурационные классы секций.
Осталось лишь объяснить менеджеру конфигураций, что раздел SORM нужно сопоставить с нашим классом SORMSettings. Для этого в разделе configSections конфигурационного файла опишем нашу секцию:

<section
             name="SORM"
             type="Polyakov.SORM.Settings.SORMSettings, Polyakov.SORM, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
             />

Параметры section:
  • Name – это то, как будет называться наше секция.
  • Type – тип объекта, который будет предоставлять доступ к данным конфигурации – это наш класс секции.

Это всё! Для красоты и удобства мы можем группировать секции, для этого необходимо в configSections создать элемент sectionGroup и уже в него помещать секции.
Обратиться к данным конфигурации можно из кода так:

var cfg = (Settings.SORMSettings)ConfigurationManager.GetSection("SORMSettings/SORM");
       if (cfg == null)
             return;
       Console.WriteLine(cfg.DefaultAdapterName);

А файл конфигурации будет выглядеть так:

<configuration>
       <configSections>
             <sectionGroup name="SORMSettings">
                    <section
                           name="SORM"
                           type="Polyakov.SORM.Settings.SORMSettings, Polyakov.SORM, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
                           allowLocation="true"
                           allowDefinition="Everywhere"
                           />
             sectionGroup>
       configSections>

       <SORMSettings>
             <SORM defaultAdapter="SimpleXMLAdapter">
                    <adapters>
                           <add name="SimpleXMLAdapter" type="Polyakov.SORM.Adapters.XML.SimpleXMLAdapter, Polyakov.SORM, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
                                  <parameters>
                                        <add name="DataBasePath" value="DataBase" />
                                  parameters>
                           add>
                    adapters>
             SORM>
       SORMSettings>
configuration>










Отправить комментарий