MVVM-Implementierung der WPF-Anwendungskonfiguration, die auf dem Catel-Framework basiert

Das Implementieren der Verwaltung von Softwareeinstellungen ist wahrscheinlich eines der Dinge, die in fast jeder Anwendung anders implementiert werden. Die meisten Frameworks und anderen Add-Ons bieten normalerweise ihre eigenen Tools zum Speichern / Laden von Werten aus einem Schlüsselwert der Parameterspeicherung.


In den meisten Fällen liegt die Implementierung eines bestimmten Einstellungsfensters und damit verbundener Dinge jedoch im Ermessen des Benutzers. In diesem Artikel möchte ich den Ansatz teilen, zu dem ich gekommen bin. In meinem Fall muss ich die Arbeit mit Einstellungen im MVVM-freundlichen Stil und unter Verwendung der Besonderheiten des in diesem Fall verwendeten Catel-Frameworks implementieren.


Haftungsausschluss : In diesem Hinweis gibt es keine technischen Feinheiten, die schwieriger sind als die grundlegende Reflexion. Dies ist nur eine Beschreibung des Ansatzes zur Lösung eines kleinen Problems, das ich am Wochenende hatte. Ich wollte darüber nachdenken, wie ich den Standard-Boilerplate-Code und das Kopieren und Einfügen im Zusammenhang mit dem Speichern / Laden von Anwendungseinstellungen entfernen kann. Die Lösung selbst erwies sich dank der praktischen verfügbaren .NET / Catel-Tools als eher trivial, aber vielleicht spart jemand ein paar Stunden Zeit oder schlägt nützliche Gedanken vor.


Catel Framework Brief

Wie andere WPF-Frameworks (Prism, MVVM Light, Caliburn.Micro usw.) bietet Catel praktische Tools zum Erstellen von Anwendungen im MVVM-Stil.
Die Hauptkomponenten:


  • IoC (integriert in MVVM-Komponenten)
  • ModelBase: Eine Basisklasse, die die automatische Implementierung von PropertyChanged (insbesondere in Verbindung mit Catel.Fody), Serialisierung und BeginEdit / CancelEdit / EndEdit (klassisch "apply" / "cancel") bietet.
  • ViewModelBase kann an das Modell binden und dessen Eigenschaften umschließen.
  • Arbeiten Sie mit Ansichten, die automatisch erstellt und an das ViewModel gebunden werden können. Verschachtelte Steuerelemente werden unterstützt.

Anforderungen


Wir gehen davon aus, dass wir von den Konfigurationstools Folgendes erwarten:


  • Einfach strukturierter Zugriff auf die Konfiguration. Zum Beispiel
    CultureInfo culture = settings.Application.PreferredCulture;
    TimeSpan updateRate = settings.Perfomance.UpdateRate; .
    • Alle Parameter werden als normale Eigenschaften dargestellt. Die Speichermethode ist im Inneren gekapselt. Bei einfachen Typen sollte alles automatisch ablaufen, bei komplexeren Typen sollte es möglich sein, die Serialisierung des Werts in eine Zeichenfolge zu konfigurieren.
  • Einfachheit und Zuverlässigkeit. Ich möchte keine fragilen Tools wie die vollständige Serialisierung des gesamten Konfigurationsmodells oder eines Entity Frameworks verwenden. Auf der unteren Ebene bleibt die Konfiguration ein einfaches Repository von Parameter-Wert-Paaren.
  • Die Möglichkeit, an der Konfiguration vorgenommene Änderungen zu verwerfen, z. B. wenn der Benutzer im Einstellungsfenster auf "Abbrechen" geklickt hat.
  • Möglichkeit, Konfigurationsupdates zu abonnieren. Beispielsweise möchten wir die Anwendungssprache unmittelbar nach dem Ändern der Konfiguration aktualisieren.
  • Migration zwischen Anwendungsversionen. Es sollte möglich sein, Aktionen beim Umschalten zwischen Anwendungsversionen festzulegen (Parameter umbenennen usw.).
  • Minimaler Boilerplate-Code, minimale Tippfehler. Im Idealfall möchten wir nur die Auto-Eigenschaft festlegen und nicht darüber nachdenken, wie sie gespeichert wird, unter welchem ​​Zeichenfolgenschlüssel usw. Wir möchten nicht jede der Eigenschaften im Ansichtsmodell des Einstellungsfensters manuell kopieren, alles sollte automatisch funktionieren.

Standardwerkzeuge


Catel bietet den IConfigurationService-Dienst, mit dem Werte von Zeichenfolgenschlüsseln aus dem lokalen Speicher (eine Datei auf der Festplatte in der Standardimplementierung) gespeichert und geladen werden können.


Wenn wir diesen Dienst in seiner reinen Form verwenden möchten, müssen wir diese Schlüssel selbst deklarieren, indem wir beispielsweise folgende Konstanten festlegen:


 public static class Application { public const String PreferredCulture = "Application.PreferredCulture"; public static readonly String PreferredCultureDefaultValue = Thread.CurrentThread.CurrentUICulture.ToString(); } 

Dann können wir diese Parameter wie folgt erhalten:


 var preferredCulture = new CultureInfo(configurationService.GetRoamingValue( Application.PreferredCulture, Application.PreferredCultureDefaultValue)); 

Es ist viel und mühsam zu schreiben. Bei vielen Einstellungen ist es einfach, Tippfehler zu machen. Darüber hinaus unterstützt der Dienst nur einfache Typen, z. B. kann CultureInfo nicht ohne zusätzliche Transformationen gespeichert werden.


Um die Arbeit mit diesem Service zu vereinfachen, wurde ein Wrapper erhalten, der aus mehreren Komponenten besteht.


Der vollständige Beispielcode ist im GitHub-Repository verfügbar. Es enthält die einfachste Anwendung mit der Möglichkeit, einige Parameter in den Einstellungen zu bearbeiten und sicherzustellen, dass alles funktioniert. Ich habe mich nicht um die Lokalisierung gekümmert. Der Parameter "Sprache" in den Einstellungen wird ausschließlich zur Demonstration der Konfiguration verwendet. Bei Interesse verfügt Catel über praktische Lokalisierungsmechanismen , auch auf WPF-Ebene. Wenn Sie Ressourcendateien nicht mögen, können Sie Ihre eigene Implementierung beispielsweise mit GNU gettext erstellen.


Aus Gründen der Lesbarkeit wurden in den Codebeispielen im Text dieser Veröffentlichung alle xml-doc-Kommentare entfernt.



Konfigurationsdienst


Ein Dienst, der über IoC eingebettet werden kann und von überall in der Anwendung auf Einstellungen zugreifen kann.


Das Hauptziel des Dienstes besteht darin, ein Einstellungsmodell bereitzustellen, das wiederum eine einfache und strukturierte Möglichkeit bietet, auf sie zuzugreifen.


Zusätzlich zum Einstellungsmodell bietet der Dienst auch die Möglichkeit, an den Einstellungen vorgenommene Änderungen abzubrechen oder zu speichern.


Schnittstelle:


 public interface IApplicationConfigurationProviderService { event TypedEventHandler<IApplicationConfigurationProviderService> ConfigurationSaved; ConfigurationModel Configuration { get; } void LoadSettingsFromStorage(); void SaveChanges(); } 

Implementierung:


 public partial class ApplicationConfigurationProviderService : IApplicationConfigurationProviderService { private readonly IConfigurationService _configurationService; public ApplicationConfigurationProviderService(IConfigurationService configurationService) { _configurationService = configurationService; Configuration = new ConfigurationModel(); LoadSettingsFromStorage(); ApplyMigrations(); } public event TypedEventHandler<IApplicationConfigurationProviderService> ConfigurationSaved; public ConfigurationModel Configuration { get; } public void LoadSettingsFromStorage() { Configuration.LoadFromStorage(_configurationService); } public void SaveChanges() { Configuration.SaveToStorage(_configurationService); ConfigurationSaved?.Invoke(this); } private void ApplyMigrations() { var currentVersion = typeof(ApplicationConfigurationProviderService).Assembly.GetName().Version; String currentVersionString = currentVersion.ToString(); String storedVersionString = _configurationService.GetRoamingValue("SolutionVersion", currentVersionString); if (storedVersionString == currentVersionString) return; //Either migrations were already applied or we are on fresh install var storedVersion = new Version(storedVersionString); foreach (var migration in _migrations) { Int32 comparison = migration.Version.CompareTo(storedVersion); if (comparison <= 0) continue; migration.Action.Invoke(); } _configurationService.SetRoamingValue("SolutionVersion", currentVersionString); } } 

Die Implementierung ist trivial. Der Inhalt des ConfigurationModel wird in den folgenden Abschnitten beschrieben. Das einzige, was wahrscheinlich Aufmerksamkeit erregt, ist die ApplyMigrations Methode.


In der neuen Version des Programms kann sich etwas ändern, z. B. die Methode zum Speichern eines komplexen Parameters oder dessen Name. Wenn wir unsere Einstellungen nicht nach jedem Update verlieren möchten, bei dem vorhandene Parameter geändert werden, benötigen wir einen Migrationsmechanismus. Die ApplyMigrations Methode ApplyMigrations sehr einfache Unterstützung für die Ausführung von Aktionen während des Übergangs zwischen Versionen.


Wenn sich in der neuen Version der Anwendung etwas geändert hat, fügen wir einfach die erforderlichen Aktionen (z. B. Speichern des Parameters unter einem neuen Namen) in der neuen Version zur Liste der in der benachbarten Datei enthaltenen Migrationen hinzu:


  private readonly IReadOnlyCollection<Migration> _migrations = new Migration[] { new Migration(new Version(1,1,0), () => { //... }) } .OrderBy(migration => migration.Version) .ToArray(); private class Migration { public readonly Version Version; public readonly Action Action; public Migration(Version version, Action action) { Version = version; Action = action; } } 

Einstellungsmodell


Die Automatisierung von Routineoperationen ist wie folgt. Die Konfiguration wird als reguläres Modell (Datenobjekt) beschrieben. Catel bietet eine praktische Basisklasse ModelBase , die den Kern aller MVVM-Tools bildet, z. B. automatische Bindungen zwischen allen drei MVVM-Komponenten. Insbesondere können Sie problemlos auf die Modelleigenschaften zugreifen, die wir speichern möchten.


Durch Deklarieren eines solchen Modells können wir seine Eigenschaften abrufen, Zeichenfolgenschlüssel zuordnen, sie aus Eigenschaftsnamen erstellen und dann automatisch Werte aus der Konfiguration laden und speichern. Mit anderen Worten, binden Sie Eigenschaften und Werte in einer Konfiguration.


Konfigurationsoptionen deklarieren


Dies ist das Wurzelmodell:


 public partial class ConfigurationModel : ConfigurationGroupBase { public ConfigurationModel() { Application = new ApplicationConfiguration(); Performance = new PerformanceConfiguration(); } public ApplicationConfiguration Application { get; private set; } public PerformanceConfiguration Performance { get; private set; } } 

ApplicationConfiguration und PerfomanceConfiguration sind Unterklassen, die ihre Einstellungsgruppen beschreiben:


 public partial class ConfigurationModel { public class PerformanceConfiguration : ConfigurationGroupBase { [DefaultValue(10)] public Int32 MaxUpdatesPerSecond { get; set; } } } 

Unter der Haube wird diese Eigenschaft an den Parameter "Performance.MaxUpdatesPerSecond" gebunden, dessen Name aus dem Namen des Typs PerformanceConfiguration generiert wird.


Es sollte beachtet werden, dass die Möglichkeit, diese Eigenschaften zu deklarieren, dank der Verwendung von Catel.Fody , einem Plug-In für den bekannten .NET-Codegenerator Fody , so präzise war . Wenn Sie es aus irgendeinem Grund nicht verwenden möchten, sollten die Eigenschaften gemäß der Dokumentation wie gewohnt deklariert werden (visuell ähnlich wie DependencyProperty von WPF).


Falls gewünscht, kann der Verschachtelungsgrad erhöht werden.


Implementieren Sie die Eigenschaftsbindung mit IConfigurationService


Die Bindung erfolgt in der Basisklasse ConfigurationGroupBase , die wiederum von ModelBase geerbt wird. Betrachten Sie den Inhalt genauer.


Zunächst erstellen wir eine Liste der Eigenschaften, die wir speichern möchten:


 public abstract class ConfigurationGroupBase : ModelBase { private readonly IReadOnlyCollection<ConfigurationProperty> _configurationProperties; private readonly IReadOnlyCollection<PropertyData> _nestedConfigurationGroups; protected ConfigurationGroupBase() { var properties = this.GetDependencyResolver() .Resolve<PropertyDataManager>() .GetCatelTypeInfo(GetType()) .GetCatelProperties() .Select(property => property.Value) .Where(property => property.IncludeInBackup && !property.IsModelBaseProperty) .ToArray(); _configurationProperties = properties .Where(property => !property.Type.IsSubclassOf(typeof(ConfigurationGroupBase))) .Select(property => { // ReSharper disable once PossibleNullReferenceException String configurationKeyBase = GetType() .FullName .Replace("+", ".") .Replace(typeof(ConfigurationModel).FullName + ".", string.Empty); configurationKeyBase = configurationKeyBase.Remove(configurationKeyBase.Length - "Configuration".Length); String configurationKey = $"{configurationKeyBase}.{property.Name}"; return new ConfigurationProperty(property, configurationKey); }) .ToArray(); _nestedConfigurationGroups = properties .Where(property => property.Type.IsSubclassOf(typeof(ConfigurationGroupBase))) .ToArray(); } ... private class ConfigurationProperty { public readonly PropertyData PropertyData; public readonly String ConfigurationKey; public ConfigurationProperty(PropertyData propertyData, String configurationKey) { PropertyData = propertyData; ConfigurationKey = configurationKey; } } } 

Hier wenden wir uns einfach dem Analogon der Reflexion für Catel-Modelle zu, erhalten Eigenschaften (durch Filtern von Dienstprogrammen oder solchen, die wir explizit mit dem Attribut [ExcludeFromBackup] markiert haben) und generieren Zeichenfolgenschlüssel für diese. Eigenschaften, die selbst vom Typ ConfigurationGroupBase , werden in einer separaten Liste aufgeführt.


Die LoadFromStorage() -Methode schreibt Werte aus der Konfiguration in die zuvor erhaltenen Eigenschaften oder in Standardwerte, wenn diese nicht zuvor gespeichert wurden. Für Untergruppen heißt LoadFromStorage() :


 public void LoadFromStorage(IConfigurationService configurationService) { foreach (var property in _configurationProperties) { try { LoadPropertyFromStorage(configurationService, property.ConfigurationKey, property.PropertyData); } catch (Exception ex) { Log.Error(ex, "Can't load from storage nested configuration group {Name}", property.PropertyData.Name); } } foreach (var property in _nestedConfigurationGroups) { var configurationGroup = GetValue(property) as ConfigurationGroupBase; if (configurationGroup == null) { Log.Error("Can't load from storage configuration property {Name}", property.Name); continue; } configurationGroup.LoadFromStorage(configurationService); } } protected virtual void LoadPropertyFromStorage(IConfigurationService configurationService, String configurationKey, PropertyData propertyData) { var objectConverterService = this.GetDependencyResolver().Resolve<IObjectConverterService>(); Object value = configurationService.GetRoamingValue(configurationKey, propertyData.GetDefaultValue()); if (value is String stringValue) value = objectConverterService.ConvertFromStringToObject(stringValue, propertyData.Type, CultureInfo.InvariantCulture); SetValue(propertyData, value); } 

Die LoadPropertyFromStorage Methode bestimmt, wie der Wert von der Konfiguration an die Eigenschaft übertragen wird. Es ist virtuell und kann für nicht triviale Eigenschaften neu definiert werden.


Eine kleine Funktion des internen Betriebs des IConfigurationService Dienstes: Sie können die Verwendung von IObjectConverterService . IConfigurationService.GetValue ist erforderlich, da IConfigurationService.GetValue in diesem Fall mit einem generischen Parameter vom Typ Object IConfigurationService.GetValue In diesem Fall werden die geladenen Zeichenfolgen beispielsweise nicht in Zahlen konvertiert. Sie müssen dies also selbst tun.


Ähnliches gilt für das Speichern von Parametern:


 public void SaveToStorage(IConfigurationService configurationService) { foreach (var property in _configurationProperties) { try { SavePropertyToStorage(configurationService, property.ConfigurationKey, property.PropertyData); } catch (Exception ex) { Log.Error(ex, "Can't save to storage configuration property {Name}", property.PropertyData.Name); } } foreach (var property in _nestedConfigurationGroups) { var configurationGroup = GetValue(property) as ConfigurationGroupBase; if (configurationGroup == null) { Log.Error("Can't save to storage nested configuration group {Name}", property.Name); continue; } configurationGroup.SaveToStorage(configurationService); } } protected virtual void SavePropertyToStorage(IConfigurationService configurationService, String configurationKey, PropertyData propertyData) { Object value = GetValue(propertyData); configurationService.SetRoamingValue(configurationKey, value); } 

Es ist zu beachten, dass Sie innerhalb des Konfigurationsmodells einfache Namenskonventionen befolgen müssen, um einheitliche Parameterzeichenfolgenschlüssel zu erhalten:


  • Die Arten von Einstellungsgruppen (mit Ausnahme des Stamms) sind Unterklassen der "übergeordneten" Gruppe, und ihre Namen enden mit "Konfiguration".
  • Für jeden solchen Typ gibt es eine entsprechende Eigenschaft. Zum Beispiel die ApplicationSettings Gruppe und die Application Eigenschaft. Der Name der Eigenschaft hat keine Auswirkungen, dies ist jedoch die logischste und erwartete Option.

Einstellung Einzelne Eigenschaften speichern


Die automatische IConfigurationService Catel.Fody und IConfigurationService (direktes Speichern des Werts in IConfigurationService und des Attributs [DefaultValue] ) funktioniert nur für einfache Typen und konstante Standardwerte. Für komplexe Eigenschaften müssen Sie etwas authentischer malen:


 public partial class ConfigurationModel { public class ApplicationConfiguration : ConfigurationGroupBase { public CultureInfo PreferredCulture { get; set; } [DefaultValue("User")] public String Username { get; set; } protected override void LoadPropertyFromStorage(IConfigurationService configurationService, String configurationKey, PropertyData propertyData) { switch (propertyData.Name) { case nameof(PreferredCulture): String preferredCultureDefaultValue = CultureInfo.CurrentUICulture.ToString(); if (preferredCultureDefaultValue != "en-US" || preferredCultureDefaultValue != "ru-RU") preferredCultureDefaultValue = "en-US"; String value = configurationService.GetRoamingValue(configurationKey, preferredCultureDefaultValue); SetValue(propertyData, new CultureInfo(value)); break; default: base.LoadPropertyFromStorage(configurationService, configurationKey, propertyData); break; } } protected override void SavePropertyToStorage(IConfigurationService configurationService, String configurationKey, PropertyData propertyData) { switch (propertyData.Name) { case nameof(PreferredCulture): Object value = GetValue(propertyData); configurationService.SetRoamingValue(configurationKey, value.ToString()); break; default: base.SavePropertyToStorage(configurationService, configurationKey, propertyData); break; } } } } 

Jetzt können wir zum Beispiel im Einstellungsfenster an eine der Modelleigenschaften binden:


 <TextBox Text="{Binding Configuration.Application.Username}" /> 

Es bleibt zu beachten, dass die Vorgänge beim Schließen des ViewModel-Einstellungsfensters überschrieben werden müssen:


 protected override Task<Boolean> SaveAsync() { _applicationConfigurationProviderService.SaveChanges(); return base.SaveAsync(); } protected override Task<Boolean> CancelAsync() { _applicationConfigurationProviderService.LoadSettingsFromStorage(); return base.CancelAsync(); } 

Mit der Erhöhung der Anzahl der Parameter und dementsprechend der Komplexität der Benutzeroberfläche können Sie auf einfache Weise separate Ansichten und ViewModel für jeden Einstellungsabschnitt erstellen.

Source: https://habr.com/ru/post/de460981/


All Articles