diff --git a/samples/MvcSample.Web/Models/User.cs b/samples/MvcSample.Web/Models/User.cs index 1cc9b93066..015a9ce00d 100644 --- a/samples/MvcSample.Web/Models/User.cs +++ b/samples/MvcSample.Web/Models/User.cs @@ -8,6 +8,7 @@ namespace MvcSample.Web.Models [MinLength(4)] public string Name { get; set; } public string Address { get; set; } + [Range(27, 70)] public int Age { get; set; } public decimal GPA { get; set; } public User Dependent { get; set; } diff --git a/samples/MvcSample.Web/Views/Home/Create.cshtml b/samples/MvcSample.Web/Views/Home/Create.cshtml index f07cba929a..b93085eba6 100644 --- a/samples/MvcSample.Web/Views/Home/Create.cshtml +++ b/samples/MvcSample.Web/Views/Home/Create.cshtml @@ -50,6 +50,9 @@ @Html.TextBox("Name") + + @Html.ValidationMessage("Name.Name", "Name is required", new { @style = "font-weight: bold" }) + @@ -66,6 +69,9 @@ @Html.DropDownListFor(model => model.Age, (IEnumerable)ViewBag.Ages, htmlAttributes: new { @class = "form-control" }) + + @Html.ValidationMessageFor(model => model.Age, "Age must be between 27 and 70", new { @style = "font-weight: bold" }) + diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs index e799eef1d7..ac65526509 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs @@ -416,6 +416,12 @@ namespace Microsoft.AspNet.Mvc.Rendering return new HtmlString(value == null ? null : value.ToString()); } + /// + public HtmlString ValidationMessage(string expression, string message, object htmlAttributes) + { + return GenerateValidationMessage(expression, message, htmlAttributes); + } + /// public virtual HtmlString ValidationSummary(bool excludePropertyErrors, string message, IDictionary htmlAttributes) { @@ -508,46 +514,6 @@ namespace Microsoft.AspNet.Mvc.Rendering return divBuilder.ToHtmlString(TagRenderMode.Normal); } - public HtmlString ValidationMessage(string name, string message, object htmlAttributes) - { - ModelState modelState; - ViewData.ModelState.TryGetValue(name, out modelState); - - ModelErrorCollection errors = null; - if (modelState != null) - { - errors = modelState.Errors; - } - - bool hasError = errors != null && errors.Any(); - if (!hasError && !ViewContext.UnobtrusiveJavaScriptEnabled) - { - return null; - } - else - { - string error = null; - if (hasError) - { - error = message ?? errors.First().ErrorMessage; - } - - var tagBuilder = new TagBuilder("span") { InnerHtml = Encode(error) }; - tagBuilder.MergeAttributes(AnonymousObjectToHtmlAttributes(htmlAttributes)); - - if (ViewContext.UnobtrusiveJavaScriptEnabled) - { - bool replaceValidationMessageContents = string.IsNullOrEmpty(message); - tagBuilder.MergeAttribute("data-valmsg-for", name); - tagBuilder.MergeAttribute("data-valmsg-replace", - replaceValidationMessageContents.ToString().ToLowerInvariant()); - } - - tagBuilder.AddCssClass(hasError ? ValidationMessageCssClassName : ValidationMessageValidCssClassName); - return tagBuilder.ToHtmlString(TagRenderMode.Normal); - } - } - /// /// Returns the HTTP method that handles form input (GET or POST) as a string. /// @@ -698,7 +664,7 @@ namespace Microsoft.AspNet.Mvc.Rendering var resolvedDisplayName = metadata.PropertyName; if (resolvedDisplayName == null) { - resolvedDisplayName = string.IsNullOrEmpty(htmlFieldName) ? + resolvedDisplayName = string.IsNullOrEmpty(htmlFieldName) ? string.Empty : htmlFieldName.Split('.').Last(); } @@ -823,7 +789,7 @@ namespace Microsoft.AspNet.Mvc.Rendering return new HtmlString(Encode(ViewData.TemplateInfo.GetFullHtmlFieldName(expression))); } - protected virtual HtmlString GenerateLabel([NotNull] ModelMetadata metadata, + protected virtual HtmlString GenerateLabel([NotNull] ModelMetadata metadata, string htmlFieldName, string labelText, object htmlAttributes) @@ -832,7 +798,7 @@ namespace Microsoft.AspNet.Mvc.Rendering string resolvedLabelText = labelText ?? metadata.PropertyName; if (resolvedLabelText == null) { - resolvedLabelText = string.IsNullOrEmpty(htmlFieldName) ? + resolvedLabelText = string.IsNullOrEmpty(htmlFieldName) ? string.Empty : htmlFieldName.Split('.').Last(); } @@ -1220,6 +1186,76 @@ namespace Microsoft.AspNet.Mvc.Rendering return tagBuilder.ToHtmlString(TagRenderMode.SelfClosing); } + protected virtual HtmlString GenerateValidationMessage(string expression, string message, + object htmlAttributes) + { + var modelName = ViewData.TemplateInfo.GetFullHtmlFieldName(expression); + if (string.IsNullOrEmpty(modelName)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "expression"); + } + + var formContext = ViewContext.GetFormContextForClientValidation(); + + if (!ViewData.ModelState.ContainsKey(modelName) && formContext == null) + { + return null; + } + + ModelState modelState; + var tryGetModelStateResult = ViewData.ModelState.TryGetValue(modelName, out modelState); + var modelErrors = tryGetModelStateResult ? modelState.Errors : null; + + ModelError modelError = null; + if(modelErrors != null && modelErrors.Count != 0) + { + modelError = modelErrors.FirstOrDefault(m => !string.IsNullOrEmpty(m.ErrorMessage)) ?? modelErrors[0]; + } + + if (modelError == null && formContext == null) + { + return null; + } + + // Even if there are no model errors, we generate the span and add the validation message + // if formContext is not null. + var builder = new TagBuilder("span"); + builder.MergeAttributes(AnonymousObjectToHtmlAttributes(htmlAttributes)); + + // Only the style of the span is changed according to the errors if message is null or empty. + // Otherwise the content and style is handled by the client-side validation. + builder.AddCssClass((modelError != null) ? + ValidationMessageCssClassName : + ValidationMessageValidCssClassName); + + if (!string.IsNullOrEmpty(message)) + { + builder.SetInnerText(message); + } + else if (modelError != null) + { + builder.SetInnerText(ValidationHelpers.GetUserErrorMessageOrDefault(modelError, modelState)); + } + + if (formContext != null) + { + var replaceValidationMessageContents = string.IsNullOrEmpty(message); + + if (ViewContext.UnobtrusiveJavaScriptEnabled) + { + builder.MergeAttribute("data-valmsg-for", modelName); + builder.MergeAttribute("data-valmsg-replace", + replaceValidationMessageContents.ToString().ToLowerInvariant()); + } + + // TODO: (WebFX-217) Add support for Unobtrusive JS disabled - + // Modify the field metadata to add the validation message, + // Add the client validation id in the field metadata + } + + return builder.ToHtmlString(TagRenderMode.Normal); + } + protected virtual HtmlString GenerateValue(string name, object value, string format, bool useViewData) { var fullName = ViewData.TemplateInfo.GetFullHtmlFieldName(name); diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelperOfT.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelperOfT.cs index 8141b33a2e..d1d7e3497f 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelperOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelperOfT.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.Rendering /// Initializes a new instance of the class. /// public HtmlHelper( - [NotNull] IViewEngine viewEngine, + [NotNull] IViewEngine viewEngine, [NotNull] IModelMetadataProvider metadataProvider, [NotNull] IUrlHelper urlHelper, [NotNull] AntiForgery antiForgeryInstance) @@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Mvc.Rendering } /// - public new ViewDataDictionary ViewData { get; private set;} + public new ViewDataDictionary ViewData { get; private set; } public override void Contextualize([NotNull] ViewContext viewContext) { @@ -30,7 +30,7 @@ namespace Microsoft.AspNet.Mvc.Rendering { throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull( "ViewData", - typeof(ViewContext)), + typeof(ViewContext)), "viewContext"); } @@ -74,7 +74,7 @@ namespace Microsoft.AspNet.Mvc.Rendering object additionalViewData) { var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, - ViewData, + ViewData, MetadataProvider); return GenerateDisplay(metadata, @@ -212,7 +212,14 @@ namespace Microsoft.AspNet.Mvc.Rendering } /// - public HtmlString ValueFor([NotNull] Expression> expression, string format) + public HtmlString ValidationMessageFor([NotNull] Expression> expression, + string message, object htmlAttributes) + { + return GenerateValidationMessage(ExpressionHelper.GetExpressionText(expression), message, htmlAttributes); + } + + /// + public HtmlString ValueFor(Expression> expression, string format) { var metadata = GetModelMetadata(expression); return GenerateValue(ExpressionHelper.GetExpressionText(expression), metadata.Model, format, diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlHelperValidationExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlHelperValidationExtensions.cs index cbef6ccb22..df70629fc5 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlHelperValidationExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/HtmlHelperValidationExtensions.cs @@ -1,9 +1,53 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; namespace Microsoft.AspNet.Mvc.Rendering { public static class HtmlHelperValidationExtensions { + public static HtmlString ValidationMessage([NotNull] this IHtmlHelper htmlHelper, + string expression) + { + return htmlHelper.ValidationMessage(expression, message: null, htmlAttributes: null); + } + + public static HtmlString ValidationMessage([NotNull] this IHtmlHelper htmlHelper, + string expression, string message) + { + return htmlHelper.ValidationMessage(expression, message, htmlAttributes: null); + } + + public static HtmlString ValidationMessage([NotNull] this IHtmlHelper htmlHelper, + string expression, object htmlAttributes) + { + return htmlHelper.ValidationMessage(expression, message: null, htmlAttributes: htmlAttributes); + } + + public static HtmlString ValidationMessage([NotNull] this IHtmlHelper htmlHelper, + string expression, string message, object htmlAttributes) + { + return htmlHelper.ValidationMessage(expression, message, htmlAttributes); + } + + public static HtmlString ValidationMessageFor([NotNull] this IHtmlHelper htmlHelper, + [NotNull] Expression> expression) + { + return htmlHelper.ValidationMessageFor(expression, message: null, htmlAttributes: null); + } + + public static HtmlString ValidationMessageFor([NotNull] this IHtmlHelper htmlHelper, + [NotNull] Expression> expression, string message) + { + return htmlHelper.ValidationMessageFor(expression, message, htmlAttributes: null); + } + + public static HtmlString ValidationMessageFor([NotNull] this IHtmlHelper htmlHelper, + [NotNull] Expression> expression, string message, object htmlAttributes) + { + return htmlHelper.ValidationMessageFor(expression, message, htmlAttributes); + } + public static HtmlString ValidationSummary([NotNull] this IHtmlHelper htmlHelper) { return ValidationSummary(htmlHelper, excludePropertyErrors: false); @@ -55,41 +99,5 @@ namespace Microsoft.AspNet.Mvc.Rendering return htmlHelper.ValidationSummary(excludePropertyErrors: false, message: message, htmlAttributes: htmlAttributes); } - - public static HtmlString ValidationMessage([NotNull] this IHtmlHelper htmlHelper, - string modelName) - { - return ValidationMessage(htmlHelper, modelName, message: null, htmlAttributes: null); - } - - public static HtmlString ValidationMessage([NotNull] this IHtmlHelper htmlHelper, - string modelName, string message) - { - return ValidationMessage(htmlHelper, modelName, message, htmlAttributes: null); - } - - public static HtmlString ValidationMessage([NotNull] this IHtmlHelper htmlHelper, - string modelName, object htmlAttributes) - { - return ValidationMessage(htmlHelper, modelName, message: null, htmlAttributes: htmlAttributes); - } - - public static HtmlString ValidationMessage([NotNull] this IHtmlHelper htmlHelper, - string modelName, IDictionary htmlAttributes) - { - return ValidationMessage(htmlHelper, modelName, message: null, htmlAttributes: htmlAttributes); - } - - public static HtmlString ValidationMessage([NotNull] this IHtmlHelper htmlHelper, - string modelName, string message, IDictionary htmlAttributes) - { - return htmlHelper.ValidationMessage(modelName, message, htmlAttributes); - } - - public static HtmlString ValidationMessage([NotNull] this IHtmlHelper htmlHelper, - string modelName, string message, object htmlAttributes) - { - return htmlHelper.ValidationMessage(modelName, message, htmlAttributes); - } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelper.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelper.cs index 80c37299ff..0891342236 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelper.cs @@ -393,6 +393,18 @@ namespace Microsoft.AspNet.Mvc.Rendering /// New containing the rendered HTML. HtmlString TextBox(string name, object value, string format, IDictionary htmlAttributes); + /// + /// Returns the validation message if an error exists in the object. + /// + /// The name of the property that is being validated. + /// The message to be displayed. This will always be visible but client-side + /// validation may update the associated CSS class. + /// An object that contains the HTML attributes to set for the element. + /// Alternatively, an instance containing the HTML attributes. + /// + /// An that contains the validation message + HtmlString ValidationMessage(string modelName, string message, object htmlAttributes); + /// /// Returns an unordered list (ul element) of validation messages that are in the /// object. diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelperOfT.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelperOfT.cs index d8ecfdd115..c7619f8614 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelperOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelperOfT.cs @@ -224,14 +224,17 @@ namespace Microsoft.AspNet.Mvc.Rendering IDictionary htmlAttributes); /// - /// Returns the validation message if an error exists in the object. + /// Returns the validation message for the specified expression /// - /// The name of the property that is being validated. - /// The message to be displayed if the specified field contains an error. - /// Dictionary that contains the HTML attributes which should - /// be applied on the element - /// - HtmlString ValidationMessage(string modelName, string message, object htmlAttributes); + /// An expression, relative to the current model. + /// The message to be displayed. This will always be visible but client-side + /// validation may update the associated CSS class. + /// An object that contains the HTML attributes to set for the element. + /// Alternatively, an /// instance containing the HTML attributes. + /// + /// An that contains the validation message + HtmlString ValidationMessageFor([NotNull] Expression> expression, + string message, object htmlAttributes); /// /// Returns the model value for the given expression . diff --git a/src/Microsoft.AspNet.Mvc.Core/ViewContext.cs b/src/Microsoft.AspNet.Mvc.Core/ViewContext.cs index 5c49b84f2b..f551a2cbe8 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ViewContext.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ViewContext.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.IO; -using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.Mvc.Rendering; namespace Microsoft.AspNet.Mvc @@ -82,5 +79,10 @@ namespace Microsoft.AspNet.Mvc public ViewDataDictionary ViewData { get; set; } public TextWriter Writer { get; set; } + + public FormContext GetFormContextForClientValidation() + { + return (ClientValidationEnabled) ? FormContext : null; + } } }