Разработка интерфейса для источников данных обеспечила независимость от их типов. Кроме того, был создан механизм взаимодействия с ними. Теперь создадим реализацию провайдера метаданных модели как часть провайдера её конфигурации.
Создание провайдера метаданных Модели
Данный процесс уже достаточно знаком, поэтому не будем сильно вдаваться в его подробности. Однако разберемся что изменилось в данной реализации. Рассматривать код будем также по частям.
Разместим создаваемый класс 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
}
}
}
Разработка провайдера метаданных завершена. Можно скомпилировать проект чтобы убедиться что ошибки в нем отсутствуют. В следующей части создадим провайдер правил проверки данных и необходимые для его дополнительные классы. >>