Провайдер метаданных Dynamic Model (non-class) в MVC
Мы разрабатываем приложение, в котором схема конечного пользователя является динамической (для этого у нас есть хороший бизнес-сценарий - это не то, с чем легко справляется статическая модель).
Я использовал класс .NET DynamicObject, чтобы позволить этим объектам динамических схем легко обращаться с кодом, и ожидал, что это будет просто работать с метаданными модели MVC. Однако поддержка метаданных MVC, похоже, затруднена тем, что она касается только метаданных, определенных для каждого типа, а не для каждого объекта, который будет иметь место здесь.
Даже когда я откопал и попытался реализовать собственный ModelMetadataProvider, кажется, что необходимая информация просто не проходит - метод GetMetadataForProperty особенно проблематичен. Фактически мне нужно получить доступ к родительскому или контейнерному объекту для свойства, но все, что передается, это тип.
Вышеупомянутое вызывается в основном из метода FromStringExpression в классе ModelMetadata. Этот метод фактически имеет контейнер (по крайней мере, в этом случае), но не проходит его. Эта ветка выполняется, когда она находит данные представления о сохраненном выражении (кэшированном?) В ViewData. Если это не удается, оно возвращается к просмотру через объект ModelMetadata, что иронически может сработать для меня. Особенно раздражает то, что метод FromStringExpression является статическим, поэтому я не могу легко переопределить его поведение.
В отчаянии я рассмотрел попытку пересечения выражения modelAccessor, но это похоже на kludge в лучшем случае и очень хрупкое.
Я искал экстенсивно для решения этого. Многие указывают на разговор Брэда Уилсона (http://channel9.msdn.com/Series/mvcConf/mvcConf-2011-Brad-Wilson-Advanced-MVC-3) по неклассическим моделям, однако, если вы посмотрите на фактический представленный код, вы увидите, что он TOO привязан к ТИП, а не объект - другими словами, не очень полезно. Другие указали на http://fluentvalidation.codeplex.com/, но похоже, что это относится только к стороне проверки, и я подозреваю, что страдает от той же проблемы (привязанной к типу, а не к объекту) как указано выше.
Например, у меня может быть словарь, содержащий ряд объектов поля. Это выглядит примерно так (очень сокращенный/упрощенный пример):
public class Entity : DynamicObject, ICustomTypeDescriptor
{
public Guid ID { get; set; }
public Dictionary<string, EntityProp> Props { get; set; }
... DynamicObject and ICustomTypeDescriptor implementation to expose Props as dynamic properties against this Entity ...
}
public class EntityProp
{
public string Name { get; set; }
public object Value { get; set; }
public Type Type { get; set; }
public bool IsRequired { get; set; }
}
Это может быть передано виду в виде его режима просмотра (или его части), и, на мой взгляд, я хотел бы использовать:
@Html.EditorForModel()
Кто-нибудь нашел способ обойти это?
Я определил два возможных альтернативных подхода, но оба имеют существенные недостатки:
- Отказаться от использования MVC ModelMetadata для этого и вместо этого создать модель представления, которая непосредственно содержит необходимые метаданные, вместе с шаблонами, необходимыми для отображения этих более сложных объектов модели представления. Значит, однако, что мне тогда придется относиться к этим объектам по-разному к "нормальным" объектам, несколько навредив цели и увеличив количество шаблонов представлений, которые нам нужно построить. Это подход, к которому я склоняюсь сейчас - более или менее отказ от интеграции с материалом MVC ModelMetadatastrong >
- Создайте уникальный ключ для каждого шаблонного свойства и используйте его для имени свойства, а не для отображаемого имени, которое позволит ModelMetadataProvider найти метаданные, связанные с этим свойством, без ссылки на его родительский элемент. Однако это привело бы к довольно уродливой ситуации при отладке и снова показалось бы крупномасштабным kludge. Я сейчас попробовал упрощенную версию этого и, похоже, работает, но имеет какое-то нежелательное поведение, например, нужно использовать бессмысленное имя свойства, если я хочу явно привязать к элементам модели.
- В ModelMetadataProvider при возврате коллекции объектов ModelMetadata для содержащихся свойств запишите контейнер в ModelMetadataProvider, который связан с этими возвращаемыми свойствами. Я пробовал это, но эта возвращенная коллекция метаданных свойств игнорируется в этом случае, а метод FromStringExpression переходит непосредственно к методу GetMetadataForProperty.
Ответы
Ответ 1
Возможно создание пользовательского ModelMetadataProvider:
public class CustomViewModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
public override IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType)
{
if (containerType == null)
{
throw new ArgumentNullException("containerType");
}
return GetMetadataForPropertiesImpl(container, containerType);
}
private IEnumerable<ModelMetadata> GetMetadataForPropertiesImpl(object container, Type containerType)
{
var propertiesMetadata = new List<ModelMetadata>();
foreach (EntityProp eprop in ((Entity)container).Props.Values)
{
Func<object> modelAccessor = () => eprop;
propertiesMetadata.add(GetMetadataForProperty(modelAccessor, containerType, eprop.Name));
}
return propertiesMetadata; // List returned instead of yielding, hoping not be needed to re-call this method more than once
}
public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName) {
if (containerType == null) {
throw new ArgumentNullException("containerType");
}
if (String.IsNullOrEmpty(propertyName)) {
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "propertyName");
}
return CreateMetadata(null, containerType, modelAccessor, modelAccessor().Type, propertyName);
}
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
EntityProp eprop = modelAccessor();
DataAnnotationsModelMetadata result;
if (propertyName == null)
{
// You have the main object
return base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
}
else
{
// You have here the property object
result = new DataAnnotationsModelMetadata(this, containerType, () => eprop.Value, modelType, propertyName, null);
result.IsRequired = eprop.IsRequired;
}
return result;
}
}
Наконец, настройте своего настраиваемого поставщика в Global.asax.cs
:
protected void Application_Start()
{
//...
ModelMetadataProviders.Current = new CustomViewModelMetadataProvider();
}