четверг, 7 июля 2011 г.

Создание пользовательских разделов конфигурации (ConfigurationSection). Часть 2: Унификация и управление конфигурацией

В прошлой части я показал, как можно создавать обработчики собственных секций файла конфигурации (app.config или web.config) и вызывать их из кода. Во второй части осталось рассмотреть еще несколько интересных моментов, которые могут пригодиться при работе с конфигурацией.


Унификация настроек

Вспомним, какой вид имеет наша секция настроек в файле конфигурации:

<table fileName="files\import.txt" codePage="utf8">
    <fields>
        <add name="Id" elementType="int" length="10" />
        <add name="Name" elementType="string" length="100" />
        <add name="UserName" elementType="string" length="50" />
        <add name="ObjectId" elementType="int" length="10" />
        <add name="ParentObjectId" elementType="int" length="10" />
        <add name="SerialNumber" elementType="string" length="20" />
    </fields>
</table>

В принципе, все хорошо и понятно. Но все-таки посмотрим, какой вид имеют стандартные секции connectionStrings и appSettings:

<connectionStrings>
    <add name="MyConnectionString1" connectionString="Data Source=sqlexpress;Initial Catalog=Base1;User ID=sa;Password=123" />
    <add name="MyConnectionString2" connectionString="Data Source=sqlexpress;Initial Catalog=Base2;User ID=sa;Password=123" />
</connectionStrings>
 
<appSettings>
    <add key="key1" value="value1"/>
    <add key="key2" value="value2"/>
    <add key="key3" value="value3"/>
</appSettings>

Видно, что у стандартных секций отстутствует дополнительный элемент: элементы коллекции добавляются напрамую через <add />. Для того чтобы избавиться от лишних элементов <fields />, необходимо перебить некоторые базовые механизмы. Конечно, странно, что такой механизм не предоставляется по умолчанию.

Новые классы TableSection и FieldsCollection будут иметь следующий вид:

/// 
/// Обработчик секции, которая хранит описание файла с данными
/// 
public class TableSection : ConfigurationSection
{
    private static ConfigurationPropertyCollection _properties;
    private static readonly ConfigurationProperty FileNameProperty;
    private static readonly ConfigurationProperty CodePageProperty;
    private static readonly ConfigurationProperty FieldsProperty;

    protected override ConfigurationPropertyCollection Properties
    {
        get { return _properties; }
    }

    static TableSection()
    {
        FileNameProperty = new ConfigurationProperty("fileName", typeof(string), "import.txt");
        CodePageProperty = new ConfigurationProperty("codePage", typeof(string), "utf8");
        FieldsProperty = new ConfigurationProperty("", typeof(FieldsCollection),
            null, ConfigurationPropertyOptions.IsDefaultCollection);

        _properties = new ConfigurationPropertyCollection();
        _properties.Add(FileNameProperty);
        _properties.Add(CodePageProperty);
        _properties.Add(FieldsProperty);
    }

    /// 
    /// Путь к файлу с данными
    /// 
    public string FileName
    {
        get { return (string)this["fileName"]; }
        set { this["fileName"] = value; }
    }

    /// 
    /// Кодировка файла
    /// 
    public string CodePage
    {
        get { return (string)this["codePage"]; }
        set { this["codePage"] = value; }
    }

    /// 
    /// Коллекция столбцов таблицы
    /// 
    public FieldsCollection Fields
    {
        get { return (FieldsCollection)base[FieldsProperty]; }
    }
}

и

/// 
/// Обработчик коллекции данных (в нашем случае, коллекции полей)
/// 
public class FieldsCollection : ConfigurationElementCollection
{
    protected override ConfigurationElement CreateNewElement()
    {
        return new FieldElement();
    }

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

    #region Методы-обертки для привычной работы с коллекцией

    /// 
    /// Получение элемента по его индексу
    /// 
    /// /// 
    public FieldElement this[int index]
    {
        get { return (FieldElement)BaseGet(index); }
        set
        {
            if (BaseGet(index) != null)
                BaseRemoveAt(index);
            BaseAdd(index, value);
        }
    }

    /// 
    /// Получение элемента по его имени
    /// 
    /// /// 
    public new FieldElement this[string name]
    {
        get { return (FieldElement)BaseGet(name); }
    }

    /// 
    /// Добавление элемента в коллекцию
    /// 
    /// public void Add(FieldElement element)
    {
        BaseAdd(element);
    }

    /// 
    /// Очистка коллекции от элементов
    /// 
    public void Clear()
    {
        BaseClear();
    }

    public int IndexOf(FieldElement element)
    {
        return BaseIndexOf(element);
    }

    public void Remove(FieldElement element)
    {
        if (BaseIndexOf(element) >= 0)
        {
            BaseRemove(element.Name);
        }
    }

    public void Remove(string name)
    {
        BaseRemove(name);
    }

    public void RemoveAt(int index)
    {
        BaseRemoveAt(index);
    }

    #endregion
}

Разберемся, что к чему. Из кода видно, что была произведена замена атрибутов на реализацию собственной коллекции свойст.

  • ConfigurationProperty - класс, который представляет атрибут или дочерний элемент элемента конфигурации.
  • Класс ConfigurationPropertyCollection представляет коллекцию объектов ConfigurationProperty, которые могут быть либо атрибутами, либо объектами ConfigurationElement элементов конфигурации. В нашем случае, Properties содержат два атрибута (fileName и codePage) и коллекцию Fields.

Для того, чтобы не создавался дополнительный элемент, важна строка:

FieldsProperty = new ConfigurationProperty("", typeof(FieldsCollection), null, ConfigurationPropertyOptions.IsDefaultCollection);

Обратите внимание, что первым параметром мы передаем null (можно и пустую строку "" поставить), а при обращении к свойству Fields возвращаем коллекцию не по имени, как остальные свойства, а по самому объекту:

/// 
/// Коллекция столбцов таблицы
/// 
public FieldsCollection Fields
{
    get { return (FieldsCollection)base[FieldsProperty]; }
}

Кроме того, чтобы работать с коллекцией элементов (FieldsCollection) в коде подобно тому, как это делается в случае KeyValueConfigurationCollection или ConnectionStringSettingsCollection, я описал дополнительные методы в FieldsCollection.

Полученный обработчик совершенно спокойно скушает следующую конфигурацию:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <configSections>
        <section name="tableFields" type="UserSettings2.TableSection, UserSettings2" />
    </configSections>
    <tableFields fileName="file.txt" codePage="cp1251">
        <add name="Id" elementType="int" length="10" />
        <add name="Name" elementType="string" length="100" />
        <add name="UserName" elementType="string" length="55" />
        <add name="ObjectId" elementType="int" length="10" />
        <add name="ParentObjectId" elementType="int" length="10" />
        <add name="SerialNumber" elementType="string" length="20" />
    </tableFields>
</configuration>

Управление конфигурацией из кода

На небольшом примере покажу, как программно можно изменять и сохранять конфигурацию:

class Program
{
    static void Main(string[] args)
    {
        Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
        TableSection tableSection = new TableSection { CodePage = "cp1251", FileName = "file.txt" };
        tableSection.Fields.Add(new FieldElement { Name = "Id", ElementType = "int", Length = 10 });
        tableSection.Fields.Add(new FieldElement { Name = "Name", ElementType = "string", Length = 100 });
        tableSection.Fields.Add(new FieldElement { Name = "UserName", ElementType = "string", Length = 50 });
        tableSection.Fields.Add(new FieldElement { Name = "ObjectId", ElementType = "int", Length = 10 });
        tableSection.Fields.Add(new FieldElement { Name = "ParentObjectId", ElementType = "int", Length = 10 });
        tableSection.Fields.Add(new FieldElement { Name = "SerialNumber", ElementType = "string", Length = 20 });

        tableSection.Fields["UserName"].Length = 55;
        config.Sections.Remove("tableFields");
        config.Sections.Add("tableFields", tableSection);

        config.SaveAs("test.xml");
    }
}

Думаю, что код предельно понятен и не требует пояснений. Если возникнут вопросы пишите в комментариях или смотрите справку на msdn.

Конечно, может показаться, что проделано неоправданно много работы. С одной стороны - да. С другой - если, например, одни и те же настройки таскаются из проекта в проект, то можно выделить это все в отдельную сборку и дальше пользоваться этим удобным инструментом. К тому же такой вид добавляет настройкам наглядность и позволяет использовать единый подход для работы с файлом конфигурации. Считаю, что мы получили красивый результат.

Ссылки:

2 комментария:

  1. Здравствуйте. В последнем примере вы удаляете одно поле. В случае с web.config будет ли происходит перезагрузка пула приложения?

    ОтветитьУдалить
  2. Честно говоря, никогда не проверял). Но если учесть, что при изменении файла конфигурации пул приложения обычно перезагружается, то думаю, что и в этом случае будет точно также. Если, конечно, не возникнет каких-нибудь проблем с правами на запись...

    ОтветитьУдалить