Часть 22.2 – Провайдер конфигурации Модели. Метаданные

Разработка интерфейса для источников данных обеспечила независимость от их типов. Кроме того, был создан механизм взаимодействия с ними. Теперь создадим реализацию провайдера метаданных модели как часть провайдера её конфигурации.

Создание провайдера метаданных Модели

Данный процесс уже достаточно знаком, поэтому не будем сильно вдаваться в его подробности. Однако разберемся что изменилось в данной реализации. Рассматривать код будем также по частям.

Разместим создаваемый класс ModelMetadataProviderAdapter в папке Models\Configuration. Но при этом он будет вложенный в ModelConfigurationProvider, что, как было отмечено, позволит ему обращаться к закрытым методам этого класса.

Конструктор и поля класса

/// Copyright (c) 2011 Andrey Veselov. All rights reserved.
/// WebSite: http://andrey.moveax.ru 
/// Email: andrey@moveax.ru
/// This source is subject to the Microsoft Public License (Ms-PL).

namespace MVCDemo.Models.Configuration
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Web.Mvc;
    using System.Xml.Linq;
    using MVCDemo.Models.Extensions;
    using MVCDemo.Models.MetadataProviders;

    public partial class ModelConfigurationProvider
    {
        private class ModelMetadataProviderAdapter : AssociatedMetadataProvider, IModelMetadataProvider
        {
            #region Available properties list

            private readonly string[] _availableProperties = 
            {
                // string
                "DataTypeName",
                "Description",
                "DisplayFormatString",
                "DisplayName",
                "EditFormatString",
                "NullDisplayText",
                "ShortDisplayName",
                "SimpleDisplayText",
                "TemplateHint",
                "Watermark",

                // bool
                "ConvertEmptyStringToNull",
                "HideSurroundingHtml",
                "IsReadOnly",
                "IsRequired",
                "ShowForDisplay",
                "ShowForEdit",

                // IDictionary<string, object>
                "AdditionalValues"
            };

            #endregion

            private readonly ModelConfigurationProvider _cfgProvider;

            public ModelMetadataProviderAdapter(ModelConfigurationProvider cfgProvider)
            {
                if (cfgProvider == null) {
                    throw new ArgumentNullException();
                }

                this._cfgProvider = cfgProvider;

                this.ValidateAvailablePropertiesList(); // called in debug mode only                
            }

Как и в прошлой реализации, присутствует список разрешенных для изменения полей ModelMetadata. Но в код остальной части изменен. Добавлено закрытое поле, содержащее ссылку на провайдера конфигурации Модели, в котором он расположен экземпляр данного класса. Соответственно, оно теперь задается как параметр конструктора класса, вместо пути до хранилища XML-файлов. Через это поле в дальнейшем можно будет обратиться к его закрытым методам.

Реализация IModelMetadataProvider

А вот реализация IModelMetadataProvider претерпела изменения. Теперь используется обращение к провайдеру конфигурации Модели, чтобы узнать возможность получения данных для указанного типа. Поскольку аналога данного метода у него нет, то просто запросим ссылку на экземпляр конфигурации для данного типа и сравним её с пустой ссылкой (null).

            #region IModelMetadataProvider Members

            public bool IsAbleToProvideMetadata(Type containerType)
            {
                return this._cfgProvider.GetModelConfiguraion(containerType) != null;
            }

            #endregion

Реализация AssociatedMetadataProvider

Здесь есть изменения, но не такие глобальные. Теперь также используется провайдер конфигурации Модели для получения данных о ней. Разумеется, убедимся что Модель способна их представить и выбросим исключение, если это не так. Кроме того, во вспомогательные методы будем передавать экземпляр типа ресурса, используемого по умолчанию.

            #region AssociatedMetadataProvider method override

            protected override ModelMetadata CreateMetadata(
                IEnumerable<System.Attribute> attributes,
                Type containerType,
                Func<object> modelAccessor,
                Type modelType,
                string propertyName)
            {
                var metadata = new ModelMetadata(
                    this, containerType, modelAccessor, modelType, propertyName);

                if (string.IsNullOrEmpty(propertyName)) {
                    return metadata;
                }

                var cfgData = this._cfgProvider.GetModelConfiguraion(containerType);
                if (cfgData == null) {
                    throw new ArgumentException(
                        string.Format("Unknown type: {0}", containerType.FullName));
                }

                // Get the data for the specified property.
                var xmlPropertyElement = cfgData.Source.Elements("property").FirstOrDefault(
                    element => string.Equals(element.Attribute("name").Value, propertyName));

                if (xmlPropertyElement == null) {
                    return metadata;
                }

                // Set the metadata for the property using XML attributes.
                foreach (var element in xmlPropertyElement.Elements()) {
                    if (!string.Equals(element.Name.LocalName, "metadata")) {
                        continue;
                    }

                    string xmlPropertyName = this.GetAndValidatePropertyName(element);

                    if (string.Equals(xmlPropertyName, "AdditionalValues")) {
                        this.SetMetadataAdditionalValue(
                            metadata, element, cfgData.DefaultResourceType);
                    }
                    else {
                        var metadataProperty = typeof(ModelMetadata).GetProperty(xmlPropertyName);
                        object value = Convert.ChangeType(
                            this.GetValueFromElement(element, cfgData.DefaultResourceType),
                            metadataProperty.PropertyType);

                        metadataProperty.SetValue(metadata, value, null);
                    }
                }

                return metadata;
            }

            #endregion

Логика данного метода не изменилась. После получения экземпляра данных для текущей Модели, происходит перебор его элементов <propetry>. Для каждого свойства выбираются элементы <metadata> и их данные переписываются в метаданные Модели. При этом, ввиду своего типа, отдельная обработка реализована для его свойства AdditionalValues, т.к. это коллекция.

Вспомогательные методы

Рассмотрим методы, расположены в блоке вспомогательного кода. Они используются в рассмотренном выше CreateMetadata() и предназначены, большей частью, для улучшения его читаемости.

Первый из них GetAndValidatePropertyName(). Он считывает имя свойства класса ModelMetatdata и производит проверку, которая заключается в поиске его в списке разрешенных для изменения свойств. При обнаружении ошибки выбрасывается исключение.

            #region Private methods

            private string GetAndValidatePropertyName(XElement element)
            {
                string propertyName = element.GetValueOrNull("name");
                if (propertyName == null) {
                    throw new InvalidDataException(
                        string.Format("The name was not set. Element: {0}.", element));
                }

                // Validate the property name.
                var nameFound = this._availableProperties.
                    FirstOrDefault(p => string.Equals(p, propertyName));

                if (nameFound == null) {
                    throw new InvalidDataException(
                        string.Format("Unknown or forbidden property: {0}.", propertyName));
                }

                return propertyName;
            }

Следующий метод SetMetadataAdditionalValue() реализует логику записи значения в коллекцию AdditionalValues. В данном случае, сначала необходимо получить текущий экземпляр данной коллекции, и уже затем добавлять в него пару "ключ-значение". При этом метод получил дополнительный параметр defaultResourceType для указания типа ресурса по умолчанию.

            private void SetMetadataAdditionalValue(
                ModelMetadata metadata, XElement element, Type defaultResourceType)
            {
                string key = element.GetValueOrNull("key");
                if (key == null) {
                    throw new InvalidDataException(
                        string.Format("The key was not set. Element: {0}.", element));
                }

                // Set the value's type from XML element (or use default System.String).
                string typeName = element.GetValueOrNull("type");
                if (typeName == null) {
                    typeName = "System.String";
                }

                // Get AdditionalValues property and add the new value.
                var addValProperty = typeof(ModelMetadata).GetProperty("AdditionalValues");
                var addVal = (IDictionary<string, object>)addValProperty.GetValue(metadata, null);
                object value = Convert.ChangeType(
                    this.GetValueFromElement(element, defaultResourceType),
                    Type.GetType(typeName));

                addVal.Add(key, value);
            }

И, наконец, последний вспомогательный метод GetValueFromElement(). Его задача прочитать значение, которые будет присвоено свойству экземпляра ModelMetadata. Оно может быть задано как в виде текста, так и в виде указания на ресурсы веб-приложения. В первом случае используется атрибут value, а во втором - resname, определяющих имя ресурса. При этом возможно переопределить ресурса используя опциональный атрибут resource.

            private string GetValueFromElement(XElement element, Type defaultResourceType)
            {
                // Try to get the value.
                string value = element.GetValueOrNull("value");
                if (value != null) {
                    return value;
                }

                // Try to get the value from the resource.
                string resourceName = element.GetValueOrNull("resname");
                if (resourceName == null) {
                    throw new InvalidDataException(
                        string.Format("The value was not set. Element: {0}.", element));
                }

                string resourceTypeName = element.GetValueOrNull("resource");
                Type resourceType = defaultResourceType;

                if (resourceTypeName != null) {
                    resourceType = Type.GetType(resourceTypeName, throwOnError: true);
                }

                var resourceProperty = resourceType.GetProperty(
                    resourceName,
                    BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);

                return (string)resourceProperty.GetValue(null, null);
            }

            #endregion

Отладочные методы

Метод ValidateAvailablePropertiesList() предназначен для контроля списка доступных для изменения имен свойств текущего экземпляра ModelMetadata. В случае нахождения в нем имени, не определенного в указанном классе, будет выброшено исключение. Сам метод отмечен как отладочный и запускается только в соответствующем режиме Debug.

            #region Debug methods

            [Conditional("DEBUG")]
            private void ValidateAvailablePropertiesList()
            {
                PropertyInfo[] props = typeof(ModelMetadata).GetProperties();

                foreach (var propertyName in this._availableProperties) {
                    if (props.FirstOrDefault(p => string.Equals(p.Name, propertyName)) == null) {
                        throw new InvalidDataException(
                            string.Format("Unknown ModelMetadata property: {0}.", propertyName));
                    }
                }
            }

            #endregion
        }
    }
}

Разработка провайдера метаданных завершена. Можно скомпилировать проект чтобы убедиться что ошибки в нем отсутствуют. В следующей части создадим провайдер правил проверки данных и необходимые для его дополнительные классы. >>

Pingbacks and trackbacks (3)+

Добавить комментарий