воскресенье, 25 ноября 2012 г.

Хранение однотипных групп пользовательских настроек, созданных во время выполнения приложения [Visual Studio, .NET]

[Using of the user scope settings for the storing the similar setting groups in run-time]

Платформа .NET и Visual Studio предлагает относительно развитые возможности для простого использования настроек приложения. Это, например, разделение настроек на пользовательские (UserScope) и программные (AppScope), их статическая типизация, наличие встроенного в Visual Studio дизайнера и прочие вкусности. Неплохой обзор для VS 2005, но все еще актуальный, можно посмотреть здесь:
http://msdn.microsoft.com/en-us/library/aa730869(v=vs.80).aspx

Всего этого достаточно, чтобы покрыть большинство потребностей небольшого приложения. Но если вам когда-нибудь понадобиться выйти за границы статического объявления настроек и/или захочется хранить данные в своем формате и тем не менее оставаться в рамках стандартных механизмов .NET, вам, скорее всего, придется разбираться с классами ConfigurationManager и ConfigurationSection:
http://msdn.microsoft.com/en-us/library/system.configuration.configurationmanager(v=vs.100).aspx

Так случилось и со мной, мне понадобилось хранить группы определенных настроек, количество которых определялось во время выполнения программы. Группы нужно было идентифицировать по некоторому ключу, в моем случае им была уникальная строка. Но в своем решении я избежал сложностей испльзования ConfigurationManager, а просто “вложил” свою группу настроек в одну пользовательскую установку. Решение получилось достаточно простым, но не совсем очевидным, поэтому с удовольствием спешу поделиться этим знанием :)

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


Здесь-то мы и добавим наш “контейнер” групп, но для начала нужно его объявить.

Для многократного использования общей конструкции я использовал общий (generic) класс. В моей задаче мне нужно было ко всему прочему иметь “глобальное” значение, которое бы использовалось как стандартное для всех новых и/или неопределенных значений. Ниже код:

public class SettingSetBase<T>
{
   public SettingSetBase() { // ctor
     KeyValList = new List<KeyVal<T>>();
     DefaultValue = default(T);
   }

   public SettingSetBase(T defVal) { // ctor
     KeyValList = new List<KeyVal<T>>();
     DefaultValue = defVal;
   }

   public T DefaultValue { get; set; }
   public List<KeyVal<T>> KeyValList { get; set; }

   public T GetValue(string key) {
     KeyVal<T> keyVal = KeyValList.FirstOrDefault(x => String.Equals(x.Key, key, StringComparison.CurrentCultureIgnoreCase));
     if (keyVal == null) return DefaultValue;
     else return keyVal.Val;
   }

   public void SetValue(String key, T val) {
     KeyVal<T> keyVal = KeyValList.FirstOrDefault(x => String.Equals(x.Key, key, StringComparison.CurrentCultureIgnoreCase));
     if (keyVal == null)
       KeyValList.Add(new KeyVal<T>() { Key = key, Val = val });
     else
       keyVal.Val = val;
   }
}

public class KeyVal<T>
{
   public String Key { get; set; }
   public T Val { get; set; }
}


* This source code was highlighted with Source Code Highlighter.

Не каждый тип может быть использован в качестве типа установки, важное условие - он должен быть Xml-сериализуемым. Избегая излишних сложностей сериализации словарей (Dictionary) применил List и пары значение ключ.

Теперь этот generic-класс можно “конкретизировать” любым сериализуемым типом:

// Например для String:
public class SettingSetString : SettingSetBase<String>
{
   public SettingSetString() { }
   public SettingSetString(String defVal) : base(defVal) { }
}

// Или для булевых значений:
public class SettingSetBool : SettingSetBase<bool>
{
   public SettingSetBool() { }
   public SettingSetBool(bool defVal) : base(defVal) { }
}

// Или для глобального уникального идентификатора (GUID):
public class SettingSetGuid : SettingSetBase<Guid>
{
   public SettingSetGuid() { }
   public SettingSetGuid(Guid defVal) : base(defVal) { }
}

// Или даже для его обнулямого варианта:
public class SettingSetGuidNullable : SettingSetBase<Guid?>
{
   public SettingSetGuidNullable() { }
   public SettingSetGuidNullable(Guid? defVal) : base(defVal) { }
}


* This source code was highlighted with Source Code Highlighter.

Конечно же, элементарными типами дело не ограничивается и можно использовать комплексные xml-сериализуемые типы, как например этот для хранения параметров окон:
public class WinPosSett
{
  public Point Location { get; set; }
  public int Width { get; set; }
  public int Height { get; set; }
}


* This source code was highlighted with Source Code Highlighter.

Теперь все готово, можно объявить контейнеры настроек в дизайнере. Ограничусь для демонстрации двумя видами - SettingSetString и SettingSetWinPosSett:

Замечание! Что бы эти типы стали доступными для выбора, необходимо пересоздать проект.

Как вы может быть уже заметили, для таких типов в дизайнере невозможно определить стандартное значение (колонка “Value”, или “Wert“ у меня в немецкой студии). Поэтому просто, не усложняя, расширим созданный генератором класс настроек следующим методом:

internal sealed partial class Settings
{
   public void InitDefaults() {
     if (MyStringSet == null)
       MyStringSet = new SettingSetString("DefaultValue");
     if (MyWinPosSet == null)
       MyWinPosSet = new SettingSetWinPosSett(new WinPosSett() { Height = 300, Width = 300, Location = new Point(100, 100) });
   }
}


* This source code was highlighted with Source Code Highlighter.

Ну все, можно пользоваться:
Properties.Settings.Default.InitDefaults();
String strGroup1 = Properties.Settings.Default.MyStringSet.GetValue("Group1"); // "DefaultValue"
Properties.Settings.Default.MyStringSet.SetValue("Group1", "My new Value");
Properties.Settings.Default.Save();
String strGroup1NewValue = Properties.Settings.Default.MyStringSet.GetValue("Group1"); // "My new Value"
String strGroup2 = Properties.Settings.Default.MyStringSet.GetValue("Group2"); // "DefaultValue"


* This source code was highlighted with Source Code Highlighter.

Вот как выглядит часть файла user.config после выполненных операций:

<setting name="MyStringSet" serializeAs="Xml">
  <value>
    <SettingSetString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:xsd="http://www.w3.org/2001/XMLSchema">
      <DefaultValue>DefaultValue</DefaultValue>
      <KeyValList>
        <KeyValOfString>
          <Key>Group1</Key>
          <Val>My new Value</Val>
        </KeyValOfString>
      </KeyValList>
    </SettingSetString>
  </value>
</setting>


* This source code was highlighted with Source Code Highlighter.

Неплохо! Теперь попробуем использовать комплексный тип WinPosSett:
WinPosSett WinPosMainWindow = Properties.Settings.Default.MyWinPosSet.GetValue("MainForm");
// .. используем значения для установки размеров и позиции окна
// ..
// .. окно закрывается, сохраняем значения:
Properties.Settings.Default.MyWinPosSet.SetValue("MainForm", new WinPosSett() { Height = 400, Width = 200, Location = new Point(10, 10) });
Properties.Settings.Default.Save();


* This source code was highlighted with Source Code Highlighter.

Сохраненные настройки выглядят так:
<setting name="MyWinPosSet" serializeAs="Xml">
<value>
   <SettingSetWinPosSett xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
     <DefaultValue>
       <Location>
         <X>100</X>
         <Y>100</Y>
       </Location>
       <Width>300</Width>
       <Height>300</Height>
     </DefaultValue>
     <KeyValList>
       <KeyValOfWinPosSett>
         <Key>MainForm</Key>
         <Val>
           <Location>
             <X>10</X>
             <Y>10</Y>
           </Location>
           <Width>200</Width>
           <Height>400</Height>
         </Val>
       </KeyValOfWinPosSett>
     </KeyValList>
   </SettingSetWinPosSett>
</value>
</setting>


* This source code was highlighted with Source Code Highlighter.

Пример приложения можно скачать здесь:
https://sites.google.com/site/selosite/GroupUserSettings.zip