تطبيق MVVM لتكوين تطبيق WPF مبني على أساس إطار Catel

قد يكون تطبيق إدارة إعدادات البرامج أحد الأشياء التي يتم تنفيذها بشكل مختلف في كل تطبيق تقريبًا. توفر معظم الأُطُر والوظائف الإضافية الأخرى عادةً أدواتها الخاصة لحفظ / تحميل القيم من بعض القيم الرئيسية لتخزين المعلمات.


ومع ذلك ، في معظم الحالات ، يتم ترك تطبيق نافذة إعدادات محددة والعديد من الأشياء ذات الصلة لتقدير المستخدم. في هذه المقالة أريد أن أشارك في النهج الذي تمكنت من التوصل إليه. في حالتي ، أحتاج إلى تنفيذ العمل باستخدام الإعدادات بأسلوب MVVM الصديق واستخدام تفاصيل إطار عمل Catel المستخدم في هذه الحالة.


إخلاء المسئولية : في هذه الملاحظة ، لن يكون هناك أي تفاصيل فنية أكثر صعوبة من التفكير الأساسي. هذا مجرد وصف لأسلوب حل مشكلة صغيرة حصلت عليها في نهاية الأسبوع. كنت أرغب في التفكير في كيفية التخلص من شفرة المعيار القياسية ولصق النسخ المتعلقة بحفظ / تنزيل إعدادات التطبيق. تبين أن الحل نفسه تافه إلى حد ما بفضل أدوات .NET / Catel المريحة المتاحة ، ولكن ربما سيوفر شخص ما بضع ساعات من الوقت أو يقترح أفكارًا مفيدة.


ملخص إطار Catel

مثل إطارات WPF الأخرى (Prism ، MVVM Light ، Caliburn.Micro ، وما إلى ذلك) ، توفر Catel أدوات ملائمة لبناء التطبيقات بأسلوب MVVM.
المكونات الرئيسية:


  • IoC (متكامل مع مكونات MVVM)
  • ModelBase: فئة أساسية توفر تطبيقًا تلقائيًا لـ PropertyChanged (خاصةً بالاشتراك مع Catel.Fody) والتسلسل و BeginEdit / CancelEdit / EndEdit (الكلاسيكية "تطبيق" / "إلغاء").
  • ViewModelBase ، قادرة على ربط النموذج ، التفاف خصائصه.
  • اعمل باستخدام طرق العرض ، والتي يمكنها تلقائيًا إنشاء ViewModel والربط به. عناصر التحكم المتداخلة مدعومة.

متطلبات


سننتقل من حقيقة أننا نريد ما يلي من أدوات التكوين:


  • الوصول إلى التكوين بطريقة منظمة بسيطة. على سبيل المثال
    CultureInfo culture = settings.Application.PreferredCulture;
    TimeSpan updateRate = settings.Perfomance.UpdateRate; .
    • يتم تقديم جميع المعلمات كخصائص طبيعية. يتم تغليف طريقة التخزين بالداخل. بالنسبة للأنواع البسيطة ، يجب أن يحدث كل شيء تلقائيًا ؛ وبالنسبة للأنواع الأكثر تعقيدًا ، يجب أن يكون من الممكن تكوين تسلسل القيمة في سلسلة.
  • البساطة والموثوقية. لا أريد استخدام أدوات هشة مثل إجراء تسلسل لنموذج التكوين بالكامل أو بعض Entity Framework. في المستوى الأدنى ، يظل التكوين مستودعًا بسيطًا لأزواج قيمة المعلمات.
  • القدرة على تجاهل التغييرات التي أجريت على التكوين ، على سبيل المثال ، إذا نقر المستخدم على "إلغاء" في نافذة الإعدادات.
  • القدرة على الاشتراك في تحديثات التكوين. على سبيل المثال ، نريد تحديث لغة التطبيق فور تغيير التكوين.
  • الترحيل بين إصدارات التطبيق. يجب أن يكون من الممكن ضبط الإجراءات عند التبديل بين إصدارات التطبيق (إعادة تسمية المعلمات ، وما إلى ذلك).
  • الحد الأدنى من رمز النمذجة ، الأخطاء المطبعية الدنيا. من الناحية المثالية ، نريد فقط تعيين خاصية السيارات وعدم التفكير في كيفية حفظها ، وبموجب مفتاح السلسلة ، وما إلى ذلك ... لا نريد نسخ كل من الخصائص يدويًا في نموذج عرض نافذة الإعدادات ، يجب أن يعمل كل شيء تلقائيًا.

الأدوات القياسية


توفر Catel خدمة IConfigurationService ، والتي تسمح بتخزين وتحميل قيم مفاتيح السلسلة من التخزين المحلي (ملف موجود على القرص في التطبيق القياسي).


إذا كنا نرغب في استخدام هذه الخدمة في شكلها الخالص ، فسيتعين علينا أن نعلن عن هذه المفاتيح بأنفسنا ، على سبيل المثال ، عن طريق تعيين مثل هذه الثوابت:


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

ثم يمكننا الحصول على هذه المعلمات مثل هذا:


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

من الممتع والكثير الكتابة ، فمن السهل عمل أخطاء مطبعية عندما يكون هناك الكثير من الإعدادات. بالإضافة إلى ذلك ، تدعم الخدمة أنواعًا بسيطة فقط ، على سبيل المثال لا يمكن حفظ CultureInfo دون تحويلات إضافية.


لتبسيط العمل مع هذه الخدمة ، تم الحصول على غلاف يتكون من عدة مكونات.


يتوفر نموذج التعليمات البرمجية الكامل في مستودع GitHub . أنه يحتوي على أبسط تطبيق مع القدرة على تحرير اثنين من المعلمات في الإعدادات والتأكد من أن كل شيء يعمل. لم أكن مهتمًا بالترجمة ، يتم استخدام المعلمة "اللغة" في الإعدادات فقط لإظهار التكوين. إذا كانت مهتمة ، لدى Catel آليات توطين ملائمة ، بما في ذلك على مستوى WPF. إذا كنت لا تحب ملفات الموارد ، فيمكنك جعل تطبيقك يعمل مع gettext ، على سبيل المثال.


لسهولة القراءة ، في أمثلة التعليمات البرمجية في نص هذا المنشور ، تمت إزالة جميع تعليقات مستند XML.



خدمة التكوين


خدمة يمكن تضمينها من خلال IoC وتتمتع بإمكانية الوصول إلى العمل مع الإعدادات من أي مكان في التطبيق.


الهدف الرئيسي من الخدمة هو توفير نموذج إعدادات ، والذي بدوره يوفر طريقة بسيطة ومنظمة للوصول إليها.


بالإضافة إلى طراز الإعدادات ، توفر الخدمة أيضًا القدرة على التراجع عن أو حفظ التغييرات التي تم إجراؤها على الإعدادات.


التفاعل:


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

التنفيذ:


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

تطبيق تافهة ، ConfigurationModel وصف محتويات ConfigurationModel في الأقسام التالية. الشيء الوحيد المحتمل لجذب الانتباه هو أسلوب ApplyMigrations .


في الإصدار الجديد من البرنامج ، قد يتغير شيء ما ، على سبيل المثال ، طريقة تخزين بعض المعلمات المعقدة أو اسمه. إذا كنا لا نريد أن نفقد إعداداتنا بعد كل تحديث يغير المعلمات الحالية ، فنحن بحاجة إلى آلية ترحيل. ApplyMigrations طريقة ApplyMigrations دعمًا بسيطًا للغاية لأداء أي إجراءات أثناء الانتقال بين الإصدارات.


إذا تغير شيء ما في الإصدار الجديد من التطبيق ، فنحن ببساطة نضيف الإجراءات اللازمة (على سبيل المثال ، حفظ المعلمة باسم جديد) في الإصدار الجديد إلى قائمة عمليات الترحيل الموجودة في الملف المجاور:


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

نموذج الإعدادات


أتمتة العمليات الروتينية كما يلي. يوصف التكوين كنموذج منتظم (كائن البيانات). توفر Catel ModelBase الفئة الأساسية ModelBase ، والذي يمثل جوهر جميع أدوات MVVM ، مثل الروابط التلقائية بين مكونات MVVM الثلاثة. على وجه الخصوص ، يتيح لك الوصول بسهولة إلى خصائص الطراز التي نريد حفظها.


بإعلان مثل هذا النموذج ، يمكننا الحصول على خصائصه وتعيين مفاتيح سلسلة لها ، وإنشائها من أسماء الممتلكات ، ثم تحميل القيم وحفظها تلقائيًا من التكوين. بمعنى آخر ، قم بربط الخصائص والقيم في التكوين.


إعلان خيارات التكوين


هذا هو نموذج الجذر:


 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 و PerfomanceConfiguration هي فئات فرعية تصف مجموعات الإعدادات الخاصة بها:


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

ضمن الغطاء ، ستربط هذه الخاصية المعلمة "Performance.MaxUpdatesPerSecond" ، التي يتم إنشاء اسمها من اسم النوع PerformanceConfiguration .


تجدر الإشارة إلى أن القدرة على الإعلان عن هذه الخصائص كانت موجزة للغاية بفضل استخدام Catel.Fody ، وهو مكون إضافي لمولد رمز .NET الشهير Fody . إذا كنت لا ترغب في استخدامه لسبب ما ، فيجب الإعلان عن الخصائص كالمعتاد ، وفقًا للوثائق (تشبه بصريًا DependencyProperty من WPF).


إذا رغبت في ذلك ، يمكن زيادة مستوى التعشيش.


تطبيق ربط الممتلكات مع IConfigurationService


يحدث الربط في ConfigurationGroupBase الفئة الأساسية ، والتي بدورها موروثة من ModelBase. النظر في محتوياته بمزيد من التفاصيل.


أولاً وقبل كل شيء ، نقوم بعمل قائمة من الخصائص التي نريد حفظها:


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

هنا ، ننتقل ببساطة إلى تناظرية الانعكاس لنماذج Catel ، ونحصل على الخصائص (عن طريق تصفية الأداة المساعدة أو تلك التي وضعناها بشكل صريح مع سمة [ExcludeFromBackup] ) وإنشاء مفاتيح سلسلة لها. ConfigurationGroupBase سرد الخصائص التي هي من النوع ConfigurationGroupBase في قائمة منفصلة.


يكتب الأسلوب LoadFromStorage() القيم من التكوين إلى الخصائص التي تم الحصول عليها مسبقًا ، أو القيم القياسية ، إذا لم يتم حفظها مسبقًا. بالنسبة للمجموعات الفرعية ، 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); } 

تحدد طريقة LoadPropertyFromStorage كيفية نقل القيمة من التكوين إلى الخاصية. إنه افتراضي ويمكن إعادة تعريفه لخصائص غير تافهة.


ميزة صغيرة من التشغيل الداخلي لخدمة IConfigurationService : يمكنك ملاحظة استخدام IObjectConverterService . هناك حاجة لأن IConfigurationService.GetValue في هذه الحالة تسمى بمعلمة عامة من النوع Object ولن تقوم في هذه الحالة بتحويل السلاسل المحملة إلى أرقام بحد ذاتها ، على سبيل المثال ، لذلك عليك القيام بذلك بنفسك.


وبالمثل مع توفير المعلمات:


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

تجدر الإشارة إلى أنه داخل نموذج التكوين ، تحتاج إلى اتباع اصطلاحات تسمية بسيطة للحصول على مفاتيح سلسلة معلمة موحدة:


  • أنواع مجموعات الإعدادات (باستثناء الجذر) هي فئات فرعية من المجموعة "الأصل" وتنتهي أسمائها في التكوين.
  • لكل نوع من هذا النوع هناك خاصية المقابلة لذلك. على سبيل المثال ، مجموعة Application وخاصية Application . لا يؤثر اسم الخاصية على أي شيء ، ولكن هذا هو الخيار الأكثر منطقية والمتوقع.

إعداد حفظ الخصائص الفردية


يعمل التصميم التلقائي IConfigurationService Catel.Fody و IConfigurationService (الحفظ المباشر للقيمة في IConfigurationService والخاصية [DefaultValue] ) فقط للأنواع البسيطة والقيم الافتراضية الثابتة. بالنسبة للخصائص المعقدة ، عليك أن ترسم أصالة أكثر بقليل:


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

يمكننا الآن ، على سبيل المثال ، في نافذة الإعدادات ربط أي من خصائص النموذج:


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

يبقى أن نتذكر تجاوز العمليات عند إغلاق نافذة إعدادات ViewModel:


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

مع زيادة عدد المعلمات ، وبالتالي ، تعقيد الواجهة ، يمكنك بسهولة إنشاء View و ViewModel منفصلين لكل قسم إعدادات.

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


All Articles