diff --git a/samples/MvcSample.Web/Views/Shared/MyView.cshtml b/samples/MvcSample.Web/Views/Shared/MyView.cshtml index 8b0000fb83..9f8a781e71 100644 --- a/samples/MvcSample.Web/Views/Shared/MyView.cshtml +++ b/samples/MvcSample.Web/Views/Shared/MyView.cshtml @@ -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 @@
-
+
@@ -98,6 +99,76 @@
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + @Html.TextBox("Name") +
+ + + @Html.TextBoxFor(m => m.Address, htmlAttributes: new { @class = "form-control" }) +
+ + + @Html.TextBox("Anon.Name") +
+ + + @Html.TextBox("Anon.Address.Street", (object)ViewBag.Anon.Address.Street) +
+ + + @Html.TextBox("Anon.Address.City", value: null, format: "{0} (3)") +
+ + + @Html.TextBox("Anon.Address.State", value: null, htmlAttributes: new { @class = "form-control" }) +
+ + + @Html.TextBox("Anon.Address.Country", value: null, format: "{0} (4)", + htmlAttributes: new { @class = "form-control" }) +
+ +
+
+ +
+
+
+
+
@await Component.InvokeAsync("Tags", 15)
diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs b/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs index fbecf88cde..47ba0034d9 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs @@ -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 /// /// Initializes a new instance of the class. /// - 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; } + /// /// 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; } + /// + 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) + /// + public HtmlString TextBox(string name, object value, string format, IDictionary 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 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
to attach them to). + protected IDictionary GetValidationAttributes(string name, ModelMetadata metadata) + { + // TODO: Add validation attributes to input helpers. + return new Dictionary(); + } + + protected virtual HtmlString GenerateTextBox(ModelMetadata metadata, string name, object value, string format, + IDictionary 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 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) { diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelperOfT.cs b/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelperOfT.cs index f87f91a438..eda6d32ac1 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelperOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelperOfT.cs @@ -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 /// /// Initializes a new instance of the class. /// - 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); } + /// + public HtmlString TextBoxFor([NotNull] Expression> expression, + string format, IDictionary htmlAttributes) + { + var metadata = GetModelMetadata(expression); + return GenerateTextBox(metadata, GetExpressionName(expression), metadata.Model, format, htmlAttributes); + } + protected string GetExpressionName([NotNull] Expression> expression) { return ExpressionHelper.GetExpressionText(expression); } + + protected ModelMetadata GetModelMetadata([NotNull] Expression> expression) + { + var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, MetadataProvider); + if (metadata == null) + { + var expressionName = GetExpressionName(expression); + throw new InvalidOperationException(Resources.FormatHtmlHelper_NullModelMetadata(expressionName)); + } + + return metadata; + } } } diff --git a/src/Microsoft.AspNet.Mvc.Rendering/HtmlHelperInputExtensions.cs b/src/Microsoft.AspNet.Mvc.Rendering/HtmlHelperInputExtensions.cs new file mode 100644 index 0000000000..38fcc1855c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/HtmlHelperInputExtensions.cs @@ -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([NotNull] this IHtmlHelper htmlHelper, string name) + { + return TextBox(htmlHelper, name, value: null); + } + + public static HtmlString TextBox([NotNull] this IHtmlHelper htmlHelper, string name, + object value) + { + return TextBox(htmlHelper, name, value, format: null); + } + + public static HtmlString TextBox([NotNull] this IHtmlHelper htmlHelper, string name, + object value, string format) + { + return TextBox(htmlHelper, name, value, format, htmlAttributes: null); + } + + public static HtmlString TextBox([NotNull] this IHtmlHelper htmlHelper, string name, + object value, object htmlAttributes) + { + return TextBox(htmlHelper, name, value, format: null, htmlAttributes: htmlAttributes); + } + + public static HtmlString TextBox([NotNull] this IHtmlHelper htmlHelper, string name, + object value, string format, object htmlAttributes) + { + return htmlHelper.TextBox(name, value, format, + HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static HtmlString TextBox([NotNull] this IHtmlHelper htmlHelper, string name, + object value, IDictionary htmlAttributes) + { + return htmlHelper.TextBox(name, value, format: null, htmlAttributes: htmlAttributes); + } + + public static HtmlString TextBoxFor([NotNull] this IHtmlHelper htmlHelper, + [NotNull] Expression> expression) + { + return TextBoxFor(htmlHelper, expression, format: null); + } + + public static HtmlString TextBoxFor([NotNull] this IHtmlHelper htmlHelper, + [NotNull] Expression> expression, string format) + { + return TextBoxFor(htmlHelper, expression, format, htmlAttributes: null); + } + + public static HtmlString TextBoxFor([NotNull] this IHtmlHelper htmlHelper, + [NotNull] Expression> expression, object htmlAttributes) + { + return TextBoxFor(htmlHelper, expression, format: null, htmlAttributes: htmlAttributes); + } + + public static HtmlString TextBoxFor([NotNull] this IHtmlHelper htmlHelper, + [NotNull] Expression> expression, string format, object htmlAttributes) + { + return htmlHelper.TextBoxFor(expression, format: format, + htmlAttributes: HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static HtmlString TextBoxFor([NotNull] this IHtmlHelper htmlHelper, + [NotNull] Expression> expression, IDictionary htmlAttributes) + { + return htmlHelper.TextBoxFor(expression, format: null, htmlAttributes: htmlAttributes); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Rendering/IHtmlHelperOfT.cs b/src/Microsoft.AspNet.Mvc.Rendering/IHtmlHelperOfT.cs index dd91b32d93..aaa2320562 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/IHtmlHelperOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/IHtmlHelperOfT.cs @@ -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 /// A task that represents when rendering has completed. Task RenderPartialAsync([NotNull] string partialViewName, object model, ViewDataDictionary viewData); + /// + /// Render an input element of type "text". + /// + /// + /// 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 is null. + /// + /// + /// If non-null, value to include in the element. Ignore if named value is found in submitted data. + /// + /// + /// + /// containing additional HTML attributes. + /// + /// New containing the rendered HTML. + HtmlString TextBox(string name, object value, string format, IDictionary htmlAttributes); + + /// + /// Render an input element of type "text". + /// + /// + /// An expression that identifies the object that contains the properties to render. + /// + /// + /// + /// containing additional HTML attributes. + /// + /// New containing the rendered HTML. + HtmlString TextBoxFor([NotNull] Expression> expression, string format, + IDictionary htmlAttributes); + /// /// Returns an unordered list (ul element) of validation messages that are in the object. /// diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.Designer.cs index ca111c1832..4c109eedc7 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.Designer.cs @@ -1,3 +1,20 @@ + + + + + + + + + + + + + + + + + // namespace Microsoft.AspNet.Mvc.Rendering { @@ -122,6 +139,22 @@ namespace Microsoft.AspNet.Mvc.Rendering return string.Format(CultureInfo.CurrentCulture, GetString("ExpressionHelper_InvalidIndexerExpression"), p0, p1); } + /// + /// The IModelMetadataProvider was unable to provide metadata for expression '{0}'. + /// + internal static string HtmlHelper_NullModelMetadata + { + get { return GetString("HtmlHelper_NullModelMetadata"); } + } + + /// + /// The IModelMetadataProvider was unable to provide metadata for expression '{0}'. + /// + internal static string FormatHtmlHelper_NullModelMetadata(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("HtmlHelper_NullModelMetadata"), p0); + } + /// /// Must call 'Contextualize' method before using this HtmlHelper instance. /// diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.resx b/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.resx index 7adfea7bc5..82726bc1a2 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.resx @@ -138,6 +138,9 @@ The expression compiler was unable to evaluate the indexer expression '{0}' because it references the model parameter '{1}' which is unavailable. + + The IModelMetadataProvider was unable to provide metadata for expression '{0}'. + Must call 'Contextualize' method before using this HtmlHelper instance.