После разработки менеджера провайдеров метаданных в прошлой части, появилась возможность использовать несколько их экземпляров. Используем её для указания метаданных Модели с помощью информации из 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)