[PL] własny SettingsProvider
W .NET mamy do dyspozycji wygodny mechanizm do zapisywania ustawień aplikacji. Nie będę tutaj opisywać podstaw obsługi tego mechanizmu. Osoby niezaznajomione z tym mechanizmem odsyłam do dokumentacji. W tym wpisie skupię się na stworzeniu własnego dostawcy ustawień.
Domyślnym i jedynym standardowo dostępnym dostawcą, który zajmuje się zapisem ustawień jest LocalFileSettingsProvider. Dostawca ten zapisuje pliki do lokalnego katalogu ustawień danego komputera. W systemie Windows 7 przykładowa ścieżka dla pliku ustawień ma następującą postać:
C:\Users\Damian\AppData\Local\Microsoft\CustomSettingsProvider.ex_Url_sxc2govh1iyjc1j3np0gzhgzqqk3dctn\1.0.0.0\user.config
Ścieżka do pliku nie wygląda zbyt pięknie. W tym wpisie możemy się dowiedzieć (pytanie “Why is the path so obscure? Is there any way to change/customize it?”), że algorytm generujący ścieżkę musiał zapewnić jej unikalność. Ja chciałbym jednak uzyskać następującą ścieżkę:
C:\Users\Damian\AppData\Local\Microsoft\CustomSettingsProvider\user.config
Prawda, że ładniej? Standardowo nie mamy żadnej możliwości określenia w jakiej lokacji ma być przechowywany plik ustawień. Jedyną możliwością jest stworzenie własnego dostawcy ustawień, który musi dziedziczyć po klasie SettingsProvider. Tworząc nowego dostawcę musimy zaimplementować przynajmniej trzy następujące metody: GetPropertyValues, SetPropertyValues i ApplicationName. Na początek implementacja właściwości ApplicationName:
public override string ApplicationName { get { return EntryAssemblyHelper.ProductName; } set { } }
Na jej potrzeby stworzyłem klasę pomocniczą EntryAssemlbyHelper:
public static class EntryAssemblyHelper { #region properties public static string ProductName { get { return GetAttribute<AssemblyProductAttribute>().Product; } } public static string CompanyName { get { return GetAttribute<AssemblyCompanyAttribute>().Company; } } #endregion #region private methods private static T GetAttribute<T>() where T : class { return Assembly.GetEntryAssembly() .GetCustomAttributes(typeof(T), true) .First() as T; } #endregion }
We właściwości ApplicationName zwracana jest więc nazwa aplikacji zapisana w Assembly, które zostało uruchomione na samym początku. Następnie należy zaimplementować metodę SetPropertyValues, która to wygląda następująco:
public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection settings) { this.CreateRequiredFoldersAndFilesIfNeeded(); XDocument document; try { document = XDocument.Load(this.appSettingsFilePath); } catch { this.BackupSettingsFile(); document = new XDocument(); document.Add(new XElement("settings")); } XElement root = document.Element("settings"); string groupName = context["GroupName"].ToString(); XElement contextSettings = root.Element(groupName); if (contextSettings == null) { contextSettings = new XElement(groupName); root.Add(contextSettings); } IList<XElement> nodesToAdd = new List<XElement>(); foreach (SettingsPropertyValue property in settings) { bool saveWithoutDefault = false; XElement setting = contextSettings.Elements() .Where(element => element.Name == "setting" && element.HasAttributes && element.FirstAttribute.Name == "name" && element.FirstAttribute.Value == property.Name && element.HasElements) .FirstOrDefault(); if (setting != null) { XElement defaultValue = setting.Element("defaultValue"); if (defaultValue != null) { setting = new XElement("setting", new XAttribute("name", property.Name), new XElement("value", property.SerializedValue), new XElement("defaultValue", defaultValue.Value)); } else { saveWithoutDefault = true; } } else { saveWithoutDefault = true; } if (saveWithoutDefault) { setting = new XElement("setting", new XAttribute("name", property.Name), new XElement("value", property.SerializedValue)); } nodesToAdd.Add(setting); } contextSettings.RemoveAll(); foreach (XElement node in nodesToAdd) contextSettings.Add(node); document.Save(this.appSettingsFilePath); }
Parametr “context” typu SettingsContext(klasa dziedziczy po Hashtable) zawiera informacje na temat pliku “settings” w jakim zostały stworzone nasze ustawienia. Podglądając tą zmienną podczas debugowania możemy zobaczyć np. takie informacje:
Nas interesuje klucz “GroupName”, który zawiera nazwę i przestrzeń nazw w jakiej znajduje się plik ustawień. Implementacja metdy SetPropertyValues używa LINQ to XML do zapisu ustawień. Wynikiem działania tej metody będzie plik XML o następującej strukturze:
<?xml version="1.0" encoding="utf-8"?> <settings> <CustomSettingsProvider.FavoriteSettings> <setting name="FavoriteActor"> <value>Clint Eastwood</value> <defaultValue>Edward Norton</defaultValue> </setting> <setting name="FavoriteMovie"> <value>The Good, the Bad and the Ugly</value> <defaultValue>Fight Club</defaultValue> </setting> </CustomSettingsProvider.FavoriteSettings> <CustomSettingsProvider.HouseSettings> <setting name="DoorsColor"> <value>Gold</value> <defaultValue>Blue</defaultValue> </setting> </CustomSettingsProvider.HouseSettings> </settings>
Zostały tutaj zapisane dwa węzły “CustomSettingsProvider.FavoriteSettings” oraz “CustomSettingsProvider.HouseSettings”, ponieważ w przykładowym projekcie stworzyłem dwa pliki typu “settings”. Kolej na implementację metody GetPropertyValues, która będzie odczytywać zapisane ustawienia:
public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection settingsCollection) { XDocument document; try { document = XDocument.Load(this.appSettingsFilePath); } catch { document = new XDocument(); } XElement root = document.Element("settings"); if (root == null) { root = new XElement("settings"); document.Add(root); } string groupName = context["GroupName"].ToString(); XElement contextSettings = root.Element(groupName); if (contextSettings == null) { contextSettings = new XElement(groupName); root.Add(contextSettings); } SettingsPropertyValueCollection settings = new SettingsPropertyValueCollection(); foreach (SettingsProperty property in settingsCollection) { XElement setting = contextSettings.Elements() .Where(element => element.Name == "setting" && element.HasAttributes && element.FirstAttribute.Name == "name" && element.FirstAttribute.Value == property.Name && element.HasElements) .FirstOrDefault(); if (setting != null) { XElement propertyValue = setting.Element("value"); settings.Add(new SettingsPropertyValue(property) { SerializedValue = propertyValue.Value }); XElement defaultPropertyValue = setting.Element("defaultValue"); if (defaultPropertyValue == null) { defaultPropertyValue = new XElement("defaultValue"); setting.Add(defaultPropertyValue); } defaultPropertyValue.Value = property.DefaultValue.ToString(); } else { XElement propertySetting = new XElement("setting", new XAttribute("name", property.Name), new XElement("value", property.DefaultValue), new XElement("defaultValue", property.DefaultValue)); contextSettings.Add(propertySetting); settings.Add(new SettingsPropertyValue(property) { SerializedValue = property.DefaultValue }); } } document.Save(this.appSettingsFilePath); return settings; }
W przypadku braku pliku lub braku informacji o ustawieniach w pliku, zostaną zwrócone wartości domyślne. Również i w tej metodzie zostało użyte LINQ to XML.
Przedstawione trzy metody wystarczą w zupełności do stworzenia własnego dostawcy ustawień. Pozostaje jednak pytanie jak tego dostawcy użyć? Po stworzeniu pliku “settings” należy zaznaczyć daną właściwość i przejść na zakładkę “Properties”. W tej zakładce w polu “Provider” należy wpisać nazwę stworzonego dostawcy. Poniżej został przedstawiony zrzut ekranu, który to obrazuje:
W jednym pliku “settings” możemy używać wielu dostawców. W przykładowym projekcie, który stworzyłem, plik “FavoriteMovie.settings” używa mojego dostawcy w ustawieniach “FavoriteMovie” oraz “FavoriteActor”. “FavoriteBand” jest zapisywany przed domyślnego dostawcę LocalFileSettingsProvider.
A co z przywróceniem domyślnych ustawień? W tym celu nasz dostawca ustawień musi implementować interfejs IApplicationSettingsProvider. Interfejs ten zawiera trzy metody: GetPreviousVersion, Reset, Upgrade. Ich implementacja została przedstawiona poniżej:
public SettingsPropertyValue GetPreviousVersion(SettingsContext context, SettingsProperty property) { return null; } public void Reset(SettingsContext context) { if (File.Exists(this.appSettingsFilePath)) { XDocument document; try { document = XDocument.Load(this.appSettingsFilePath); } catch (Exception exception) { throw new SettingsException("Can't read settings file. Can't reset to default values. See inner exception for details.", exception); } XElement root = document.Element("settings"); if (root == null) this.ThrowExceptionDefaultDataNotFound(); string groupName = context["GroupName"].ToString(); XElement contextSettings = root.Element(groupName); if (contextSettings == null) this.ThrowExceptionDefaultDataNotFound(); foreach (XElement node in contextSettings.Elements()) { XElement defaultValue = node.Element("defaultValue"); if (defaultValue != null) { XElement value = node.Element("value"); if (value == null) { value = new XElement("value"); node.Add(value); } value.Value = defaultValue.Value; } } document.Save(this.appSettingsFilePath); } } public void Upgrade(SettingsContext context, SettingsPropertyCollection properties) { }
Metoda GetPreviousVersion oraz Upgrade, nie robią nic ponieważ obecna implementacja mojego dostawcy ustawień, nie uwzględnia wersji programu przy zapisie ustawień do pliku. Uwzględnienie wersji programu nie jest potrzebne jeśli przyjrzycie się implementacji metod SetPropertyValues oraz GetPropertyValues. Z kolei implementacja metody Reset odczytuje wcześniej zapisany plik z ustawieniami i szuka węzłów z domyślnymi ustawieniami. Następnie obecne wartości ustawień są zamienianie na domyślne.
Klasy SettingsProvider można użyć np. do stworzenia dostawcy, który sprawdzi się w przypadku aplikacji typu “portable” albo dostawcy, który będzie zapisywać ustawienia w rejestrze. Jeśli potrzebujemy szyfrować ustawienia to stworzenie własnego dostawcy może okazać się dobrym pomysłem.
Tutaj znajduje się link do źródeł przykładowego projektu, który korzysta z mojego dostawcy ustawień. Projekt został stworzony w Visual Studio 2010. Klasa dostawcy powinna niedługo trafić do repozytorium DotBeer’a.
[Edit 22.08.10 13:03]
Po przespanej nocy i ponownym przeczytaniu kodu znalazłem parę błędów w kodzie. Kod we wpisie został zaktualizowany, jak i również przykładowy projekt.
Dobry kawalek kodu ale mam pytanie czy CustomLocalFileSettingsProvider nie powinien byc singletonem.. ile razy siegam w kodzie po ustawienia zapisane w klasie FavoriteSettings tyle razy wolany jest konstruktor Twojego providera. Jesli singleton to co w takim razie z thread-safety. Bylbym Ci wdzieczny za rozwianie moich watpliwosci