Własna konfiguracja w app.config

Jeśli to czytacie, to znaczy że zdążyłem jeszcze z wpisem przed nowym rokiem.  Udało mi się napisać jeszcze jednego posta przed nowym rokiem – jestem królem świata!

Dzisiaj o wykorzystaniu app.config dla własnych celów. Skąd taki pomysł? Otóż jak każdy pewnie ma lub miał w życiu, czasem mieć taką potrzebę aby zmieniać zachowanie aplikacji inaczej niż przez jej przekompilowanie. Wtedy właśnie z pomocą przychodzą pliki .txt, .xml, .config czy dawno dawno temu .ini. Można napisać własne system do obsługi konfiguracji, własne klasy serializujące i deserializujące, można pisać cudne rzeczy. Ale po co, skoro można skorzystać z tego co oferuje app.config. Też tak pomyślałem i rozpętało się piekło. Bo nie da się ot tak dowalić do app.configu swojego kawałka xmla. Trzba to wszystko dokładnie, ale to dokładnie opisać. O ile wytrzymacie ten wpis, to zaraz wszystko wyjaśnię.

Zacznę od początku na prostych obiektach. Np. gdybym chciał w pliku zdefiniować wygląd jakiegoś prostego obiektu, np.

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, „Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <configuration>
   3:      <MyConfiguration>
   4:          <Rectangle Color="Red" Width="10" Height="5" Fill="False"/>
   5:      </MyConfiguration>
   6:  </configuration>

Wiecie co się stanie przy próbie uruchomienia takiego prostego kodu jak ten:

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, „Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  static void Main(string[] args)
   2:  {
   3:      var cfg = ConfigurationManager.AppSettings;
   4:  }

Aby konfiguracja zadziałała, trzeba dodać informację do app.cofig o naszej sekcji konfiguracyjnej.

   1:  <configSections>
   2:      <section name="MyConfiguration" type="AppConfigExample.MyConfiguration, AppConfigExample"/>
   3:  </configSections>

W części type podajemy gdzie znajdzie się klasa opisująca naszą konfigurację. Wpis musi zawierać pełny namespace, następnie należy podać także assembly w który się ona znajduje. Tutaj wszystko mam w jednym assembly.
Skoro trzeba podać klasę to trzeba ją także gdzieś i jakoś zdefiniować i podziedziczyć:

   1:  public class MyConfiguration : ConfigurationSection
   2:  {
   3:  }

A następnie spróbować się do niej dostać:

   1:  var cfg = ConfigurationManager.GetSection("MyConfiguration");

Niestety to nadal za mało (powoli buduje nastrój grozy):

Lubię .net i jego wyjątki za czytelne błędy i opisy problemów, lubię też google czy SO za pomoc w rozwiązywaniu tych wyjątków. Chwila szukania i już wiem co należy dodać, aby móc odczytać mój kwadracik z konfiguracji:

   1:  public class MyConfiguration : ConfigurationSection
   2:  {
   3:      [ConfigurationProperty("Rectangle")]
   4:      public Rectangle Rectangle
   5:      {
   6:          get { return ((Rectangle)(base["Rectangle"])); }
   7:      }
   8:  }

Skoro wspominamy tutaj o klasie Rectangle to należy ją także jakoś zdefiniować i znowu dziedziczenie:

   1:  public class Rectangle: ConfigurationElement
   2:  {
   3:      [ConfigurationProperty("Color")]
   4:      public ConsoleColor Color
   5:      {
   6:          get { return (ConsoleColor)base["Color"]; }
   7:          set { base["Color"] = value; }
   8:      }
   9:   
  10:      [ConfigurationProperty("Width")]
  11:      public int Width
  12:      {
  13:          get { return (int)base["Width"]; }
  14:          set { base["Width"] = value; }
  15:      }
  16:   
  17:      [ConfigurationProperty("Height")]
  18:      public int Height
  19:      {
  20:          get { return (int)base["Height"]; }
  21:          set { base["Height"] = value; }
  22:      }
  23:   
  24:      [ConfigurationProperty("Fill")]
  25:      public bool Fill
  26:      {
  27:          get { return (bool)base["Fill"]; }
  28:          set { base["Fill"] = value; }
  29:      }
  30:  }

Gdy mamy taki kod możemy spokojnie spróbować swoich sił i uruchomić coś takiego:

   1:  var cfg = ConfigurationManager.GetSection("MyConfiguration") as MyConfiguration;
   2:   
   3:  var oc = Console.ForegroundColor;
   4:  Console.ForegroundColor = cfg.Rectangle.Color;
   5:   
   6:  Console.WriteLine("Width:{0}, Height:{1}, Filled:{2}", cfg.Rectangle.Width, cfg.Rectangle.Height,
   7:                    cfg.Rectangle.Fill);
   8:   
   9:  Console.ForegroundColor = oc;
Co powinno dać taki wynik:

Chyba nie macie dosyć? Nie warto zaprzestawać na tylko jednym takim obiekcie, skoro na świecie jest tyle pięknych prostokątów, załóżmy taki przypadek:

   1:  <MyConfiguration>
   2:      <Rectangle Color="Red" Width="10" Height="5" Fill="False"/>
   3:      <Rectangle Color="Green" Width="12" Height="6" Fill="False"/>
   4:      <Rectangle Color="Black" Width="14" Height="3" Fill="True"/>
   5:      <Rectangle Color="White" Width="15" Height="2" Fill="True"/>
   6:      <Rectangle Color="Gray" Width="16" Height="10" Fill="False"/>
   7:  </MyConfiguration>

Co się stanie po uruchomieniu aplikacji z taką konfiguracją?

No racja racja, przecież to kolekcja prostokątów, powinna wyglądać inaczej, bo skąd biedny komputerek może wiedzieć o który obiekt dokładnie nam chodzi.

Macie tak czasami, że wy wiecie że się nie uda czegoś zrobić, a komputer jeszcze walczy? Np. klikniecie przez przypadek na napędzie CD, tam nie ma płyty, wszyscy to wiedzą, ale nie, 15 sekund trzeba czekać zanim komputerek też się dowie. Sieć nie działa, wszyscy wiedzą, a komp walczy. Najgorsze jest to że się nie uczy. Skoro nie było płyty, i napęd się nie wysunął i nie wsunął, to płyty nadal tam nie będzie. Ale kliknij znowu na ikonę z CD i znowu czekasz, bo może jednak jest. Ech takie szybkie i takie głupie.

Wracając do naszego problemu, jak zrobić żeby ten biedny komputerek poradził sobie z wieloma prostokącikami:

   1:  <Rectangles>
   2:      <Rectangle Color="Red" Width="10" Height="5" Fill="False"/>
   3:      <Rectangle Color="Green" Width="12" Height="6" Fill="False"/>
   4:      <Rectangle Color="Black" Width="14" Height="3" Fill="True"/>
   5:      <Rectangle Color="White" Width="15" Height="2" Fill="True"/>
   6:      <Rectangle Color="Gray" Width="16" Height="10" Fill="False"/>
   7:  </Rectangles>

Ale wtedy znowu będzie marudzić, że unrecognized element i że „Rectangles” to ten unrecognized. Trzeba więc zaktualizować informację z klasie MyConfiguration na taki:

   1:  public class MyConfiguration : ConfigurationSection
   2:  {
   3:      [ConfigurationProperty("Rectangles")]
   4:      public Rectangles Rectangles
   5:      {
   6:          get { return ((Rectangles)(base["Rectangles"])); }
   7:      }
   8:  }

Oraz zdefiniować odpowiednio klasę Rectangles. Możecie spróbować samodzielnie ją zdefiniować i zobaczyć jaki poleci wyjątek, lub też przeczytać dalej i zobaczyć jak robią to inni:

   1:  [ConfigurationCollection(typeof(Rectangle))]
   2:  public class Rectangles : ConfigurationElementCollection
   3:  {
   4:      protected override ConfigurationElement CreateNewElement()
   5:      {
   6:          return new Rectangle();
   7:      }
   8:   
   9:      protected override object GetElementKey(ConfigurationElement element)
  10:      {
  11:          return element.GetHashCode();
  12:      }
  13:  }

Linijka 11 nie wygląda najlepiej. A poza tym może spowodować błąd (podczas domyślnej implementacji GetHashCode), aby go uniknąć należy napisać własne GetHashCode w klasie Rectangle lub dołożyć jakiś klucz, co później ułatwi dostęp do wybranych elementów z kolekcji.
Klasę Rectangle należy uaktualnić o następujące pole:

   1:  [ConfigurationProperty("Id", IsKey = true, IsRequired = true)]
   2:  public int Id
   3:  {
   4:      get { return (int)base["Id"]; }
   5:      set { base["Id"] = value; }
   6:  }

Natomiast metoda GetElementKey wyglądać teraz będzie tak:

   1:  protected override object GetElementKey(ConfigurationElement element)
   2:  {
   3:      return ((Rectangle)element).Name;
   4:  }

Kolekcja elementów w app.config wygląda teraz tak:

   1:  <Rectangles>
   2:      <add Id="1" Color="Red" Width="10" Height="5" Fill="False"/>
   3:      <add Id="2" Color="Green" Width="12" Height="6" Fill="False"/>
   4:      <add Id="3" Color="Black" Width="14" Height="3" Fill="True"/>
   5:      <add Id="4" Color="White" Width="15" Height="2" Fill="True"/>
   6:      <add Id="5" Color="Gray" Width="16" Height="10" Fill="False"/>
   7:  </Rectangles>

I znowu coś nie cool, to idiotyczne add – bleh, brzydal. Gdzie się podziało ładne Rectangle? Ponownie zachęcam do sprawdzenia wyjątku jaki leci, gdy zamiast add będzie Rectangle. Na szczęście i to da się napisać lepiej, wystarczy ponownie zaktualizować informację o klasie Rectangles, brakuje tam małej informacji w atrybutach klasy:

   1:  [ConfigurationCollection(typeof(Rectangle), AddItemName = "Rectangle")]
   2:  public class Rectangles : ConfigurationElementCollection { /* unicorns here */ }

Od teraz znowu zamiast add możemy stosować Rectangle i uruchomić taki kod:

   1:  var cfg = ConfigurationManager.GetSection("MyConfiguration") as MyConfiguration;
   2:  var oc = Console.ForegroundColor;
   3:   
   4:  foreach (Rectangle rectangle in cfg.Rectangles)
   5:  {
   6:      Console.ForegroundColor = rectangle.Color;
   7:      Console.WriteLine("Width:{0}, Height:{1}, Filled:{2}", rectangle.Width, rectangle.Height,rectangle.Fill);
   8:      Console.ForegroundColor = oc;
   9:  }

Co według mojego działania da taki wynik:

A ja wybrać ten jeden w swoim rodzaju Rectangl? Skoro mamy jego Id to powinniśmy z niego jakoś skorzystać. Może linq? Potrzebujemy tylko IEnumerable w klasie Rectangles i będzie działać, a więc:

   1:  [ConfigurationCollection(typeof(Rectangle), AddItemName = "Rectangle")]
   2:  public class Rectangles: ConfigurationElementCollection, IEnumerable<Rectangle>
   3:  {
   4:      { /* dragons here */ }
   5:      public new IEnumerator<Rectangle> GetEnumerator()
   6:      {
   7:          int count = base.Count;
   8:          for (int i = 0; i < count; i++)
   9:          {
  10:              yield return base.BaseGet(i) as Rectangle;
  11:          }
  12:      }
  13:  }

I można już napisać coś takiego w kodzie:

   1:  Rectangle rect = cfg.Rectangles.Single(r => r.Id == 2);
   2:   
   3:  Console.ForegroundColor = rect.Color;
   4:  Console.WriteLine("Width:{0}, Height:{1}, Filled:{2}", rect.Width, rect.Height, rect.Fill);

Od teraz wszyscy mogą zazdrościć wam wiedzy o tym, jak lepiej wykorzystywać app.config w swoich aplikacjach.

A teraz cały kod z wpisu:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Configuration;
   4:  using System.Linq;
   5:  using System.Text;
   6:   
   7:  namespace AppConfigExample
   8:  {
   9:      class Program
  10:      {
  11:          static void Main(string[] args)
  12:          {
  13:              var cfg = ConfigurationManager.GetSection("MyConfiguration") as MyConfiguration;
  14:              var oc = Console.ForegroundColor;
  15:   
  16:              foreach (Rectangle rectangle in cfg.Rectangles)
  17:              {
  18:                  Console.ForegroundColor = rectangle.Color;
  19:                  Console.WriteLine("Width:{0}, Height:{1}, Filled:{2}", rectangle.Width, rectangle.Height, rectangle.Fill);
  20:              }
  21:   
  22:              Console.WriteLine("****************************************");
  23:              Rectangle rect = cfg.Rectangles.Single(r => r.Id == 2);
  24:   
  25:              Console.ForegroundColor = rect.Color;
  26:              Console.WriteLine("Width:{0}, Height:{1}, Filled:{2}", rect.Width, rect.Height, rect.Fill);
  27:   
  28:   
  29:   
  30:              Console.ForegroundColor = oc;
  31:          }
  32:      }
  33:   
  34:   
  35:      public class MyConfiguration : ConfigurationSection
  36:      {
  37:          [ConfigurationProperty("Rectangles")]
  38:          public Rectangles Rectangles
  39:          {
  40:              get { return ((Rectangles)(base["Rectangles"])); }
  41:          }
  42:      }
  43:   
  44:      [ConfigurationCollection(typeof(Rectangle), AddItemName = "Rectangle")]
  45:      public class Rectangles : ConfigurationElementCollection, IEnumerable<Rectangle>
  46:      {
  47:          protected override ConfigurationElement CreateNewElement()
  48:          {
  49:              return new Rectangle();
  50:          }
  51:   
  52:          protected override object GetElementKey(ConfigurationElement element)
  53:          {
  54:              return ((Rectangle)element).Id;
  55:          }
  56:   
  57:          public new IEnumerator<Rectangle> GetEnumerator()
  58:          {
  59:              int count = base.Count;
  60:              for (int i = 0; i < count; i++)
  61:              {
  62:                  yield return base.BaseGet(i) as Rectangle;
  63:              }
  64:          }
  65:      }
  66:   
  67:      public class Rectangle : ConfigurationElement
  68:      {
  69:          [ConfigurationProperty("Id", IsKey = true, IsRequired = true)]
  70:          public int Id
  71:          {
  72:              get { return (int)base["Id"]; }
  73:              set { base["Id"] = value; }
  74:          }
  75:   
  76:          [ConfigurationProperty("Color")]
  77:          public ConsoleColor Color
  78:          {
  79:              get { return (ConsoleColor)base["Color"]; }
  80:              set { base["Color"] = value; }
  81:          }
  82:   
  83:          [ConfigurationProperty("Width")]
  84:          public int Width
  85:          {
  86:              get { return (int)base["Width"]; }
  87:              set { base["Width"] = value; }
  88:          }
  89:   
  90:          [ConfigurationProperty("Height")]
  91:          public int Height
  92:          {
  93:              get { return (int)base["Height"]; }
  94:              set { base["Height"] = value; }
  95:          }
  96:   
  97:          [ConfigurationProperty("Fill")]
  98:          public bool Fill
  99:          {
 100:              get { return (bool)base["Fill"]; }
 101:              set { base["Fill"] = value; }
 102:          }
 103:      }
 104:  }

Oraz app.config:

   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <configuration>
   3:      <configSections>
   4:          <section name="MyConfiguration" type="AppConfigExample.MyConfiguration, AppConfigExample"/>
   5:      </configSections>
   6:      <MyConfiguration>
   7:          <Rectangles>
   8:              <Rectangle Id="1" Color="Red" Width="10" Height="5" Fill="False"/>
   9:              <Rectangle Id="2" Color="Green" Width="12" Height="6" Fill="False"/>
  10:              <Rectangle Id="3" Color="Black" Width="14" Height="3" Fill="True"/>
  11:              <Rectangle Id="4" Color="White" Width="15" Height="2" Fill="True"/>
  12:              <Rectangle Id="5" Color="Gray" Width="16" Height="10" Fill="False"/>
  13:          </Rectangles>
  14:      </MyConfiguration>
  15:  </configuration>

To by było na tyle, bierzcie i korzystajcie z tego, bo to dobre jest.
Pozostaje mi tylko życzyć dobrego w nadchodzącym nowym roku!
Tej!

4 thoughts on “Własna konfiguracja w app.config

  1. Dobry wpis, może się przydać, Podoba mi się też humor:)
    Najlepszego w Nowym Roku !
    -Driv3r

  2. Chyba kilka baboli jest w kodzie w fragmentach copy/paste, AnimationsCollection zamiast Rectangle, Name zamiast Id.

Dodaj komentarz