Часть 20 – Провайдер метаданных на основе XML конфигурации

После разработки менеджера провайдеров метаданных в прошлой части, появилась возможность использовать несколько их экземпляров. Используем её для указания метаданных Модели с помощью информации из XML файла.

Язык описания метаданных

Перед началом разработки давайте определимся с некоторыми её принципами:

  • информация, которая будет записана в метаданные Модели, представлена в виде XML файлов;
  • в одном файле размещаются данные только для одного типа;
  • имя этого файла должно совпадать с полным именем типа, да которого он предназначен;
  • все строковые данные, используемые для отображения в пользовательском интерфейсе, будут расположены в ресурсах веб-приложения.

Чтобы было проще понять, как будет выглядеть конфигурация, посмотрим на её фрагмент:

<?xml version="1.0" encoding="utf-8" ?>
<model resource="MVCDemo.Resources.Models.PaymentRes">

  <property name="UserLogin">
    <metadata name="DisplayName" resname="UserLogin" />
    <metadata name="IsRequired" value="true" />
    <metadata name="AdditionalValues" resname="UserLogin" key="Hint" />
  </property>

  .........
</model>

Уже сейчас можно догадаться о предназначении используемых элементов. Вот их полный список:

  • model – корневой элемент, содержит описания для всех свойств модели.
    • resource – атрибут, определяет имя типа ресурсов по умолчанию;
  • property – элемент, указывающий для какого свойства Модели предназначены метаданные;
  • metadata – элемент, содержащий информацию для указанного выше свойства ModelMetadata:
    • name – имя свойства;
    • value – значение;
    • resname – имя ресурса с текстом значения;
    • (опционально) resource – переопределяет имя типа ресурсов для данного элемента;
    • key – ключ (только для добавления значений в коллекцию AdditionalValues);
    • (опционально) type –  тип значения (только для коллекции AdditionalValues).

Схема описания метаданных определена.

Небольшое дополнение

Немного забежим вперед и создадим небольшой вспомогательный метод, который сделает создаваемый код более понятным и компактным. GetValueOrNull() предназначен для получения значения атрибута указанного XML элемента или null, если он не существует.

В папке Models создадим папку Extensions, в которой разместим небольшой класс XElementExenstions:

namespace MVCDemo.Models.Extensions
{
    using System.Xml.Linq;

    public static class XElementExenstions
    {
        public static string GetValueOrNull(this XElement element, string name)
        {
            var xmlAttr = element.Attribute(name);
            return (xmlAttr == null) ? null : xmlAttr.Value;
        }
    }
}

Провайдер метаданных на основе XML конфигурации

Создадим класс XmlModelMetadataProvider, который реализует интерфейс IModelMetadataProvider и является наследником AssociatedMetadataProvider. Поскольку объем кода получится достаточно большой, то будем рассматривать его по частям.

Поля и конструктор класса

При заполнении метаданных Модели нужно учесть два следующих момента. Во-первых, необходимо проверять наличие указанного в XML файле свойства. Во-вторых, будет неосмотрительно разрешить изменять значения любых свойств ModelMetadata (например, ContainerType или Properties). Кроме того, часть из них доступна только для чтения. Поэтому создадим массив _availableProperties. В нем перечислим имена существующих и разрешенных для изменения свойств метаданных Модели.

Кроме того, создадим следующие поля:

  • _xmlMetadata – класс XElement, содержащий данные из XML файла;
  • _currentModelTypeName – имя типа, для которого они предназначены;
  • _defaultResourceType – тип ресурса, заданный в <model> и используемый по умолчанию.

В качестве параметра конструктора будем требовать указание места хранения XML файлов. Проверим существование переданного пути перед его сохранением в поле _metadataStorage, при необходимости завершив его символом "\". Затем вызовем метод ValidateAvailablePropertiesList() для проверки значений списка _availableProperties. Подробнее его рассмотрим чуть позже.

namespace MVCDemo.Models.MetadataProviders
{
    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 Models.Extensions;

    public class XmlModelMetadataProvider : AssociatedMetadataProvider, IModelMetadataProvider
    {
        #region Private fields

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

        private readonly string _metadataStorage;

        private string _currentModelTypeName = string.Empty;

        private XElement _xmlMetadata;

        private Type _defaultResourceType;

        #endregion

        public XmlModelMetadataProvider(string metadataStorage)
        {
            if (!Directory.Exists(metadataStorage)) {
                throw new ArgumentException(
                    string.Format("Invalid directory {0}", metadataStorage));
            }

            if (metadataStorage.EndsWith("\\")) {
                this._metadataStorage = metadataStorage;
            }
            else {
                this._metadataStorage = string.Format("{0}\\", metadataStorage);
            }

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

IsAbleToProvideMetadata()

Перейдем к разработке метода IsAbleToProvideMetadata(). В нем определим поддержку создания метаданных для указанного типа. Для этого проверим наличие файла, имя которого совпадает с его полным именем. При этом загрузим данные, если это еще не было сделано, и сохраним тип ресурса, если он задан:

        #region IModelMetadataProvider Members

        public bool IsAbleToProvideMetadata(Type type)
        {
            if (type == null) {
                return false;
            }

            if (string.Equals(this._currentModelTypeName, type.FullName)) {
                return true;
            }

            string metadataFile = string.Format(
                "{0}{1}.config", this._metadataStorage, type.FullName);

            if (!File.Exists(metadataFile)) {
                return false;
            }

            this._xmlMetadata = XElement.Load(metadataFile);
            this._defaultResourceType = null;

            var resourceAttr = this._xmlMetadata.Attribute("resource");
            if (resourceAttr != null) {
                this._defaultResourceType = Type.GetType(resourceAttr.Value, throwOnError: true);
            }

            this._currentModelTypeName = type.FullName;

            return true;
        }

        #endregion

CreateMetadata()

Перейдем к основному методу CreateMetadata(). Его задачей будет заполнить метаданные Модели на основе информации из XML файла. Рассмотрим его работу:

  • Из общего списка выберем XML элемент <property> с данными для указанного свойства.
  • Обработаем все вложенные в него элементы <metadata>:
    • вызовем GetAndValidatePropertyName() для получения имени свойства ModelMetadata, в которое необходимо записать значение;
    • Поскольку AddionalValues является коллекцией, то для неё предусмотрен свой алгоритм установки значений в методе SetMetadataAdditionalValue().
    • Для всех остальных свойств получим значение вызовом метода GetValueFromElement() и присвоим его заданному свойству экземпляра ModelMetadata .

Исходный код метода:

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

            // Get the data for the specified property.
            var xmlPropertyElement = this._xmlMetadata.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);
                }
                else {
                    var metadataProperty = typeof(ModelMetadata).GetProperty(xmlPropertyName);
                    object value = Convert.ChangeType(
                        this.GetValueFromElement(element),
                        metadataProperty.PropertyType);

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

            return metadata;
        }

        #endregion

Дополнительные методы

Осталось рассмотреть несколько вспомогательных методов, которые использованы в приведенном выше коде. Начнем с GetAndValidatePropertyName(), ответственного за получение и проверку имени свойства:

        #region Private methods

        private string GetAndValidatePropertyName(XElement element)
        {
            string propertyName = element.GetValueOrNull("name");
            if (propertyName == null) {
                throw new InvalidDataException(
                    string.Format("Name was not set for the 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():

        private void SetMetadataAdditionalValue(ModelMetadata metadata, XElement element)
        {
            string key = element.GetValueOrNull("key");
            if (key == null) {
                throw new InvalidDataException(
                    string.Format(
                        "The key was not set for the 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),
                Type.GetType(typeName));

            addVal.Add(key, value);
        }

Этот метод реализует логику добавления новой пары "ключ-значение" в коллекцию AdditionalValues. При этом возможно явное указание его типа значения, а так же загрузка его из ресурсов веб-приложения. Кроме того, в данном случае необходимо вначале получить используемый экземпляр коллекции. И уже затем добавить в него новую пару "ключ-значение".

Последний из методов, используемых при форматировании метаданных, это GetValueFromElement(). Его цель – вернуть значение, взяв его или из XML атрибута или ресурса веб-приложения:

        private string GetValueFromElement(XElement element)
        {
            // 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 is not set for the attribute '{0}'.", element));
            }

            string resourceTypeName = element.GetValueOrNull("resource");
            Type resourceType = this._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(), предназначенный для контроля списка _availableProperties. При обнаружении в нем имени свойства, несуществующего в классе ModelMetadata, будет выброшено исключение. Отметим этот метод атрибутом [Conditional("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
    }
}

Назначаем метаданные для PaymentModel

Перед тем, как задействовать созданного провайдера, необходимо определить информацию для записи в метаданные Модели. Для этого добавим служебную папку App_Data. В ней создадим еще одну папку ModelMetadata, где и будем размещать данные для конфигурации. Как и было оговорено, назовем файл полным имением типа "MVCDemo.Models.PaymentModel.config". Обратите внимание на то, что в самой конфигурации также указываются полные имена используемых типов:

<?xml version="1.0" encoding="utf-8" ?>
<model resource="MVCDemo.Resources.Models.PaymentRes">

  <property name="Id">
    <metadata name="ShowForDisplay" value="false" />
    <metadata name="ShowForEdit" value="false" />
  </property>
  
  <property name="UserLogin">
    <metadata name="DisplayName" resname="UserLogin" />
    <metadata name="IsRequired" value="true" />
  </property>

  <property name="Date">
    <metadata name="DisplayName" resname="Date" />
    <metadata name="IsRequired" value="true" />
    <metadata name="DataTypeName" value="DateTime" />
  </property>

  <property name="Amount">
    <metadata name="DisplayName" resname="Amount" />
    <metadata name="DataTypeName" value="Currency" />
  </property>

  <property name="Info">
    <metadata name="DisplayName" resname="Info" />
    <metadata name="DataTypeName" value="MultilineText" />
  </property>

</model>

В методе Application_Start() добавим в список менеджера экземпляр XmlModelMetadataProvider:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);

    var manager = new ModelMetadataProvidersManager(ModelMetadataProviders.Current);

    manager.Providers.Add(
        new XmlModelMetadataProvider(
            Server.MapPath("~/App_Data/ModelMetadata/")));

    ModelMetadataProviders.Current = manager;
}

Как обычно, в завершении части запустим веб-приложение. Перейдя на страницу формы добавления данных о платеже, можно легко заметить изменения. Поля подписаны, осуществляется контроль обязательного ввода значений. Кроме того, после дополнительной информации стало многострочным.

Не хватает только проверки вводимых значений. Это и будет целью следующих частей.


Исходный код проекта (C#, Visual Studio 2010): MVCDemo-Part20.zip (502 Kb)

Pingbacks and trackbacks (3)+

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