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. // 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. // 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.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
@ -20,6 +19,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
HiddenInput = attributes.OfType<HiddenInputAttribute>().FirstOrDefault(); HiddenInput = attributes.OfType<HiddenInputAttribute>().FirstOrDefault();
Required = attributes.OfType<RequiredAttribute>().FirstOrDefault(); Required = attributes.OfType<RequiredAttribute>().FirstOrDefault();
ScaffoldColumn = attributes.OfType<ScaffoldColumnAttribute>().FirstOrDefault(); ScaffoldColumn = attributes.OfType<ScaffoldColumnAttribute>().FirstOrDefault();
UIHint = attributes.OfType<UIHintAttribute>().FirstOrDefault();
BinderMetadata = attributes.OfType<IBinderMetadata>().FirstOrDefault(); BinderMetadata = attributes.OfType<IBinderMetadata>().FirstOrDefault();
PropertyBindingPredicateProviders = attributes.OfType<IPropertyBindingPredicateProvider>(); PropertyBindingPredicateProviders = attributes.OfType<IPropertyBindingPredicateProvider>();
BinderModelNameProvider = attributes.OfType<IModelNameProvider>().FirstOrDefault(); BinderModelNameProvider = attributes.OfType<IModelNameProvider>().FirstOrDefault();
@ -82,7 +83,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public HiddenInputAttribute HiddenInput { get; protected set; } public HiddenInputAttribute HiddenInput { get; protected set; }
/// <summary> /// <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})"/> /// collection passed to the <see cref="CachedDataAnnotationsMetadataAttributes(IEnumerable{object})"/>
/// constructor, if any. /// constructor, if any.
/// </summary> /// </summary>
@ -91,5 +92,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public RequiredAttribute Required { get; protected set; } public RequiredAttribute Required { get; protected set; }
public ScaffoldColumnAttribute ScaffoldColumn { 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 namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
// Class does not override ComputeIsComplexType() because value calculated in ModelMetadata's base implementation // Class does not override ComputeIsCollectionType() or ComputeIsComplexType() because values calculated in
// is correct. // ModelMetadata's base implementation are correct. No data annotations override those calculations.
public class CachedDataAnnotationsModelMetadata : CachedModelMetadata<CachedDataAnnotationsMetadataAttributes> public class CachedDataAnnotationsModelMetadata : CachedModelMetadata<CachedDataAnnotationsMetadataAttributes>
{ {
private static readonly string HtmlName = DataType.Html.ToString(); private static readonly string HtmlName = DataType.Html.ToString();
@ -333,6 +333,31 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
: base.ComputeShowForEdit(); : 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, private static void ValidateDisplayColumnAttribute(DisplayColumnAttribute displayColumnAttribute,
PropertyInfo displayColumnProperty, Type modelType) PropertyInfo displayColumnProperty, Type modelType)
{ {

View File

@ -25,13 +25,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private bool _hasNonDefaultEditFormat; private bool _hasNonDefaultEditFormat;
private bool _hideSurroundingHtml; private bool _hideSurroundingHtml;
private bool _htmlEncode; private bool _htmlEncode;
private bool _isReadOnly; private bool _isCollectionType;
private bool _isComplexType; private bool _isComplexType;
private bool _isReadOnly;
private bool _isRequired; private bool _isRequired;
private string _nullDisplayText; private string _nullDisplayText;
private int _order; private int _order;
private bool _showForDisplay; private bool _showForDisplay;
private bool _showForEdit; private bool _showForEdit;
private string _templateHint;
private IBinderMetadata _binderMetadata; private IBinderMetadata _binderMetadata;
private string _binderModelName; private string _binderModelName;
private IPropertyBindingPredicateProvider _propertyBindingPredicateProvider; private IPropertyBindingPredicateProvider _propertyBindingPredicateProvider;
@ -46,13 +48,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private bool _hasNonDefaultEditFormatComputed; private bool _hasNonDefaultEditFormatComputed;
private bool _hideSurroundingHtmlComputed; private bool _hideSurroundingHtmlComputed;
private bool _htmlEncodeComputed; private bool _htmlEncodeComputed;
private bool _isReadOnlyComputed; private bool _isCollectionTypeComputed;
private bool _isComplexTypeComputed; private bool _isComplexTypeComputed;
private bool _isReadOnlyComputed;
private bool _isRequiredComputed; private bool _isRequiredComputed;
private bool _nullDisplayTextComputed; private bool _nullDisplayTextComputed;
private bool _orderComputed; private bool _orderComputed;
private bool _showForDisplayComputed; private bool _showForDisplayComputed;
private bool _showForEditComputed; private bool _showForEditComputed;
private bool _templateHintComputed;
private bool _isBinderMetadataComputed; private bool _isBinderMetadataComputed;
private bool _isBinderModelNameComputed; private bool _isBinderModelNameComputed;
private bool _isBinderTypeComputed; 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 /> /// <inheritdoc />
public sealed override bool IsReadOnly 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 /> /// <inheritdoc />
public sealed override string NullDisplayText 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 /> /// <inheritdoc />
public sealed override Type BinderType public sealed override Type BinderType
{ {
@ -605,6 +645,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return base.HtmlEncode; 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() protected virtual bool ComputeIsReadOnly()
{ {
return base.IsReadOnly; return base.IsReadOnly;
@ -615,11 +673,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return base.IsRequired; return base.IsRequired;
} }
protected virtual bool ComputeIsComplexType()
{
return base.IsComplexType;
}
/// <summary> /// <summary>
/// Calculate the <see cref="NullDisplayText"/> value. /// Calculate the <see cref="NullDisplayText"/> value.
/// </summary> /// </summary>
@ -647,5 +700,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{ {
return base.ShowForEdit; 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> /// </remarks>
public virtual bool HideSurroundingHtml { get; set; } 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 public virtual bool IsCollectionType
{ {
get { return TypeHelper.IsCollectionType(ModelType); } 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 public virtual bool IsComplexType
{ {
get { return !TypeHelper.HasStringConverter(ModelType); } get { return !TypeHelper.HasStringConverter(ModelType); }
@ -238,8 +252,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
protected IModelMetadataProvider Provider { get; set; } protected IModelMetadataProvider Provider { get; set; }
/// <returns> /// <returns>
/// Gets <c>T</c> if <see cref="ModelType"/> is <see cref="Nullable{T}"/>; /// Gets runtime <see cref="Type"/> of <see cref="Model"/> if <see cref="Model"/> is non-<c>null</c> and
/// <see cref="ModelType"/> otherwise. /// <see cref="ModelType"/> is not <see cref="Nullable{T}"/>; <see cref="ModelType"/> otherwise.
/// </returns> /// </returns>
public Type RealModelType public Type RealModelType
{ {
@ -294,6 +308,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
set { _showForEdit = value; } 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; } public virtual string TemplateHint { get; set; }
internal EfficientTypePropertyKey<Type, string> CacheKey internal EfficientTypePropertyKey<Type, string> CacheKey

View File

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

View File

@ -180,7 +180,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
} }
[Fact] [Fact]
public void HiddenInputWorksOnProperty() public void HiddenInputWorksOnProperty_ForHideSurroundingHtml()
{ {
// Arrange // Arrange
var provider = new DataAnnotationsModelMetadataProvider(); var provider = new DataAnnotationsModelMetadataProvider();
@ -195,7 +195,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
} }
[Fact] [Fact]
public void HiddenInputWorksOnPropertyType() public void HiddenInputWorksOnPropertyType_ForHideSurroundingHtml()
{ {
// Arrange // Arrange
var provider = new DataAnnotationsModelMetadataProvider(); var provider = new DataAnnotationsModelMetadataProvider();
@ -209,6 +209,36 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.True(result); 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] [Fact]
public void GetMetadataForProperty_WithNoBinderMetadata_GetsItFromType() public void GetMetadataForProperty_WithNoBinderMetadata_GetsItFromType()
{ {

View File

@ -109,7 +109,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new DisplayFormatAttribute { NullDisplayText = "value" }, metadata => metadata.NullDisplayText 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, metadata => metadata.HideSurroundingHtml,
false false
}, },
{
new HiddenInputAttribute(),
metadata => string.Equals("HiddenInput", metadata.TemplateHint, StringComparison.Ordinal),
true
},
{ {
new RequiredAttribute(), new RequiredAttribute(),
metadata => metadata.IsRequired, metadata => metadata.IsRequired,
@ -466,6 +474,28 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Null(metadata.EditFormatString); 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] [Fact]
public void Constructor_FindsBinderTypeProviders_Null() 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc;
namespace MvcTagHelpersWebSite.Models namespace MvcTagHelpersWebSite.Models
{ {
public class Person public class Person
{ {
[HiddenInput(DisplayValue = false)]
[Range(1, 100)] [Range(1, 100)]
public int Number public int Number
{ {
@ -28,6 +30,7 @@ namespace MvcTagHelpersWebSite.Models
} }
[EnumDataType(typeof(Gender))] [EnumDataType(typeof(Gender))]
[UIHint("GenderUsingTagHelpers")]
public Gender Gender public Gender Gender
{ {
get; get;

View File

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