Add `TextBox` and `TextBoxFor`

- HtmlHelper service now needs an `IModelMetadataProvider` instance
- make `GetInputTypeString()` private
- use `TextBox()` and `TextBoxFor()` in MVC sample
- throw if `ExpressionMetadataProvider.FromLambdaExpression()` returns `null`
This commit is contained in:
dougbu 2014-03-31 13:27:48 -07:00
parent a1a180d4d0
commit 0a62a581de
7 changed files with 355 additions and 5 deletions

View File

@ -7,6 +7,7 @@
ViewBag.Anon = new
{
Name = "FirstName LastName",
Address = new { Street = "123 Fake St.", City = "Redmond", State = "WA", Country = "USA", },
};
}
@ -61,7 +62,7 @@
</div>
<div class="row">
<div style="float: left; border: 5px solid blue;">
<div style="float: left; border: 5px solid blue; padding-right: 10px;">
<table>
<tr>
<td>
@ -98,6 +99,76 @@
</table>
</div>
<div style="float: left; border: thick solid lightskyblue; margin-left: 15px; padding-right: 10px">
<form action="/Home/Hello" method="post">
<table>
<tr>
<td>
<label class="control-label col-md-2">Model.Name</label>
</td>
<td>
@Html.TextBox("Name")
</td>
</tr>
<tr>
<td>
<label class="control-label col-md-2">Model.Address</label>
</td>
<td>
@Html.TextBoxFor(m => m.Address, htmlAttributes: new { @class = "form-control" })
</td>
</tr>
<tr>
<td>
<label class="control-label col-md-2">Anon.Name</label>
</td>
<td>
@Html.TextBox("Anon.Name")
</td>
</tr>
<tr>
<td>
<label class="control-label col-md-2">Anon.Address.Street</label>
</td>
<td>
@Html.TextBox("Anon.Address.Street", (object)ViewBag.Anon.Address.Street)
</td>
</tr>
<tr>
<td>
<label class="control-label col-md-2">Anon.Address.City</label>
</td>
<td>
@Html.TextBox("Anon.Address.City", value: null, format: "{0} (3)")
</td>
</tr>
<tr>
<td>
<label class="control-label col-md-2">Anon.Address.State</label>
</td>
<td>
@Html.TextBox("Anon.Address.State", value: null, htmlAttributes: new { @class = "form-control" })
</td>
</tr>
<tr>
<td>
<label class="control-label col-md-2">Anon.Address.Country</label>
</td>
<td>
@Html.TextBox("Anon.Address.Country", value: null, format: "{0} (4)",
htmlAttributes: new { @class = "form-control" })
</td>
</tr>
</table>
<div class="form-group">
<div class="col-md-2">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</form>
</div>
<div style="float: right; border: 5px solid red;">
@await Component.InvokeAsync("Tags", 15)
</div>

View File

@ -7,6 +7,7 @@ using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Abstractions;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc.Rendering
{
@ -30,9 +31,10 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// <summary>
/// Initializes a new instance of the <see cref="HtmlHelper"/> class.
/// </summary>
public HtmlHelper(IViewEngine viewEngine)
public HtmlHelper([NotNull] IViewEngine viewEngine, [NotNull] IModelMetadataProvider metadataProvider)
{
_viewEngine = viewEngine;
MetadataProvider = metadataProvider;
// Underscores are fine characters in id's.
IdAttributeDotReplacement = "_";
@ -75,6 +77,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
}
}
protected IModelMetadataProvider MetadataProvider { get; private set; }
/// <summary>
/// Creates a dictionary of HTML attributes from the input object,
/// translating underscores to dashes.
@ -125,6 +129,12 @@ namespace Microsoft.AspNet.Mvc.Rendering
return value != null ? WebUtility.HtmlEncode(value.ToString()) : string.Empty;
}
/// <inheritdoc />
public string FormatValue(object value, string format)
{
return ViewDataDictionary.FormatValue(value, format);
}
public string GenerateIdFromName([NotNull] string name)
{
return TagBuilder.CreateSanitizedId(name, IdAttributeDotReplacement);
@ -283,7 +293,108 @@ namespace Microsoft.AspNet.Mvc.Rendering
}
}
public static string GetInputTypeString(InputType inputType)
/// <inheritdoc />
public HtmlString TextBox(string name, object value, string format, IDictionary<string, object> htmlAttributes)
{
return GenerateTextBox(metadata: null, name: name, value: value, format: format,
htmlAttributes: htmlAttributes);
}
protected string EvalString(string key, string format)
{
return Convert.ToString(ViewData.Eval(key, format), CultureInfo.CurrentCulture);
}
protected object GetModelStateValue(string key, Type destinationType)
{
ModelState modelState;
if (ViewData.ModelState.TryGetValue(key, out modelState) && modelState.Value != null)
{
return modelState.Value.ConvertTo(destinationType, culture: null);
}
return null;
}
protected IDictionary<string, object> GetValidationAttributes(string name)
{
return GetValidationAttributes(name, metadata: null);
}
// Only render attributes if unobtrusive client-side validation is enabled, and then only if we've
// never rendered validation for a field with this name in this form. Also, if there's no form context,
// then we can't render the attributes (we'd have no <form> to attach them to).
protected IDictionary<string, object> GetValidationAttributes(string name, ModelMetadata metadata)
{
// TODO: Add validation attributes to input helpers.
return new Dictionary<string, object>();
}
protected virtual HtmlString GenerateTextBox(ModelMetadata metadata, string name, object value, string format,
IDictionary<string, object> htmlAttributes)
{
return GenerateInput(InputType.Text,
metadata,
name,
value,
useViewData: (metadata == null && value == null),
isChecked: false,
setId: true,
isExplicitValue: true,
format: format,
htmlAttributes: htmlAttributes);
}
protected virtual HtmlString GenerateInput(InputType inputType, ModelMetadata metadata, string name,
object value, bool useViewData, bool isChecked, bool setId, bool isExplicitValue, string format,
IDictionary<string, object> htmlAttributes)
{
// Not valid to use TextBoxForModel() in a top-level view; would end up with an unnamed input elements.
// But we support the *ForModel() methods in any lower-level template, once HtmlFieldPrefix is non-empty.
var fullName = ViewData.TemplateInfo.GetFullHtmlFieldName(name);
if (string.IsNullOrEmpty(fullName))
{
throw new ArgumentException(Resources.ArgumentNullOrEmpty, "name");
}
var tagBuilder = new TagBuilder("input");
tagBuilder.MergeAttributes(htmlAttributes);
tagBuilder.MergeAttribute("type", GetInputTypeString(inputType));
tagBuilder.MergeAttribute("name", fullName, replaceExisting: true);
var valueParameter = FormatValue(value, format);
switch (inputType)
{
case InputType.Text:
default:
var attributeValue = (string)GetModelStateValue(fullName, typeof(string));
if (attributeValue == null)
{
attributeValue = useViewData ? EvalString(fullName, format) : valueParameter;
}
tagBuilder.MergeAttribute("value", attributeValue, replaceExisting: isExplicitValue);
break;
}
if (setId)
{
tagBuilder.GenerateId(fullName, IdAttributeDotReplacement);
}
// If there are any errors for a named field, we add the CSS attribute.
ModelState modelState;
if (ViewData.ModelState.TryGetValue(fullName, out modelState) && modelState.Errors.Count > 0)
{
tagBuilder.AddCssClass(ValidationInputCssClassName);
}
tagBuilder.MergeAttributes(GetValidationAttributes(name, metadata));
return tagBuilder.ToHtmlString(TagRenderMode.SelfClosing);
}
private static string GetInputTypeString(InputType inputType)
{
switch (inputType)
{

View File

@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq.Expressions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering.Expressions;
namespace Microsoft.AspNet.Mvc.Rendering
@ -9,8 +12,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// <summary>
/// Initializes a new instance of the <see cref="HtmlHelper{TModel}"/> class.
/// </summary>
public HtmlHelper(IViewEngine viewEngine)
: base(viewEngine)
public HtmlHelper([NotNull] IViewEngine viewEngine, [NotNull] IModelMetadataProvider metadataProvider)
: base(viewEngine, metadataProvider)
{
}
@ -45,9 +48,29 @@ namespace Microsoft.AspNet.Mvc.Rendering
return Name(expressionName);
}
/// <inheritdoc />
public HtmlString TextBoxFor<TProperty>([NotNull] Expression<Func<TModel, TProperty>> expression,
string format, IDictionary<string, object> htmlAttributes)
{
var metadata = GetModelMetadata(expression);
return GenerateTextBox(metadata, GetExpressionName(expression), metadata.Model, format, htmlAttributes);
}
protected string GetExpressionName<TProperty>([NotNull] Expression<Func<TModel, TProperty>> expression)
{
return ExpressionHelper.GetExpressionText(expression);
}
protected ModelMetadata GetModelMetadata<TProperty>([NotNull] Expression<Func<TModel, TProperty>> expression)
{
var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider);
if (metadata == null)
{
var expressionName = GetExpressionName(expression);
throw new InvalidOperationException(Resources.FormatHtmlHelper_NullModelMetadata(expressionName));
}
return metadata;
}
}
}

View File

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
namespace Microsoft.AspNet.Mvc.Rendering
{
public static class HtmlHelperInputExtensions
{
public static HtmlString TextBox<TModel>([NotNull] this IHtmlHelper<TModel> htmlHelper, string name)
{
return TextBox(htmlHelper, name, value: null);
}
public static HtmlString TextBox<TModel>([NotNull] this IHtmlHelper<TModel> htmlHelper, string name,
object value)
{
return TextBox(htmlHelper, name, value, format: null);
}
public static HtmlString TextBox<TModel>([NotNull] this IHtmlHelper<TModel> htmlHelper, string name,
object value, string format)
{
return TextBox(htmlHelper, name, value, format, htmlAttributes: null);
}
public static HtmlString TextBox<TModel>([NotNull] this IHtmlHelper<TModel> htmlHelper, string name,
object value, object htmlAttributes)
{
return TextBox(htmlHelper, name, value, format: null, htmlAttributes: htmlAttributes);
}
public static HtmlString TextBox<TModel>([NotNull] this IHtmlHelper<TModel> htmlHelper, string name,
object value, string format, object htmlAttributes)
{
return htmlHelper.TextBox(name, value, format,
HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public static HtmlString TextBox<TModel>([NotNull] this IHtmlHelper<TModel> htmlHelper, string name,
object value, IDictionary<string, object> htmlAttributes)
{
return htmlHelper.TextBox(name, value, format: null, htmlAttributes: htmlAttributes);
}
public static HtmlString TextBoxFor<TModel, TProperty>([NotNull] this IHtmlHelper<TModel> htmlHelper,
[NotNull] Expression<Func<TModel, TProperty>> expression)
{
return TextBoxFor(htmlHelper, expression, format: null);
}
public static HtmlString TextBoxFor<TModel, TProperty>([NotNull] this IHtmlHelper<TModel> htmlHelper,
[NotNull] Expression<Func<TModel, TProperty>> expression, string format)
{
return TextBoxFor(htmlHelper, expression, format, htmlAttributes: null);
}
public static HtmlString TextBoxFor<TModel, TProperty>([NotNull] this IHtmlHelper<TModel> htmlHelper,
[NotNull] Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
{
return TextBoxFor(htmlHelper, expression, format: null, htmlAttributes: htmlAttributes);
}
public static HtmlString TextBoxFor<TModel, TProperty>([NotNull] this IHtmlHelper<TModel> htmlHelper,
[NotNull] Expression<Func<TModel, TProperty>> expression, string format, object htmlAttributes)
{
return htmlHelper.TextBoxFor(expression, format: format,
htmlAttributes: HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public static HtmlString TextBoxFor<TModel, TProperty>([NotNull] this IHtmlHelper<TModel> htmlHelper,
[NotNull] Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
{
return htmlHelper.TextBoxFor(expression, format: null, htmlAttributes: htmlAttributes);
}
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Collections.Generic;
using System.Threading.Tasks;
@ -101,6 +102,37 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// <returns>A task that represents when rendering has completed.</returns>
Task RenderPartialAsync([NotNull] string partialViewName, object model, ViewDataDictionary viewData);
/// <summary>
/// Render an input element of type "text".
/// </summary>
/// <param name="name">
/// Rendered element's name. Also use this name to find value in submitted data or view data. Use view data
/// only if value is not in submitted data and <paramref name="value"/> is <c>null</c>.
/// </param>
/// <param name="value">
/// If non-<c>null</c>, value to include in the element. Ignore if named value is found in submitted data.
/// </param>
/// <param name="format"></param>
/// <param name="htmlAttributes">
/// <see cref="IDictionary{string, object}"/> containing additional HTML attributes.
/// </param>
/// <returns>New <see cref="HtmlString"/> containing the rendered HTML.</returns>
HtmlString TextBox(string name, object value, string format, IDictionary<string, object> htmlAttributes);
/// <summary>
/// Render an input element of type "text".
/// </summary>
/// <param name="expression">
/// An expression that identifies the object that contains the properties to render.
/// </param>
/// <param name="format"></param>
/// <param name="htmlAttributes">
/// <see cref="IDictionary{string, object}"/> containing additional HTML attributes.
/// </param>
/// <returns>New <see cref="HtmlString"/> containing the rendered HTML.</returns>
HtmlString TextBoxFor<TProperty>([NotNull] Expression<Func<TModel, TProperty>> expression, string format,
IDictionary<string, object> htmlAttributes);
/// <summary>
/// Returns an unordered list (ul element) of validation messages that are in the <see cref="ModelStateDictionary"/> object.
/// </summary>

View File

@ -1,3 +1,20 @@
// <auto-generated />
namespace Microsoft.AspNet.Mvc.Rendering
{
@ -122,6 +139,22 @@ namespace Microsoft.AspNet.Mvc.Rendering
return string.Format(CultureInfo.CurrentCulture, GetString("ExpressionHelper_InvalidIndexerExpression"), p0, p1);
}
/// <summary>
/// The IModelMetadataProvider was unable to provide metadata for expression '{0}'.
/// </summary>
internal static string HtmlHelper_NullModelMetadata
{
get { return GetString("HtmlHelper_NullModelMetadata"); }
}
/// <summary>
/// The IModelMetadataProvider was unable to provide metadata for expression '{0}'.
/// </summary>
internal static string FormatHtmlHelper_NullModelMetadata(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("HtmlHelper_NullModelMetadata"), p0);
}
/// <summary>
/// Must call 'Contextualize' method before using this HtmlHelper instance.
/// </summary>

View File

@ -138,6 +138,9 @@
<data name="ExpressionHelper_InvalidIndexerExpression" xml:space="preserve">
<value>The expression compiler was unable to evaluate the indexer expression '{0}' because it references the model parameter '{1}' which is unavailable.</value>
</data>
<data name="HtmlHelper_NullModelMetadata" xml:space="preserve">
<value>The IModelMetadataProvider was unable to provide metadata for expression '{0}'.</value>
</data>
<data name="HtmlHelper_NotContextualized" xml:space="preserve">
<value>Must call 'Contextualize' method before using this HtmlHelper instance.</value>
</data>