// 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; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Rendering.Expressions; namespace Microsoft.AspNet.Mvc.Rendering { /// /// Default implementation of non-generic portions of . /// public class HtmlHelper : IHtmlHelper, ICanHasViewContext { public static readonly string ValidationInputCssClassName = "input-validation-error"; public static readonly string ValidationInputValidCssClassName = "input-validation-valid"; public static readonly string ValidationMessageCssClassName = "field-validation-error"; public static readonly string ValidationMessageValidCssClassName = "field-validation-valid"; public static readonly string ValidationSummaryCssClassName = "validation-summary-errors"; public static readonly string ValidationSummaryValidCssClassName = "validation-summary-valid"; private const string HiddenListItem = @"
  • "; private readonly IUrlHelper _urlHelper; private readonly ICompositeViewEngine _viewEngine; private readonly AntiForgery _antiForgeryInstance; private readonly IActionBindingContextProvider _actionBindingContextProvider; private ViewContext _viewContext; /// /// Initializes a new instance of the class. /// public HtmlHelper( [NotNull] ICompositeViewEngine viewEngine, [NotNull] IModelMetadataProvider metadataProvider, [NotNull] IUrlHelper urlHelper, [NotNull] AntiForgery antiForgeryInstance, [NotNull] IActionBindingContextProvider actionBindingContextProvider) { _viewEngine = viewEngine; MetadataProvider = metadataProvider; _urlHelper = urlHelper; _antiForgeryInstance = antiForgeryInstance; _actionBindingContextProvider = actionBindingContextProvider; // Underscores are fine characters in id's. IdAttributeDotReplacement = "_"; } /// public Html5DateRenderingMode Html5DateRenderingMode { get; set; } /// public string IdAttributeDotReplacement { get; set; } /// public ViewContext ViewContext { get { if (_viewContext == null) { throw new InvalidOperationException(Resources.HtmlHelper_NotContextualized); } return _viewContext; } private set { _viewContext = value; } } /// public dynamic ViewBag { get { return ViewContext.ViewBag; } } /// public ViewDataDictionary ViewData { get { return ViewContext.ViewData; } } /// public IModelMetadataProvider MetadataProvider { get; private set; } /// public HtmlString ActionLink( [NotNull] string linkText, string actionName, string controllerName, string protocol, string hostname, string fragment, object routeValues, object htmlAttributes) { var url = _urlHelper.Action(actionName, controllerName, routeValues); return GenerateLink(linkText, url, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); } /// /// Creates a dictionary from an object, by adding each public instance property as a key with its associated /// value to the dictionary. It will expose public properties from derived types as well. This is typically /// used with objects of an anonymous type. /// /// If the object is already an instance, then it is /// returned as-is. /// /// /// new { property_name = "value" } will translate to the entry { "property_name" , "value" } /// in the resulting dictionary. /// /// The object to be converted. /// The created dictionary of property names and property values. public static IDictionary ObjectToDictionary(object obj) { return TypeHelper.ObjectToDictionary(obj); } /// /// Creates a dictionary of HTML attributes from the input object, /// translating underscores to dashes in each public instance property. /// /// If the object is already an instance, then it is /// returned as-is. /// /// new { data_name="value" } will translate to the entry { "data-name" , "value" } /// in the resulting dictionary. /// /// /// Anonymous object describing HTML attributes. /// A dictionary that represents HTML attributes. public static IDictionary AnonymousObjectToHtmlAttributes(object htmlAttributes) { var dictionary = htmlAttributes as IDictionary; if (dictionary != null) { return new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase); } dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); if (htmlAttributes != null) { foreach (var helper in HtmlAttributePropertyHelper.GetProperties(htmlAttributes)) { dictionary.Add(helper.Name, helper.GetValue(htmlAttributes)); } } return dictionary; } public virtual void Contextualize([NotNull] ViewContext viewContext) { ViewContext = viewContext; } /// public HtmlString AntiForgeryToken() { return _antiForgeryInstance.GetHtml(ViewContext.HttpContext); } /// public MvcForm BeginForm(string actionName, string controllerName, object routeValues, FormMethod method, object htmlAttributes) { // Only need a dictionary if htmlAttributes is non-null. TagBuilder.MergeAttributes() is fine with null. IDictionary htmlAttributeDictionary = null; if (htmlAttributes != null) { htmlAttributeDictionary = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes); } return GenerateForm(actionName, controllerName, routeValues, method, htmlAttributeDictionary); } /// public void EndForm() { var mvcForm = CreateForm(); mvcForm.EndForm(); } /// public HtmlString CheckBox(string name, bool? isChecked, object htmlAttributes) { return GenerateCheckBox(metadata: null, name: name, isChecked: isChecked, htmlAttributes: htmlAttributes); } /// public string Encode(string value) { return (!string.IsNullOrEmpty(value)) ? WebUtility.HtmlEncode(value) : string.Empty; } /// public string Encode(object value) { 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); } /// public HtmlString Display(string expression, string templateName, string htmlFieldName, object additionalViewData) { var metadata = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); return GenerateDisplay(metadata, htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), templateName, additionalViewData); } /// public HtmlString DisplayForModel(string templateName, string htmlFieldName, object additionalViewData) { return GenerateDisplay(ViewData.ModelMetadata, htmlFieldName, templateName, additionalViewData); } /// public HtmlString DisplayName(string expression) { var metadata = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); return GenerateDisplayName(metadata, expression); } /// public HtmlString DisplayText(string name) { var metadata = ExpressionMetadataProvider.FromStringExpression(name, ViewData, MetadataProvider); return GenerateDisplayText(metadata); } /// public HtmlString DropDownList(string name, IEnumerable selectList, string optionLabel, object htmlAttributes) { return GenerateDropDown( metadata: null, expression: name, selectList: selectList, optionLabel: optionLabel, htmlAttributes: htmlAttributes); } /// public HtmlString Editor(string expression, string templateName, string htmlFieldName, object additionalViewData) { var metadata = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); return GenerateEditor( metadata, htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), templateName, additionalViewData); } /// public HtmlString Hidden(string name, object value, object htmlAttributes) { return GenerateHidden(metadata: null, name: name, value: value, useViewData: (value == null), htmlAttributes: htmlAttributes); } /// public HtmlString Id(string name) { return GenerateId(name); } /// public HtmlString Label(string expression, string labelText, object htmlAttributes) { var metadata = ExpressionMetadataProvider.FromStringExpression(expression, ViewData, MetadataProvider); return GenerateLabel( metadata, expression, labelText, htmlAttributes); } /// public HtmlString ListBox(string name, IEnumerable selectList, object htmlAttributes) { return GenerateListBox(metadata: null, name: name, selectList: selectList, htmlAttributes: htmlAttributes); } /// public HtmlString Name(string name) { return GenerateName(name); } /// public async Task PartialAsync([NotNull] string partialViewName, object model, ViewDataDictionary viewData) { using (var writer = new StringWriter(CultureInfo.CurrentCulture)) { await RenderPartialCoreAsync(partialViewName, model, viewData, writer); return new HtmlString(writer.ToString()); } } /// public Task RenderPartialAsync([NotNull] string partialViewName, object model, ViewDataDictionary viewData) { return RenderPartialCoreAsync(partialViewName, model, viewData, ViewContext.Writer); } protected virtual HtmlString GenerateDisplay(ModelMetadata metadata, string htmlFieldName, string templateName, object additionalViewData) { var templateBuilder = new TemplateBuilder(_viewEngine, ViewContext, ViewData, metadata, htmlFieldName, templateName, readOnly: true, additionalViewData: additionalViewData); var templateResult = templateBuilder.Build(); return new HtmlString(templateResult); } protected virtual async Task RenderPartialCoreAsync([NotNull] string partialViewName, object model, ViewDataDictionary viewData, TextWriter writer) { // Determine which ViewData we should use to construct a new ViewData var baseViewData = viewData ?? ViewData; var newViewData = new ViewDataDictionary(baseViewData, model); var viewEngineResult = _viewEngine.FindPartialView(ViewContext, partialViewName); if (!viewEngineResult.Success) { var locations = string.Empty; if (viewEngineResult.SearchedLocations != null) { locations = Environment.NewLine + string.Join(Environment.NewLine, viewEngineResult.SearchedLocations); } throw new InvalidOperationException( Resources.FormatViewEngine_PartialViewNotFound(partialViewName, locations)); } var view = viewEngineResult.View; using (view as IDisposable) { var viewContext = new ViewContext(ViewContext, view, newViewData, writer); await viewEngineResult.View.RenderAsync(viewContext); } } /// public HtmlString Password(string name, object value, object htmlAttributes) { return GeneratePassword(metadata: null, name: name, value: value, htmlAttributes: htmlAttributes); } /// public HtmlString RadioButton(string name, object value, bool? isChecked, object htmlAttributes) { return GenerateRadioButton(metadata: null, name: name, value: value, isChecked: isChecked, htmlAttributes: htmlAttributes); } /// public HtmlString Raw(string value) { return new HtmlString(value); } /// public HtmlString Raw(object value) { return new HtmlString(value == null ? null : value.ToString()); } /// public HtmlString RouteLink( [NotNull] string linkText, string routeName, string protocol, string hostName, string fragment, object routeValues, object htmlAttributes) { var url = _urlHelper.RouteUrl(routeName, routeValues, protocol, hostName, fragment); return GenerateLink(linkText, url, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); } /// public HtmlString ValidationMessage(string expression, string message, object htmlAttributes, string tag) { return GenerateValidationMessage(expression, message, htmlAttributes, tag); } /// public HtmlString ValidationSummary(bool excludePropertyErrors, string message, IDictionary htmlAttributes, string tag) { return GenerateValidationSummary(excludePropertyErrors, message, htmlAttributes, tag); } /// /// Returns the HTTP method that handles form input (GET or POST) as a string. /// /// The HTTP method that handles the form. /// The form method string, either "get" or "post". public static string GetFormMethodString(FormMethod method) { switch (method) { case FormMethod.Get: return "get"; case FormMethod.Post: return "post"; default: return "post"; } } /// public HtmlString TextArea(string name, string value, int rows, int columns, object htmlAttributes) { var metadata = ExpressionMetadataProvider.FromStringExpression(name, ViewData, MetadataProvider); if (value != null) { metadata.Model = value; } return GenerateTextArea(metadata, name, rows, columns, htmlAttributes); } /// public HtmlString TextBox(string name, object value, string format, IDictionary htmlAttributes) { return GenerateTextBox(metadata: null, name: name, value: value, format: format, htmlAttributes: htmlAttributes); } /// public HtmlString Value([NotNull] string name, string format) { return GenerateValue(name, value: null, format: format, useViewData: true); } /// /// Override this method to return an subclass. That subclass may change /// behavior. /// /// A new instance. protected virtual MvcForm CreateForm() { return new MvcForm(ViewContext); } protected bool EvalBoolean(string key) { return Convert.ToBoolean(ViewData.Eval(key), CultureInfo.InvariantCulture); } protected string EvalString(string key) { return Convert.ToString(ViewData.Eval(key), CultureInfo.CurrentCulture); } 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; } // Only render attributes if client-side validation is enabled, and then only if we've // never rendered validation for a field with this name in this form. protected virtual IDictionary GetValidationAttributes(ModelMetadata metadata, string name) { var formContext = ViewContext.ClientValidationEnabled ? ViewContext.FormContext : null; if (formContext == null) { return null; } var fullName = ViewData.TemplateInfo.GetFullHtmlFieldName(name); if (formContext.RenderedField(fullName)) { return null; } formContext.RenderedField(fullName, true); var clientRules = GetClientValidationRules(metadata, name); return UnobtrusiveValidationAttributesGenerator.GetValidationAttributes(clientRules); } protected virtual HtmlString GenerateCheckBox(ModelMetadata metadata, string name, bool? isChecked, object htmlAttributes) { if (metadata != null) { // CheckBoxFor() case. That API does not support passing isChecked directly. Contract.Assert(!isChecked.HasValue); if (metadata.Model != null) { bool modelChecked; if (Boolean.TryParse(metadata.Model.ToString(), out modelChecked)) { isChecked = modelChecked; } } } // Only need a dictionary if htmlAttributes is non-null. TagBuilder.MergeAttributes() is fine with null. IDictionary htmlAttributeDictionary = null; if (htmlAttributes != null) { htmlAttributeDictionary = htmlAttributes as IDictionary; if (htmlAttributeDictionary == null) { htmlAttributeDictionary = AnonymousObjectToHtmlAttributes(htmlAttributes); } } var explicitValue = isChecked.HasValue; if (explicitValue && htmlAttributeDictionary != null) { // Explicit value must override dictionary htmlAttributeDictionary.Remove("checked"); } return GenerateInput(InputType.CheckBox, metadata, name, value: "true", useViewData: !explicitValue, isChecked: isChecked ?? false, setId: true, isExplicitValue: false, format: null, htmlAttributes: htmlAttributeDictionary); } protected virtual HtmlString GenerateDisplayName([NotNull] ModelMetadata metadata, string htmlFieldName) { // We don't call ModelMetadata.GetDisplayName here because // we want to fall back to the field name rather than the ModelType. // This is similar to how the GenerateLabel get the text of a label. var resolvedDisplayName = metadata.DisplayName ?? metadata.PropertyName; if (resolvedDisplayName == null) { resolvedDisplayName = string.IsNullOrEmpty(htmlFieldName) ? string.Empty : htmlFieldName.Split('.').Last(); } return new HtmlString(Encode(resolvedDisplayName)); } protected virtual HtmlString GenerateDisplayText(ModelMetadata metadata) { return new HtmlString(metadata.SimpleDisplayText); } protected HtmlString GenerateDropDown(ModelMetadata metadata, string expression, IEnumerable selectList, string optionLabel, object htmlAttributes) { return GenerateSelect(metadata, optionLabel, expression, selectList, allowMultiple: false, htmlAttributes: htmlAttributes); } protected virtual HtmlString GenerateEditor(ModelMetadata metadata, string htmlFieldName, string templateName, object additionalViewData) { var templateBuilder = new TemplateBuilder( _viewEngine, ViewContext, ViewData, metadata, htmlFieldName, templateName, readOnly: false, additionalViewData: additionalViewData); var templateResult = templateBuilder.Build(); return new HtmlString(templateResult); } /// /// Writes an opening
    tag to the response. When the user submits the form, /// the request will be processed by an action method. ///
    /// The name of the action method. /// The name of the controller. /// An object that contains the parameters for a route. The parameters are retrieved /// through reflection by examining the properties of the object. This object is typically created using object /// initializer syntax. Alternatively, an instance containing the /// route parameters. /// The HTTP method for processing the form, either GET or POST. /// An instance containing HTML /// attributes to set for the element. /// An instance which emits the closing {form} tag when disposed. protected virtual MvcForm GenerateForm(string actionName, string controllerName, object routeValues, FormMethod method, IDictionary htmlAttributes) { var tagBuilder = new TagBuilder("form"); tagBuilder.MergeAttributes(htmlAttributes); string formAction; if (actionName == null && controllerName == null && routeValues == null && method == FormMethod.Post && htmlAttributes == null) { // Submit to the original URL in the special case that user called the BeginForm() overload without // parameters. Also reachable in the even-more-unusual case that user called another BeginForm() // overload with default argument values. var request = ViewContext.HttpContext.Request; formAction = request.PathBase + request.Path + request.QueryString; } else { formAction = _urlHelper.Action(action: actionName, controller: controllerName, values: routeValues); } // action is implicitly generated, so htmlAttributes take precedence. tagBuilder.MergeAttribute("action", formAction); // method is an explicit parameter, so it takes precedence over the htmlAttributes. tagBuilder.MergeAttribute("method", HtmlHelper.GetFormMethodString(method), replaceExisting: true); ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag)); return CreateForm(); } protected virtual HtmlString GenerateHidden( ModelMetadata metadata, string name, object value, bool useViewData, object htmlAttributes) { // Only need a dictionary if htmlAttributes is non-null. TagBuilder.MergeAttributes() is fine with null. IDictionary htmlAttributeDictionary = null; if (htmlAttributes != null) { htmlAttributeDictionary = htmlAttributes as IDictionary; if (htmlAttributeDictionary == null) { htmlAttributeDictionary = AnonymousObjectToHtmlAttributes(htmlAttributes); } } // Special-case opaque values and arbitrary binary data. var byteArrayValue = value as byte[]; if (byteArrayValue != null) { value = Convert.ToBase64String(byteArrayValue); } return GenerateInput(InputType.Hidden, metadata, name, value, useViewData, isChecked: false, setId: true, isExplicitValue: true, format: null, htmlAttributes: htmlAttributeDictionary); } protected virtual HtmlString GenerateId(string expression) { return new HtmlString(Encode(ViewData.TemplateInfo.GetFullHtmlFieldName(expression))); } protected virtual HtmlString GenerateLabel([NotNull] ModelMetadata metadata, string htmlFieldName, string labelText, object htmlAttributes) { var resolvedLabelText = labelText ?? metadata.DisplayName ?? metadata.PropertyName; if (resolvedLabelText == null) { resolvedLabelText = string.IsNullOrEmpty(htmlFieldName) ? string.Empty : htmlFieldName.Split('.').Last(); } if (string.IsNullOrEmpty(resolvedLabelText)) { return HtmlString.Empty; } var tag = new TagBuilder("label"); tag.Attributes.Add( "for", TagBuilder.CreateSanitizedId( ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName), IdAttributeDotReplacement)); tag.SetInnerText(resolvedLabelText); tag.MergeAttributes(AnonymousObjectToHtmlAttributes(htmlAttributes), replaceExisting: true); return tag.ToHtmlString(TagRenderMode.Normal); } protected virtual HtmlString GenerateLink( [NotNull] string linkText, [NotNull] string url, IDictionary htmlAttributes) { var tagBuilder = new TagBuilder("a") { InnerHtml = WebUtility.HtmlEncode(linkText), }; tagBuilder.MergeAttributes(htmlAttributes); tagBuilder.MergeAttribute("href", url); return tagBuilder.ToHtmlString(TagRenderMode.Normal); } protected HtmlString GenerateListBox( ModelMetadata metadata, string name, IEnumerable selectList, object htmlAttributes) { return GenerateSelect( metadata, optionLabel: null, name: name, selectList: selectList, allowMultiple: true, htmlAttributes: htmlAttributes); } protected virtual HtmlString GenerateName(string name) { var fullName = ViewData.TemplateInfo.GetFullHtmlFieldName(name); return new HtmlString(Encode(fullName)); } protected virtual HtmlString GeneratePassword(ModelMetadata metadata, string name, object value, object htmlAttributes) { // Only need a dictionary if htmlAttributes is non-null. TagBuilder.MergeAttributes() is fine with null. IDictionary htmlAttributeDictionary = null; if (htmlAttributes != null) { htmlAttributeDictionary = htmlAttributes as IDictionary; if (htmlAttributeDictionary == null) { htmlAttributeDictionary = AnonymousObjectToHtmlAttributes(htmlAttributes); } } return GenerateInput(InputType.Password, metadata, name, value, useViewData: false, isChecked: false, setId: true, isExplicitValue: true, format: null, htmlAttributes: htmlAttributeDictionary); } protected virtual HtmlString GenerateRadioButton(ModelMetadata metadata, string name, object value, bool? isChecked, object htmlAttributes) { // Only need a dictionary if htmlAttributes is non-null. TagBuilder.MergeAttributes() is fine with null. IDictionary htmlAttributeDictionary = null; if (htmlAttributes != null) { htmlAttributeDictionary = htmlAttributes as IDictionary; if (htmlAttributeDictionary == null) { htmlAttributeDictionary = AnonymousObjectToHtmlAttributes(htmlAttributes); } } if (metadata == null) { // RadioButton() case. Do not override checked attribute if isChecked is implicit. if (!isChecked.HasValue && (htmlAttributeDictionary == null || !htmlAttributeDictionary.ContainsKey("checked"))) { // Note value may be null if isChecked is non-null. if (value == null) { throw new ArgumentNullException("value"); } // isChecked not provided nor found in the given attributes; fall back to view data. var valueString = Convert.ToString(value, CultureInfo.CurrentCulture); isChecked = !string.IsNullOrEmpty(name) && string.Equals(EvalString(name), valueString, StringComparison.OrdinalIgnoreCase); } } else { // RadioButtonFor() case. That API does not support passing isChecked directly. Contract.Assert(!isChecked.HasValue); if (value == null) { // Need a value to determine isChecked. throw new ArgumentNullException("value"); } var model = metadata.Model; var valueString = Convert.ToString(value, CultureInfo.CurrentCulture); isChecked = model != null && string.Equals(model.ToString(), valueString, StringComparison.OrdinalIgnoreCase); } var explicitValue = isChecked.HasValue; if (explicitValue && htmlAttributeDictionary != null) { // Explicit value must override dictionary htmlAttributeDictionary.Remove("checked"); } return GenerateInput(InputType.Radio, metadata, name, value, useViewData: false, isChecked: isChecked ?? false, setId: true, isExplicitValue: true, format: null, htmlAttributes: htmlAttributeDictionary); } protected virtual HtmlString GenerateSelect(ModelMetadata metadata, string optionLabel, string name, IEnumerable selectList, bool allowMultiple, object htmlAttributes) { var fullName = ViewData.TemplateInfo.GetFullHtmlFieldName(name); if (string.IsNullOrEmpty(fullName)) { throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "name"); } var usedViewData = false; // If we got a null selectList, try to use ViewData to get the list of items. if (selectList == null) { if (string.IsNullOrEmpty(name)) { // Avoid ViewData.Eval() throwing an ArgumentException with a different parameter name. Note this // is an extreme case since users must pass a non-null selectList to use CheckBox() or ListBox() // in a template, where a null or empty name has meaning. throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "name"); } selectList = GetSelectListItems(name); usedViewData = true; } var defaultValue = (allowMultiple) ? GetModelStateValue(fullName, typeof(string[])) : GetModelStateValue(fullName, typeof(string)); // If we haven't already used ViewData to get the entire list of items then we need to // use the ViewData-supplied value before using the parameter-supplied value. if (defaultValue == null && !string.IsNullOrEmpty(name)) { if (!usedViewData) { defaultValue = ViewData.Eval(name); } else if (metadata != null) { defaultValue = metadata.Model; } } if (defaultValue != null) { selectList = UpdateSelectListItemsWithDefaultValue(selectList, defaultValue, allowMultiple); } // Convert each ListItem to an if requested. var listItemBuilder = GenerateGroupsAndOptions(optionLabel, selectList); var tagBuilder = new TagBuilder("select") { InnerHtml = listItemBuilder.ToString() }; tagBuilder.MergeAttributes(AnonymousObjectToHtmlAttributes(htmlAttributes)); tagBuilder.MergeAttribute("name", fullName, true /* replaceExisting */); tagBuilder.GenerateId(fullName, IdAttributeDotReplacement); if (allowMultiple) { tagBuilder.MergeAttribute("multiple", "multiple"); } // If there are any errors for a named field, we add the css attribute. ModelState modelState; if (ViewData.ModelState.TryGetValue(fullName, out modelState)) { if (modelState.Errors.Count > 0) { tagBuilder.AddCssClass(ValidationInputCssClassName); } } tagBuilder.MergeAttributes(GetValidationAttributes(metadata, name)); return tagBuilder.ToHtmlString(TagRenderMode.Normal); } protected virtual HtmlString GenerateTextArea(ModelMetadata metadata, string name, int rows, int columns, object htmlAttributes) { if (rows < 0) { throw new ArgumentOutOfRangeException("rows", Resources.HtmlHelper_TextAreaParameterOutOfRange); } if (columns < 0) { throw new ArgumentOutOfRangeException("columns", Resources.HtmlHelper_TextAreaParameterOutOfRange); } var fullName = ViewData.TemplateInfo.GetFullHtmlFieldName(name); if (string.IsNullOrEmpty(fullName)) { throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "name"); } ModelState modelState; ViewData.ModelState.TryGetValue(fullName, out modelState); var value = string.Empty; if (modelState != null && modelState.Value != null) { value = modelState.Value.AttemptedValue; } else if (metadata.Model != null) { value = metadata.Model.ToString(); } var tagBuilder = new TagBuilder("textarea"); tagBuilder.GenerateId(fullName, IdAttributeDotReplacement); tagBuilder.MergeAttributes(AnonymousObjectToHtmlAttributes(htmlAttributes), true); if (rows > 0) { tagBuilder.MergeAttribute("rows", rows.ToString(CultureInfo.InvariantCulture), true); } if (columns > 0) { tagBuilder.MergeAttribute("columns", columns.ToString(CultureInfo.InvariantCulture), true); } tagBuilder.MergeAttribute("name", fullName, true); tagBuilder.MergeAttributes(GetValidationAttributes(metadata, name)); // If there are any errors for a named field, we add this CSS attribute. if (modelState != null && modelState.Errors.Count > 0) { tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName); } // The first newline is always trimmed when a TextArea is rendered, so we add an extra one // in case the value being rendered is something like "\r\nHello". tagBuilder.InnerHtml = WebUtility.HtmlEncode(value); return tagBuilder.ToHtmlString(TagRenderMode.Normal); } 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() and so on 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.ArgumentCannotBeNullOrEmpty, "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); var usedModelState = false; switch (inputType) { case InputType.CheckBox: var modelStateWasChecked = GetModelStateValue(fullName, typeof(bool)) as bool?; if (modelStateWasChecked.HasValue) { isChecked = modelStateWasChecked.Value; usedModelState = true; } goto case InputType.Radio; case InputType.Radio: if (!usedModelState) { var modelStateValue = GetModelStateValue(fullName, typeof(string)) as string; if (modelStateValue != null) { isChecked = string.Equals(modelStateValue, valueParameter, StringComparison.Ordinal); usedModelState = true; } } if (!usedModelState && useViewData) { isChecked = EvalBoolean(fullName); } if (isChecked) { tagBuilder.MergeAttribute("checked", "checked"); } tagBuilder.MergeAttribute("value", valueParameter, isExplicitValue); break; case InputType.Password: if (value != null) { tagBuilder.MergeAttribute("value", valueParameter, isExplicitValue); } break; 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(metadata, name)); if (inputType == InputType.CheckBox) { // Generate an additional for checkboxes. This // addresses scenarios where unchecked checkboxes are not sent in the request. // Sending a hidden input makes it possible to know that the checkbox was present // on the page when the request was submitted. var inputItemBuilder = new StringBuilder(); inputItemBuilder.Append(tagBuilder.ToString(TagRenderMode.SelfClosing)); var hiddenInput = new TagBuilder("input"); hiddenInput.MergeAttribute("type", GetInputTypeString(InputType.Hidden)); hiddenInput.MergeAttribute("name", fullName); hiddenInput.MergeAttribute("value", "false"); inputItemBuilder.Append(hiddenInput.ToString(TagRenderMode.SelfClosing)); return new HtmlString(inputItemBuilder.ToString()); } return tagBuilder.ToHtmlString(TagRenderMode.SelfClosing); } protected virtual HtmlString GenerateValidationMessage(string expression, string message, object htmlAttributes, string tag) { var modelName = ViewData.TemplateInfo.GetFullHtmlFieldName(expression); if (string.IsNullOrEmpty(modelName)) { throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "expression"); } var formContext = ViewContext.ClientValidationEnabled ? ViewContext.FormContext : null; 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. if (string.IsNullOrEmpty(tag)) { tag = ViewContext.ValidationMessageElement; } var builder = new TagBuilder(tag); 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) { builder.MergeAttribute("data-valmsg-for", modelName); var replaceValidationMessageContents = string.IsNullOrEmpty(message); builder.MergeAttribute("data-valmsg-replace", replaceValidationMessageContents.ToString().ToLowerInvariant()); } return builder.ToHtmlString(TagRenderMode.Normal); } protected virtual HtmlString GenerateValidationSummary( bool excludePropertyErrors, string message, IDictionary htmlAttributes, string tag) { var formContext = ViewContext.ClientValidationEnabled ? ViewContext.FormContext : null; if (ViewData.ModelState.IsValid && (formContext == null || excludePropertyErrors)) { // No client side validation/updates return HtmlString.Empty; } string wrappedMessage; if (!string.IsNullOrEmpty(message)) { if (string.IsNullOrEmpty(tag)) { tag = ViewContext.ValidationSummaryMessageElement; } var messageTag = new TagBuilder(tag); messageTag.SetInnerText(message); wrappedMessage = messageTag.ToString(TagRenderMode.Normal) + Environment.NewLine; } else { wrappedMessage = null; } // If excludePropertyErrors is true, describe any validation issue with the current model in a single item. // Otherwise, list individual property errors. var htmlSummary = new StringBuilder(); var modelStates = ValidationHelpers.GetModelStateList(ViewData, excludePropertyErrors); foreach (var modelState in modelStates) { foreach (var modelError in modelState.Errors) { var errorText = ValidationHelpers.GetUserErrorMessageOrDefault(modelError, modelState: null); if (!string.IsNullOrEmpty(errorText)) { var listItem = new TagBuilder("li"); listItem.SetInnerText(errorText); htmlSummary.AppendLine(listItem.ToString(TagRenderMode.Normal)); } } } if (htmlSummary.Length == 0) { htmlSummary.AppendLine(HiddenListItem); } var unorderedList = new TagBuilder("ul") { InnerHtml = htmlSummary.ToString() }; var divBuilder = new TagBuilder("div"); divBuilder.MergeAttributes(htmlAttributes); if (ViewData.ModelState.IsValid) { divBuilder.AddCssClass(HtmlHelper.ValidationSummaryValidCssClassName); } else { divBuilder.AddCssClass(HtmlHelper.ValidationSummaryCssClassName); } divBuilder.InnerHtml = wrappedMessage + unorderedList.ToString(TagRenderMode.Normal); if (formContext != null && !excludePropertyErrors) { // Inform the client where to replace the list of property errors after validation. divBuilder.MergeAttribute("data-valmsg-summary", "true"); } return divBuilder.ToHtmlString(TagRenderMode.Normal); } protected virtual HtmlString GenerateValue(string name, object value, string format, bool useViewData) { var fullName = ViewData.TemplateInfo.GetFullHtmlFieldName(name); var attemptedValue = (string)GetModelStateValue(fullName, typeof(string)); string resolvedValue; if (attemptedValue != null) { // case 1: if ModelState has a value then it's already formatted so ignore format string resolvedValue = attemptedValue; } else if (useViewData) { if (name.Length == 0) { // case 2(a): format the value from ModelMetadata for the current model var metadata = ViewData.ModelMetadata; resolvedValue = FormatValue(metadata.Model, format); } else { // case 2(b): format the value from ViewData resolvedValue = EvalString(name, format); } } else { // case 3: format the explicit value from ModelMetadata resolvedValue = FormatValue(value, format); } return new HtmlString(Encode(resolvedValue)); } /// public IEnumerable GetClientValidationRules( ModelMetadata metadata, string name) { var actionBindingContext = _actionBindingContextProvider.GetActionBindingContextAsync(ViewContext).Result; metadata = metadata ?? ExpressionMetadataProvider.FromStringExpression(name, ViewData, MetadataProvider); return actionBindingContext.ValidatorProviders .SelectMany(vp => vp.GetValidators(metadata)) .OfType() .SelectMany(v => v.GetClientValidationRules( new ClientModelValidationContext(metadata, MetadataProvider))); } private static string GetInputTypeString(InputType inputType) { switch (inputType) { case InputType.CheckBox: return "checkbox"; case InputType.Hidden: return "hidden"; case InputType.Password: return "password"; case InputType.Radio: return "radio"; case InputType.Text: return "text"; default: return "text"; } } private StringBuilder GenerateGroupsAndOptions(string optionLabel, IEnumerable selectList) { var listItemBuilder = new StringBuilder(); // Make optionLabel the first item that gets rendered. if (optionLabel != null) { listItemBuilder.AppendLine(GenerateOption(new SelectListItem() { Text = optionLabel, Value = string.Empty, Selected = false, })); } // Group items in the SelectList if requested. // Treat each item with Group == null as a member of a unique group // so they are added according to the original order. var groupedSelectList = selectList.GroupBy( item => (item.Group == null) ? item.GetHashCode() : item.Group.GetHashCode()); foreach (var group in groupedSelectList) { var optGroup = group.First().Group; // Wrap if requested. TagBuilder groupBuilder = null; if (optGroup != null) { groupBuilder = new TagBuilder("optgroup"); if (optGroup.Name != null) { groupBuilder.MergeAttribute("label", optGroup.Name); } if (optGroup.Disabled) { groupBuilder.MergeAttribute("disabled", "disabled"); } listItemBuilder.AppendLine(groupBuilder.ToString(TagRenderMode.StartTag)); } foreach (var item in group) { listItemBuilder.AppendLine(GenerateOption(item)); } if (optGroup != null) { listItemBuilder.AppendLine(groupBuilder.ToString(TagRenderMode.EndTag)); } } return listItemBuilder; } private string GenerateOption(SelectListItem item) { var encodedText = Encode(item.Text); return GenerateOption(item, encodedText); } internal static string GenerateOption(SelectListItem item, string encodedText) { var builder = new TagBuilder("option") { InnerHtml = encodedText, }; if (item.Value != null) { builder.Attributes["value"] = item.Value; } if (item.Selected) { builder.Attributes["selected"] = "selected"; } if (item.Disabled) { builder.Attributes["disabled"] = "disabled"; } return builder.ToString(TagRenderMode.Normal); } private IEnumerable GetSelectListItems(string name) { var value = ViewData.Eval(name); if (value == null) { throw new InvalidOperationException(Resources.FormatHtmlHelper_MissingSelectData( "IEnumerable", name)); } var selectList = value as IEnumerable; if (selectList == null) { throw new InvalidOperationException(Resources.FormatHtmlHelper_WrongSelectDataType( name, value.GetType().FullName, "IEnumerable")); } return selectList; } private IEnumerable UpdateSelectListItemsWithDefaultValue( IEnumerable selectList, object defaultValue, bool allowMultiple) { IEnumerable defaultValues; if (allowMultiple) { defaultValues = defaultValue as IEnumerable; if (defaultValues == null || defaultValues is string) { throw new InvalidOperationException( Resources.FormatHtmlHelper_SelectExpressionNotEnumerable("expression")); } } else { defaultValues = new[] { defaultValue }; } var values = from object value in defaultValues select Convert.ToString(value, CultureInfo.CurrentCulture); // ToString() by default returns an enum value's name. But selectList may use numeric values. var enumValues = from Enum value in defaultValues.OfType() select value.ToString("d"); values = values.Concat(enumValues); var selectedValues = new HashSet(values, StringComparer.OrdinalIgnoreCase); var newSelectList = new List(); foreach (SelectListItem item in selectList) { item.Selected = (item.Value != null) ? selectedValues.Contains(item.Value) : selectedValues.Contains(item.Text); newSelectList.Add(item); } return newSelectList; } } }