Set `ModelMetadata.TemplateHint` based on data annotations

- add `CachedDataAnnotationsMetadataAttributes.UIHint`
- set `ModelMetadata.TemplateHint` using `UIHintAttribute` or `HiddenInputAttribute`
- add doc comments for `TemplateHint`-related properties and methods
- add unit tests and use these attributes in functional tests

nits:
- cache and seal `CachedModelMetadata.IsCollectionType`
- correct doc comments for `ModelMetadata.RealModelType`
- add doc comments for `IsCollectionType`-related properties and methods
- add doc comments for `IsComplexType`-related properties and methods
- move `CachedModelMetadata.IsComplexType` right below `IsCollectionType`
 - same for related fields and methods
This commit is contained in:
Doug Bunting 2015-02-12 21:42:41 -08:00
parent 451db6fb16
commit 0549769fd0
10 changed files with 210 additions and 31 deletions

View File

@ -1,7 +1,6 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@ -20,6 +19,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
HiddenInput = attributes.OfType<HiddenInputAttribute>().FirstOrDefault();
Required = attributes.OfType<RequiredAttribute>().FirstOrDefault();
ScaffoldColumn = attributes.OfType<ScaffoldColumnAttribute>().FirstOrDefault();
UIHint = attributes.OfType<UIHintAttribute>().FirstOrDefault();
BinderMetadata = attributes.OfType<IBinderMetadata>().FirstOrDefault();
PropertyBindingPredicateProviders = attributes.OfType<IPropertyBindingPredicateProvider>();
BinderModelNameProvider = attributes.OfType<IModelNameProvider>().FirstOrDefault();
@ -82,7 +83,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public HiddenInputAttribute HiddenInput { get; protected set; }
/// <summary>
/// Gets (or sets in subclasses) <see cref="IEnumerable{IPropertyBindingPredicateProvider}"/> found in
/// Gets (or sets in subclasses) <see cref="IEnumerable{IPropertyBindingPredicateProvider}"/> found in
/// collection passed to the <see cref="CachedDataAnnotationsMetadataAttributes(IEnumerable{object})"/>
/// constructor, if any.
/// </summary>
@ -91,5 +92,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public RequiredAttribute Required { get; protected set; }
public ScaffoldColumnAttribute ScaffoldColumn { get; protected set; }
/// <summary>
/// Gets (or sets in subclasses) <see cref="UIHintAttribute"/> found in collection passed to the
/// <see cref="CachedDataAnnotationsMetadataAttributes(IEnumerable{object})"/> constructor, if any.
/// </summary>
public UIHintAttribute UIHint { get; protected set; }
}
}

View File

@ -9,8 +9,8 @@ using System.Reflection;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
// Class does not override ComputeIsComplexType() because value calculated in ModelMetadata's base implementation
// is correct.
// Class does not override ComputeIsCollectionType() or ComputeIsComplexType() because values calculated in
// ModelMetadata's base implementation are correct. No data annotations override those calculations.
public class CachedDataAnnotationsModelMetadata : CachedModelMetadata<CachedDataAnnotationsMetadataAttributes>
{
private static readonly string HtmlName = DataType.Html.ToString();
@ -333,6 +333,31 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
: base.ComputeShowForEdit();
}
/// <summary>
/// Calculate the <see cref="ModelMetadata.TemplateHint"/> value based on presence of a
/// <see cref="UIHintAttribute"/> and its <see cref="UIHintAttribute.UIHint"/> value or presence of a
/// <see cref="HiddenInputAttribute"/> when no <see cref="UIHintAttribute"/> exists.
/// </summary>
/// <returns>
/// Calculated <see cref="ModelMetadata.TemplateHint"/> value. <see cref="UIHintAttribute.UIHint"/> if a
/// <see cref="UIHintAttribute"/> exists. <c>"HiddenInput"</c> if a <see cref="HiddenInputAttribute"/> exists
/// and no <see cref="UIHintAttribute"/> exists. <c>null</c> otherwise.
/// </returns>
protected override string ComputeTemplateHint()
{
if (PrototypeCache.UIHint != null)
{
return PrototypeCache.UIHint.UIHint;
}
if (PrototypeCache.HiddenInput != null)
{
return "HiddenInput";
}
return base.ComputeTemplateHint();
}
private static void ValidateDisplayColumnAttribute(DisplayColumnAttribute displayColumnAttribute,
PropertyInfo displayColumnProperty, Type modelType)
{

View File

@ -25,13 +25,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private bool _hasNonDefaultEditFormat;
private bool _hideSurroundingHtml;
private bool _htmlEncode;
private bool _isReadOnly;
private bool _isCollectionType;
private bool _isComplexType;
private bool _isReadOnly;
private bool _isRequired;
private string _nullDisplayText;
private int _order;
private bool _showForDisplay;
private bool _showForEdit;
private string _templateHint;
private IBinderMetadata _binderMetadata;
private string _binderModelName;
private IPropertyBindingPredicateProvider _propertyBindingPredicateProvider;
@ -46,13 +48,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private bool _hasNonDefaultEditFormatComputed;
private bool _hideSurroundingHtmlComputed;
private bool _htmlEncodeComputed;
private bool _isReadOnlyComputed;
private bool _isCollectionTypeComputed;
private bool _isComplexTypeComputed;
private bool _isReadOnlyComputed;
private bool _isRequiredComputed;
private bool _nullDisplayTextComputed;
private bool _orderComputed;
private bool _showForDisplayComputed;
private bool _showForEditComputed;
private bool _templateHintComputed;
private bool _isBinderMetadataComputed;
private bool _isBinderModelNameComputed;
private bool _isBinderTypeComputed;
@ -320,6 +324,35 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
/// <inheritdoc />
public sealed override bool IsCollectionType
{
get
{
if (!_isCollectionTypeComputed)
{
_isCollectionType = ComputeIsCollectionType();
_isCollectionTypeComputed = true;
}
return _isCollectionType;
}
}
/// <inheritdoc />
public sealed override bool IsComplexType
{
get
{
if (!_isComplexTypeComputed)
{
_isComplexType = ComputeIsComplexType();
_isComplexTypeComputed = true;
}
return _isComplexType;
}
}
/// <inheritdoc />
public sealed override bool IsReadOnly
{
@ -358,20 +391,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
/// <inheritdoc />
public sealed override bool IsComplexType
{
get
{
if (!_isComplexTypeComputed)
{
_isComplexType = ComputeIsComplexType();
_isComplexTypeComputed = true;
}
return _isComplexType;
}
}
/// <inheritdoc />
public sealed override string NullDisplayText
{
@ -495,6 +514,27 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
/// <inheritdoc />
public sealed override string TemplateHint
{
get
{
if (!_templateHintComputed)
{
_templateHint = ComputeTemplateHint();
_templateHintComputed = true;
}
return _templateHint;
}
set
{
_templateHint = value;
_templateHintComputed = true;
}
}
/// <inheritdoc />
public sealed override Type BinderType
{
@ -605,6 +645,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return base.HtmlEncode;
}
/// <summary>
/// Calculate the <see cref="IsCollectionType"/> value.
/// </summary>
/// <returns>Calculated <see cref="IsCollectionType"/> value.</returns>
protected virtual bool ComputeIsCollectionType()
{
return base.IsCollectionType;
}
/// <summary>
/// Calculate the <see cref="IsComplexType"/> value.
/// </summary>
/// <returns>Calculated <see cref="IsComplexType"/> value.</returns>
protected virtual bool ComputeIsComplexType()
{
return base.IsComplexType;
}
protected virtual bool ComputeIsReadOnly()
{
return base.IsReadOnly;
@ -615,11 +673,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return base.IsRequired;
}
protected virtual bool ComputeIsComplexType()
{
return base.IsComplexType;
}
/// <summary>
/// Calculate the <see cref="NullDisplayText"/> value.
/// </summary>
@ -647,5 +700,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
return base.ShowForEdit;
}
/// <summary>
/// Calculate the <see cref="TemplateHint"/> value.
/// </summary>
/// <returns>Calculated <see cref="TemplateHint"/> value.</returns>
protected virtual string ComputeTemplateHint()
{
return base.TemplateHint;
}
}
}

View File

@ -142,11 +142,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </remarks>
public virtual bool HideSurroundingHtml { get; set; }
/// <summary>
/// Gets a value indicating whether the <see cref="ModelType"/> is a collection <see cref="Type"/>.
/// </summary>
/// <remarks>
/// <c>true</c> if the <see cref="ModelType"/> is not <see cref="string"/> and is assignable to
/// <see cref="System.Collections.IEnumerable"/>; <c>false</c> otherwise.
/// </remarks>
public virtual bool IsCollectionType
{
get { return TypeHelper.IsCollectionType(ModelType); }
}
/// <summary>
/// Gets a value indicating whether the <see cref="ModelType"/> is a complex type.
/// </summary>
/// <remarks>
/// <c>false</c> if the <see cref="ModelType"/> has a direct conversion to <see cref="string"/>; <c>true</c>
/// otherwise.
/// </remarks>
public virtual bool IsComplexType
{
get { return !TypeHelper.HasStringConverter(ModelType); }
@ -238,8 +252,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
protected IModelMetadataProvider Provider { get; set; }
/// <returns>
/// Gets <c>T</c> if <see cref="ModelType"/> is <see cref="Nullable{T}"/>;
/// <see cref="ModelType"/> otherwise.
/// Gets runtime <see cref="Type"/> of <see cref="Model"/> if <see cref="Model"/> is non-<c>null</c> and
/// <see cref="ModelType"/> is not <see cref="Nullable{T}"/>; <see cref="ModelType"/> otherwise.
/// </returns>
public Type RealModelType
{
@ -294,6 +308,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
set { _showForEdit = value; }
}
/// <summary>
/// Gets or sets a hint that suggests what template to use for this model. Overrides <see cref="DataTypeName"/>
/// in that context but, unlike <see cref="DataTypeName"/>, this value is not used elsewhere.
/// </summary>
/// <value><c>null</c> unless set manually or through additional metadata e.g. attributes.</value>
public virtual string TemplateHint { get; set; }
internal EfficientTypePropertyKey<Type, string> CacheKey

View File

@ -32,6 +32,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Null(cache.HiddenInput);
Assert.Null(cache.Required);
Assert.Null(cache.ScaffoldColumn);
Assert.Null(cache.UIHint);
Assert.Null(cache.BinderMetadata);
Assert.Null(cache.BinderModelNameProvider);
Assert.Empty(cache.PropertyBindingPredicateProviders);
@ -51,6 +52,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ new EditableAttribute(allowEdit: false), cache => cache.Editable },
{ new HiddenInputAttribute(), cache => cache.HiddenInput },
{ new RequiredAttribute(), cache => cache.Required },
{ new ScaffoldColumnAttribute(scaffold: true), cache => cache.ScaffoldColumn },
{ new UIHintAttribute("hintHint"), cache => cache.UIHint },
{ new TestBinderMetadata(), cache => cache.BinderMetadata },
{ new TestModelNameProvider(), cache => cache.BinderModelNameProvider },
};

View File

@ -180,7 +180,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
[Fact]
public void HiddenInputWorksOnProperty()
public void HiddenInputWorksOnProperty_ForHideSurroundingHtml()
{
// Arrange
var provider = new DataAnnotationsModelMetadataProvider();
@ -195,7 +195,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
[Fact]
public void HiddenInputWorksOnPropertyType()
public void HiddenInputWorksOnPropertyType_ForHideSurroundingHtml()
{
// Arrange
var provider = new DataAnnotationsModelMetadataProvider();
@ -209,6 +209,36 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.True(result);
}
[Fact]
public void HiddenInputWorksOnProperty_ForTemplateHint()
{
// Arrange
var provider = new DataAnnotationsModelMetadataProvider();
var metadata = provider.GetMetadataForType(modelAccessor: null, modelType: typeof(ClassWithHiddenProperties));
var property = metadata.Properties["DirectlyHidden"];
// Act
var result = property.TemplateHint;
// Assert
Assert.Equal("HiddenInput", result);
}
[Fact]
public void HiddenInputWorksOnPropertyType_ForTemplateHint()
{
// Arrange
var provider = new DataAnnotationsModelMetadataProvider();
var metadata = provider.GetMetadataForType(modelAccessor: null, modelType: typeof(ClassWithHiddenProperties));
var property = metadata.Properties["OfHiddenType"];
// Act
var result = property.TemplateHint;
// Assert
Assert.Equal("HiddenInput", result);
}
[Fact]
public void GetMetadataForProperty_WithNoBinderMetadata_GetsItFromType()
{

View File

@ -109,7 +109,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new DisplayFormatAttribute { NullDisplayText = "value" }, metadata => metadata.NullDisplayText
},
{
new TestModelNameProvider() { Name = "value" }, metadata => metadata.BinderModelName
new TestModelNameProvider { Name = "value" }, metadata => metadata.BinderModelName
},
{
new UIHintAttribute("value"), metadata => metadata.TemplateHint
},
};
}
@ -208,6 +211,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
metadata => metadata.HideSurroundingHtml,
false
},
{
new HiddenInputAttribute(),
metadata => string.Equals("HiddenInput", metadata.TemplateHint, StringComparison.Ordinal),
true
},
{
new RequiredAttribute(),
metadata => metadata.IsRequired,
@ -466,6 +474,28 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Null(metadata.EditFormatString);
}
[Fact]
public void TemplateHint_AttributesHaveExpectedPrecedence()
{
// Arrange
var expected = "this is a hint";
var hidden = new HiddenInputAttribute();
var uiHint = new UIHintAttribute(expected);
var provider = new DataAnnotationsModelMetadataProvider();
var metadata = new CachedDataAnnotationsModelMetadata(
provider,
containerType: null,
modelType: typeof(object),
propertyName: null,
attributes: new Attribute[] { hidden, uiHint });
// Act
var result = metadata.TemplateHint;
// Assert
Assert.Equal(expected, result);
}
[Fact]
public void Constructor_FindsBinderTypeProviders_Null()
{

View File

@ -2,11 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc;
namespace MvcTagHelpersWebSite.Models
{
public class Person
{
[HiddenInput(DisplayValue = false)]
[Range(1, 100)]
public int Number
{
@ -28,6 +30,7 @@ namespace MvcTagHelpersWebSite.Models
}
[EnumDataType(typeof(Gender))]
[UIHint("GenderUsingTagHelpers")]
public Gender Gender
{
get;

View File

@ -42,7 +42,7 @@
}
@Html.DropDownListFor(m => m.Employee.OfficeNumber, offices)
</div>
@Html.HiddenFor(m => m.Employee.Number)
@Html.EditorFor(m => m.Employee.Number)
@Html.ValidationSummary()
<input type="submit" />
</form>