Implementasi MVVM dari konfigurasi aplikasi WPF dibangun berdasarkan kerangka kerja Catel

Menerapkan pengaturan pengaturan perangkat lunak mungkin merupakan salah satu hal yang diterapkan secara berbeda di hampir setiap aplikasi. Sebagian besar kerangka kerja dan tambahan lainnya biasanya menyediakan alat mereka sendiri untuk menyimpan / memuat nilai dari beberapa nilai kunci penyimpanan parameter.


Namun, dalam kebanyakan kasus, penerapan jendela pengaturan khusus dan banyak hal terkait diserahkan kepada kebijaksanaan pengguna. Dalam artikel ini saya ingin membagikan pendekatan yang berhasil saya lakukan. Dalam kasus saya, saya perlu mengimplementasikan pekerjaan dengan pengaturan dalam gaya yang ramah-MVVM dan menggunakan spesifikasi kerangka kerja Catel yang digunakan dalam kasus ini.


Penafian : dalam catatan ini tidak akan ada kesulitan teknis yang lebih sulit daripada refleksi dasar. Ini hanya deskripsi dari pendekatan untuk memecahkan masalah kecil yang saya dapatkan selama akhir pekan. Saya ingin berpikir tentang cara menyingkirkan kode boilerplate standar dan salin-rekat yang terkait dengan pengaturan aplikasi penyimpanan / pemuatan. Solusi itu sendiri ternyata agak sepele berkat alat .NET / Catel yang mudah digunakan, tetapi mungkin seseorang akan menghemat beberapa jam waktu atau menyarankan pemikiran yang berguna.


Ikhtisar Kerangka Catel

Seperti kerangka kerja WPF lainnya (Prism, MVVM Light, Caliburn.Micro, dll.), Catel menyediakan alat yang mudah digunakan untuk membangun aplikasi dalam gaya MVVM.
Komponen utama:


  • IoC (terintegrasi dengan komponen MVVM)
  • ModelBase: kelas dasar yang menyediakan implementasi otomatis PropertyChanged (terutama dalam hubungannya dengan Catel.Fody), serialisasi, dan BeginEdit / CancelEdit / EndEdit (klasik "berlaku" / "batal").
  • ViewModelBase, dapat mengikat ke model, membungkus propertinya.
  • Bekerja dengan tampilan, yang dapat secara otomatis membuat dan mengikat ke ViewModel. Kontrol bersarang didukung.

Persyaratan


Kami akan melanjutkan dari fakta bahwa kami menginginkan yang berikut dari alat konfigurasi:


  • Akses ke konfigurasi dengan cara terstruktur yang sederhana. Sebagai contoh
    CultureInfo culture = settings.Application.PreferredCulture;
    TimeSpan updateRate = settings.Perfomance.UpdateRate; .
    • Semua parameter disajikan sebagai properti normal. Metode penyimpanan diringkas di dalam. Untuk tipe sederhana, semuanya harus terjadi secara otomatis, untuk tipe yang lebih kompleks, harus dimungkinkan untuk mengonfigurasi serialisasi nilai ke dalam string.
  • Kesederhanaan dan keandalan. Saya tidak ingin menggunakan alat yang rapuh seperti membuat serialisasi seluruh model konfigurasi seluruhnya atau beberapa Kerangka Entitas. Pada level yang lebih rendah, konfigurasi tetap merupakan repositori sederhana dari pasangan parameter-nilai.
  • Kemampuan untuk membuang perubahan yang dilakukan pada konfigurasi, misalnya, jika pengguna mengklik "batal" di jendela pengaturan.
  • Kemampuan untuk berlangganan pembaruan konfigurasi. Misalnya, kami ingin memperbarui bahasa aplikasi segera setelah konfigurasi diubah.
  • Migrasi antar versi aplikasi. Seharusnya dimungkinkan untuk mengatur tindakan saat beralih di antara versi aplikasi (ganti nama parameter, dll.).
  • Kode boilerplate minimum, kesalahan ketik minimum. Idealnya, kami hanya ingin mengatur properti-otomatis dan tidak memikirkan bagaimana itu akan disimpan, di bawah kunci string mana, dll ... Kami tidak ingin menyalin secara manual setiap properti dalam model tampilan dari jendela pengaturan, semuanya harus bekerja secara otomatis.

Alat Standar


Catel menyediakan layanan IConfigurationService, yang memungkinkan penyimpanan dan pemuatan nilai kunci string dari penyimpanan lokal (file pada disk dalam implementasi standar).


Jika kita ingin menggunakan layanan ini dalam bentuknya yang murni, kita harus mendeklarasikan kunci-kunci ini sendiri, misalnya, dengan menetapkan konstanta seperti itu:


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

Maka kita bisa mendapatkan parameter ini seperti ini:


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

Banyak dan membosankan untuk menulis, mudah untuk membuat kesalahan ketik ketika ada banyak pengaturan. Selain itu, layanan hanya mendukung jenis sederhana, misalnya CultureInfo tidak dapat disimpan tanpa transformasi tambahan.


Untuk mempermudah pekerjaan dengan layanan ini, pembungkus yang terdiri dari beberapa komponen diperoleh.


Kode sampel lengkap tersedia di repositori GitHub . Ini berisi aplikasi paling sederhana dengan kemampuan untuk mengedit beberapa parameter dalam pengaturan dan memastikan semuanya bekerja. Saya tidak repot dengan pelokalan, parameter "Bahasa" dalam pengaturan digunakan hanya untuk menunjukkan konfigurasi. Jika tertarik, Catel memiliki mekanisme lokalisasi yang nyaman, termasuk di tingkat WPF. Jika Anda tidak suka file sumber daya, Anda dapat membuat implementasi sendiri bekerja dengan GNU gettext, misalnya.


Agar mudah dibaca, dalam contoh kode dalam teks publikasi ini, semua komentar xml-doc telah dihapus.



Layanan konfigurasi


Layanan yang dapat disematkan melalui IoC dan memiliki akses untuk bekerja dengan pengaturan dari mana saja di aplikasi.


Tujuan utama dari layanan ini adalah untuk menyediakan model pengaturan, yang pada gilirannya menyediakan cara yang sederhana dan terstruktur untuk mengaksesnya.


Selain model pengaturan, layanan ini juga menyediakan kemampuan untuk membatalkan atau menyimpan perubahan yang dilakukan pada pengaturan.


Antarmuka:


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

Implementasi:


 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); } } 

Implementasinya sepele, isi ConfigurationModel dijelaskan di bagian berikut. Satu-satunya hal yang mungkin menarik perhatian adalah metode ApplyMigrations .


Dalam versi baru program, sesuatu dapat berubah, misalnya, metode penyimpanan beberapa parameter kompleks atau namanya. Jika kami tidak ingin kehilangan pengaturan setelah setiap pembaruan yang mengubah parameter yang ada, kami memerlukan mekanisme migrasi. Metode ApplyMigrations dukungan yang sangat sederhana untuk melakukan tindakan apa pun selama transisi antar versi.


Jika ada sesuatu yang berubah di versi baru aplikasi, kami cukup menambahkan tindakan yang diperlukan (misalnya, menyimpan parameter dengan nama baru) di versi baru ke daftar migrasi yang terdapat dalam file tetangga:


  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; } } 

Model Pengaturan


Otomasi operasi rutin adalah sebagai berikut. Konfigurasi digambarkan sebagai model reguler (data-objek). Catel menyediakan ModelBase kelas dasar yang nyaman, yang merupakan inti dari semua alat MVVM-nya, seperti binding otomatis antara ketiga komponen MVVM. Secara khusus, ini memungkinkan Anda untuk dengan mudah mengakses properti model yang ingin kami simpan.


Dengan mendeklarasikan model seperti itu, kita dapat memperoleh propertinya, memetakan kunci-kunci string kepada mereka, membuatnya dari nama properti, dan kemudian secara otomatis memuat dan menyimpan nilai dari konfigurasi. Dengan kata lain, ikat properti dan nilai dalam konfigurasi.


Mendeklarasikan opsi konfigurasi


Ini adalah model root:


 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 dan PerfomanceConfiguration adalah subclass yang menjelaskan grup pengaturan mereka:


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

Di bawah tenda, properti ini akan mengikat ke parameter "Performance.MaxUpdatesPerSecond" , yang namanya dihasilkan dari nama tipe PerformanceConfiguration .


Perlu dicatat bahwa kemampuan untuk mendeklarasikan properti ini sangat ringkas berkat penggunaan Catel.Fody , sebuah plug-in ke generator kode .NET Fody yang terkenal . Jika karena alasan tertentu Anda tidak ingin menggunakannya, properti harus dideklarasikan seperti biasa, sesuai dengan dokumentasi (mirip secara visual dengan DependencyProperty dari WPF).


Jika diinginkan, tingkat bersarang dapat ditingkatkan.


Terapkan Properti Binding dengan IConfigurationService


Binding terjadi di kelas dasar ConfigurationGroupBase , yang pada gilirannya diwarisi dari ModelBase. Pertimbangkan isinya secara lebih rinci.


Pertama-tama, kami membuat daftar properti yang ingin kami simpan:


 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; } } } 

Di sini kita cukup beralih ke analog refleksi untuk model Catel, dapatkan properti (dengan memfilter utilitas atau yang kami tandai secara eksplisit dengan atribut [ExcludeFromBackup] ) dan buat kunci string [ExcludeFromBackup] . Properti yang bertipe ConfigurationGroupBase dicantumkan dalam daftar terpisah.


Metode LoadFromStorage() menulis nilai dari konfigurasi ke properti yang diperoleh sebelumnya, atau nilai standar, jika sebelumnya tidak disimpan. Untuk subkelompok, LoadFromStorage() disebut:


 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); } 

Metode LoadPropertyFromStorage menentukan bagaimana nilai ditransfer dari konfigurasi ke properti. Ini virtual dan dapat didefinisikan ulang untuk properti non-sepele.


Sebuah fitur kecil dari operasi internal layanan IConfigurationService : Anda dapat melihat penggunaan IObjectConverterService . Ini diperlukan karena IConfigurationService.GetValue dalam kasus ini dengan parameter generik dari tipe Object dan dalam kasus ini ia tidak akan mengonversi string yang dimuat ke angka, misalnya, jadi Anda perlu melakukan ini sendiri.


Demikian pula dengan parameter penyimpanan:


 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); } 

Perlu dicatat bahwa di dalam model konfigurasi, Anda harus mengikuti konvensi penamaan sederhana untuk mendapatkan kunci string parameter yang seragam:


  • Jenis-jenis grup pengaturan (kecuali root) adalah subkelas dari grup "induk" dan namanya berakhir di Konfigurasi.
  • Untuk setiap jenis seperti itu ada properti yang sesuai dengannya. Misalnya, grup ApplicationSettings dan properti Application . Nama properti tidak memengaruhi apa pun, tetapi ini adalah opsi yang paling logis dan diharapkan.

Pengaturan simpan properti individual


IConfigurationService otomatis Catel.Fody dan IConfigurationService (penghematan langsung nilai dalam IConfigurationService dan atribut [DefaultValue] ) hanya akan berfungsi untuk tipe sederhana dan nilai default konstan. Untuk properti kompleks, Anda harus melukis sedikit lebih otentik:


 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; } } } } 

Sekarang kita dapat, misalnya, di jendela pengaturan mengikat ke salah satu properti model:


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

Masih harus diingat untuk mengganti operasi saat menutup jendela pengaturan ViewModel:


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

Dengan peningkatan jumlah parameter dan, karenanya, kompleksitas antarmuka, Anda dapat dengan mudah membuat View dan ViewModel terpisah untuk setiap bagian pengaturan.

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


All Articles