// Copyright (c) .NET Foundation. 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; using System.Globalization; using System.Linq; using System.Reflection; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.ViewFeatures { public class DefaultHtmlGenerator : IHtmlGenerator { private const string HiddenListItem = @"
  • "; private static readonly MethodInfo ConvertEnumFromStringMethod = typeof(DefaultHtmlGenerator).GetTypeInfo().GetDeclaredMethod(nameof(ConvertEnumFromString)); // See: (http://www.w3.org/TR/html5/forms.html#the-input-element) private static readonly string[] _placeholderInputTypes = new[] { "text", "search", "url", "tel", "email", "password", "number" }; private readonly IAntiforgery _antiforgery; private readonly IClientModelValidatorProvider _clientModelValidatorProvider; private readonly IModelMetadataProvider _metadataProvider; private readonly IUrlHelperFactory _urlHelperFactory; private readonly HtmlEncoder _htmlEncoder; private readonly ClientValidatorCache _clientValidatorCache; /// /// Initializes a new instance of the class. /// /// The instance which is used to generate antiforgery /// tokens. /// The accessor for . /// The . /// The . /// The . /// The that provides /// a list of s. public DefaultHtmlGenerator( IAntiforgery antiforgery, IOptions optionsAccessor, IModelMetadataProvider metadataProvider, IUrlHelperFactory urlHelperFactory, HtmlEncoder htmlEncoder, ClientValidatorCache clientValidatorCache) { if (antiforgery == null) { throw new ArgumentNullException(nameof(antiforgery)); } if (optionsAccessor == null) { throw new ArgumentNullException(nameof(optionsAccessor)); } if (metadataProvider == null) { throw new ArgumentNullException(nameof(metadataProvider)); } if (urlHelperFactory == null) { throw new ArgumentNullException(nameof(urlHelperFactory)); } if (htmlEncoder == null) { throw new ArgumentNullException(nameof(htmlEncoder)); } if (clientValidatorCache == null) { throw new ArgumentNullException(nameof(clientValidatorCache)); } _antiforgery = antiforgery; var clientValidatorProviders = optionsAccessor.Value.ClientModelValidatorProviders; _clientModelValidatorProvider = new CompositeClientModelValidatorProvider(clientValidatorProviders); _metadataProvider = metadataProvider; _urlHelperFactory = urlHelperFactory; _htmlEncoder = htmlEncoder; _clientValidatorCache = clientValidatorCache; // Underscores are fine characters in id's. IdAttributeDotReplacement = optionsAccessor.Value.HtmlHelperOptions.IdAttributeDotReplacement; } /// public string IdAttributeDotReplacement { get; } /// public string Encode(string value) { return !string.IsNullOrEmpty(value) ? _htmlEncoder.Encode(value) : string.Empty; } /// public string Encode(object value) { return (value != null) ? _htmlEncoder.Encode(value.ToString()) : string.Empty; } /// public string FormatValue(object value, string format) { return ViewDataDictionary.FormatValue(value, format); } /// public virtual TagBuilder GenerateActionLink( ViewContext viewContext, string linkText, string actionName, string controllerName, string protocol, string hostname, string fragment, object routeValues, object htmlAttributes) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } if (linkText == null) { throw new ArgumentNullException(nameof(linkText)); } var urlHelper = _urlHelperFactory.GetUrlHelper(viewContext); var url = urlHelper.Action(actionName, controllerName, routeValues, protocol, hostname, fragment); return GenerateLink(linkText, url, htmlAttributes); } /// public virtual IHtmlContent GenerateAntiforgery(ViewContext viewContext) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } // If we're inside a BeginForm/BeginRouteForm, the antiforgery token might have already been // created and appended to the 'end form' content OR the form tag helper might have already generated // an antiforgery token. if (viewContext.FormContext.HasAntiforgeryToken) { return HtmlString.Empty; } viewContext.FormContext.HasAntiforgeryToken = true; return _antiforgery.GetHtml(viewContext.HttpContext); } /// public virtual TagBuilder GenerateCheckBox( ViewContext viewContext, ModelExplorer modelExplorer, string expression, bool? isChecked, object htmlAttributes) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } if (modelExplorer != null) { // CheckBoxFor() case. That API does not support passing isChecked directly. Debug.Assert(!isChecked.HasValue); if (modelExplorer.Model != null) { bool modelChecked; if (bool.TryParse(modelExplorer.Model.ToString(), out modelChecked)) { isChecked = modelChecked; } } } var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes); if (isChecked.HasValue && htmlAttributeDictionary != null) { // Explicit isChecked value must override "checked" in dictionary. htmlAttributeDictionary.Remove("checked"); } // Use ViewData only in CheckBox case (metadata null) and when the user didn't pass an isChecked value. return GenerateInput( viewContext, InputType.CheckBox, modelExplorer, expression, value: "true", useViewData: (modelExplorer == null && !isChecked.HasValue), isChecked: isChecked ?? false, setId: true, isExplicitValue: false, format: null, htmlAttributes: htmlAttributeDictionary); } /// public virtual TagBuilder GenerateHiddenForCheckbox( ViewContext viewContext, ModelExplorer modelExplorer, string expression) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } var tagBuilder = new TagBuilder("input"); tagBuilder.MergeAttribute("type", GetInputTypeString(InputType.Hidden)); tagBuilder.MergeAttribute("value", "false"); tagBuilder.TagRenderMode = TagRenderMode.SelfClosing; var fullName = GetFullHtmlFieldName(viewContext, expression); tagBuilder.MergeAttribute("name", fullName); return tagBuilder; } /// public virtual TagBuilder GenerateForm( ViewContext viewContext, string actionName, string controllerName, object routeValues, string method, object htmlAttributes) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } var defaultMethod = false; if (string.IsNullOrEmpty(method)) { defaultMethod = true; } else if (string.Equals(method, "post", StringComparison.OrdinalIgnoreCase)) { defaultMethod = true; } string action; if (actionName == null && controllerName == null && routeValues == null && defaultMethod) { // Submit to the original URL in the special case that user called the BeginForm() overload without // parameters (except for the htmlAttributes parameter). Also reachable in the even-more-unusual case // that user called another BeginForm() overload with default argument values. var request = viewContext.HttpContext.Request; action = request.PathBase + request.Path + request.QueryString; } else { var urlHelper = _urlHelperFactory.GetUrlHelper(viewContext); action = urlHelper.Action(action: actionName, controller: controllerName, values: routeValues); } return GenerateFormCore(viewContext, action, method, htmlAttributes); } /// public TagBuilder GenerateRouteForm( ViewContext viewContext, string routeName, object routeValues, string method, object htmlAttributes) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } var urlHelper = _urlHelperFactory.GetUrlHelper(viewContext); var action = urlHelper.RouteUrl(routeName, routeValues); return GenerateFormCore(viewContext, action, method, htmlAttributes); } /// public virtual TagBuilder GenerateHidden( ViewContext viewContext, ModelExplorer modelExplorer, string expression, object value, bool useViewData, object htmlAttributes) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } // Special-case opaque values and arbitrary binary data. var byteArrayValue = value as byte[]; if (byteArrayValue != null) { value = Convert.ToBase64String(byteArrayValue); } var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes); return GenerateInput( viewContext, InputType.Hidden, modelExplorer, expression, value, useViewData, isChecked: false, setId: true, isExplicitValue: true, format: null, htmlAttributes: htmlAttributeDictionary); } /// public virtual TagBuilder GenerateLabel( ViewContext viewContext, ModelExplorer modelExplorer, string expression, string labelText, object htmlAttributes) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } if (modelExplorer == null) { throw new ArgumentNullException(nameof(modelExplorer)); } var resolvedLabelText = labelText ?? modelExplorer.Metadata.DisplayName ?? modelExplorer.Metadata.PropertyName; if (resolvedLabelText == null) { resolvedLabelText = string.IsNullOrEmpty(expression) ? string.Empty : expression.Split('.').Last(); } if (string.IsNullOrEmpty(resolvedLabelText)) { return null; } var tagBuilder = new TagBuilder("label"); var idString = TagBuilder.CreateSanitizedId(GetFullHtmlFieldName(viewContext, expression), IdAttributeDotReplacement); tagBuilder.Attributes.Add("for", idString); tagBuilder.InnerHtml.SetContent(resolvedLabelText); tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes), replaceExisting: true); return tagBuilder; } /// public virtual TagBuilder GeneratePassword( ViewContext viewContext, ModelExplorer modelExplorer, string expression, object value, object htmlAttributes) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes); return GenerateInput( viewContext, InputType.Password, modelExplorer, expression, value, useViewData: false, isChecked: false, setId: true, isExplicitValue: true, format: null, htmlAttributes: htmlAttributeDictionary); } /// public virtual TagBuilder GenerateRadioButton( ViewContext viewContext, ModelExplorer modelExplorer, string expression, object value, bool? isChecked, object htmlAttributes) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes); if (modelExplorer == 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(nameof(value)); } // isChecked not provided nor found in the given attributes; fall back to view data. var valueString = Convert.ToString(value, CultureInfo.CurrentCulture); isChecked = string.Equals( EvalString(viewContext, expression), valueString, StringComparison.OrdinalIgnoreCase); } } else { // RadioButtonFor() case. That API does not support passing isChecked directly. Debug.Assert(!isChecked.HasValue); // Need a value to determine isChecked. Debug.Assert(value != null); var model = modelExplorer.Model; var valueString = Convert.ToString(value, CultureInfo.CurrentCulture); isChecked = model != null && string.Equals(model.ToString(), valueString, StringComparison.OrdinalIgnoreCase); } if (isChecked.HasValue && htmlAttributeDictionary != null) { // Explicit isChecked value must override "checked" in dictionary. htmlAttributeDictionary.Remove("checked"); } return GenerateInput( viewContext, InputType.Radio, modelExplorer, expression, value, useViewData: false, isChecked: isChecked ?? false, setId: true, isExplicitValue: true, format: null, htmlAttributes: htmlAttributeDictionary); } /// public virtual TagBuilder GenerateRouteLink( ViewContext viewContext, string linkText, string routeName, string protocol, string hostName, string fragment, object routeValues, object htmlAttributes) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } if (linkText == null) { throw new ArgumentNullException(nameof(linkText)); } var urlHelper = _urlHelperFactory.GetUrlHelper(viewContext); var url = urlHelper.RouteUrl(routeName, routeValues, protocol, hostName, fragment); return GenerateLink(linkText, url, htmlAttributes); } /// public TagBuilder GenerateSelect( ViewContext viewContext, ModelExplorer modelExplorer, string optionLabel, string expression, IEnumerable selectList, bool allowMultiple, object htmlAttributes) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } var currentValues = GetCurrentValues(viewContext, modelExplorer, expression, allowMultiple); return GenerateSelect( viewContext, modelExplorer, optionLabel, expression, selectList, currentValues, allowMultiple, htmlAttributes); } /// public virtual TagBuilder GenerateSelect( ViewContext viewContext, ModelExplorer modelExplorer, string optionLabel, string expression, IEnumerable selectList, ICollection currentValues, bool allowMultiple, object htmlAttributes) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } var fullName = GetFullHtmlFieldName(viewContext, expression); if (string.IsNullOrEmpty(fullName)) { throw new ArgumentException( Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty( typeof(IHtmlHelper).FullName, nameof(IHtmlHelper.Editor), typeof(IHtmlHelper<>).FullName, nameof(IHtmlHelper.EditorFor), "htmlFieldName"), nameof(expression)); } // If we got a null selectList, try to use ViewData to get the list of items. if (selectList == null) { selectList = GetSelectListItems(viewContext, expression); } modelExplorer = modelExplorer ?? ExpressionMetadataProvider.FromStringExpression(expression, viewContext.ViewData, _metadataProvider); // Convert each ListItem to an if requested. var listItemBuilder = GenerateGroupsAndOptions(optionLabel, selectList, currentValues); var tagBuilder = new TagBuilder("select"); tagBuilder.InnerHtml.SetHtmlContent(listItemBuilder); tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(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. ModelStateEntry entry; if (viewContext.ViewData.ModelState.TryGetValue(fullName, out entry)) { if (entry.Errors.Count > 0) { tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName); } } AddValidationAttributes(viewContext, tagBuilder, modelExplorer, expression); return tagBuilder; } /// public virtual TagBuilder GenerateTextArea( ViewContext viewContext, ModelExplorer modelExplorer, string expression, int rows, int columns, object htmlAttributes) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } if (rows < 0) { throw new ArgumentOutOfRangeException(nameof(rows), Resources.HtmlHelper_TextAreaParameterOutOfRange); } if (columns < 0) { throw new ArgumentOutOfRangeException( nameof(columns), Resources.HtmlHelper_TextAreaParameterOutOfRange); } var fullName = GetFullHtmlFieldName(viewContext, expression); if (string.IsNullOrEmpty(fullName)) { throw new ArgumentException( Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty( typeof(IHtmlHelper).FullName, nameof(IHtmlHelper.Editor), typeof(IHtmlHelper<>).FullName, nameof(IHtmlHelper.EditorFor), "htmlFieldName"), nameof(expression)); } ModelStateEntry entry; viewContext.ViewData.ModelState.TryGetValue(fullName, out entry); var value = string.Empty; if (entry != null && entry.AttemptedValue != null) { value = entry.AttemptedValue; } else if (modelExplorer.Model != null) { value = modelExplorer.Model.ToString(); } var tagBuilder = new TagBuilder("textarea"); tagBuilder.GenerateId(fullName, IdAttributeDotReplacement); tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(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); AddPlaceholderAttribute(viewContext.ViewData, tagBuilder, modelExplorer, expression); AddValidationAttributes(viewContext, tagBuilder, modelExplorer, expression); // If there are any errors for a named field, we add this CSS attribute. if (entry != null && entry.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.AppendLine(); tagBuilder.InnerHtml.Append(value); return tagBuilder; } /// public virtual TagBuilder GenerateTextBox( ViewContext viewContext, ModelExplorer modelExplorer, string expression, object value, string format, object htmlAttributes) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes); return GenerateInput( viewContext, InputType.Text, modelExplorer, expression, value, useViewData: (modelExplorer == null && value == null), isChecked: false, setId: true, isExplicitValue: true, format: format, htmlAttributes: htmlAttributeDictionary); } /// public virtual TagBuilder GenerateValidationMessage( ViewContext viewContext, ModelExplorer modelExplorer, string expression, string message, string tag, object htmlAttributes) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } var fullName = GetFullHtmlFieldName(viewContext, expression); if (string.IsNullOrEmpty(fullName)) { throw new ArgumentException( Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty( typeof(IHtmlHelper).FullName, nameof(IHtmlHelper.Editor), typeof(IHtmlHelper<>).FullName, nameof(IHtmlHelper.EditorFor), "htmlFieldName"), nameof(expression)); } var formContext = viewContext.ClientValidationEnabled ? viewContext.FormContext : null; if (!viewContext.ViewData.ModelState.ContainsKey(fullName) && formContext == null) { return null; } ModelStateEntry entry; var tryGetModelStateResult = viewContext.ViewData.ModelState.TryGetValue(fullName, out entry); var modelErrors = tryGetModelStateResult ? entry.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 tagBuilder = new TagBuilder(tag); tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(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. var className = (modelError != null) ? HtmlHelper.ValidationMessageCssClassName : HtmlHelper.ValidationMessageValidCssClassName; tagBuilder.AddCssClass(className); if (!string.IsNullOrEmpty(message)) { tagBuilder.InnerHtml.SetContent(message); } else if (modelError != null) { modelExplorer = modelExplorer ?? ExpressionMetadataProvider.FromStringExpression( expression, viewContext.ViewData, _metadataProvider); tagBuilder.InnerHtml.SetContent( ValidationHelpers.GetModelErrorMessageOrDefault(modelError, entry, modelExplorer)); } if (formContext != null) { tagBuilder.MergeAttribute("data-valmsg-for", fullName); var replaceValidationMessageContents = string.IsNullOrEmpty(message); tagBuilder.MergeAttribute("data-valmsg-replace", replaceValidationMessageContents.ToString().ToLowerInvariant()); } return tagBuilder; } /// public virtual TagBuilder GenerateValidationSummary( ViewContext viewContext, bool excludePropertyErrors, string message, string headerTag, object htmlAttributes) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } if (viewContext.ViewData.ModelState.IsValid && (!viewContext.ClientValidationEnabled || excludePropertyErrors)) { // No client side validation/updates return null; } var wrappedMessage = new HtmlContentBuilder(); if (!string.IsNullOrEmpty(message)) { if (string.IsNullOrEmpty(headerTag)) { headerTag = viewContext.ValidationSummaryMessageElement; } var messageTag = new TagBuilder(headerTag); messageTag.InnerHtml.SetContent(message); wrappedMessage.AppendLine(messageTag); } 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 isHtmlSummaryModified = false; var modelStates = ValidationHelpers.GetModelStateList(viewContext.ViewData, excludePropertyErrors); var htmlSummary = new TagBuilder("ul"); foreach (var modelState in modelStates) { // Perf: Avoid allocations for (var i = 0; i < modelState.Errors.Count; i++) { var modelError = modelState.Errors[i]; var errorText = ValidationHelpers.GetModelErrorMessageOrDefault(modelError); if (!string.IsNullOrEmpty(errorText)) { var listItem = new TagBuilder("li"); listItem.InnerHtml.SetContent(errorText); htmlSummary.InnerHtml.AppendLine(listItem); isHtmlSummaryModified = true; } } } if (!isHtmlSummaryModified) { htmlSummary.InnerHtml.AppendHtml(HiddenListItem); htmlSummary.InnerHtml.AppendLine(); } var tagBuilder = new TagBuilder("div"); tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes)); if (viewContext.ViewData.ModelState.IsValid) { tagBuilder.AddCssClass(HtmlHelper.ValidationSummaryValidCssClassName); } else { tagBuilder.AddCssClass(HtmlHelper.ValidationSummaryCssClassName); } tagBuilder.InnerHtml.AppendHtml(wrappedMessage); tagBuilder.InnerHtml.AppendHtml(htmlSummary); if (viewContext.ClientValidationEnabled && !excludePropertyErrors) { // Inform the client where to replace the list of property errors after validation. tagBuilder.MergeAttribute("data-valmsg-summary", "true"); } return tagBuilder; } /// public virtual ICollection GetCurrentValues( ViewContext viewContext, ModelExplorer modelExplorer, string expression, bool allowMultiple) { if (viewContext == null) { throw new ArgumentNullException(nameof(viewContext)); } var fullName = GetFullHtmlFieldName(viewContext, expression); if (string.IsNullOrEmpty(fullName)) { throw new ArgumentException( Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty( typeof(IHtmlHelper).FullName, nameof(IHtmlHelper.Editor), typeof(IHtmlHelper<>).FullName, nameof(IHtmlHelper.EditorFor), "htmlFieldName"), nameof(expression)); } var type = allowMultiple ? typeof(string[]) : typeof(string); var rawValue = GetModelStateValue(viewContext, fullName, type); // If ModelState did not contain a current value, fall back to ViewData- or ModelExplorer-supplied value. if (rawValue == null) { if (modelExplorer == null) { // Html.DropDownList() and Html.ListBox() helper case. rawValue = viewContext.ViewData.Eval(expression); if (rawValue is IEnumerable) { // This ViewData item contains the fallback selectList collection for GenerateSelect(). // Do not try to use this collection. rawValue = null; } } else { // element generated for a collection of SelectListItems would be useless.) var value = viewContext.ViewData.Eval(expression); // First check whether above evaluation was successful and did not match ViewData.Model. if (value == null || value == viewContext.ViewData.Model) { throw new InvalidOperationException(Resources.FormatHtmlHelper_MissingSelectData( $"IEnumerable<{nameof(SelectListItem)}>", expression)); } // Second check the Eval() call returned a collection of SelectListItems. var selectList = value as IEnumerable; if (selectList == null) { throw new InvalidOperationException(Resources.FormatHtmlHelper_WrongSelectDataType( expression, value.GetType().FullName, $"IEnumerable<{nameof(SelectListItem)}>")); } return selectList; } /// public IHtmlContent GenerateGroupsAndOptions(string optionLabel, IEnumerable selectList) { return GenerateGroupsAndOptions(optionLabel: optionLabel, selectList: selectList, currentValues: null); } private IHtmlContent GenerateGroupsAndOptions( string optionLabel, IEnumerable selectList, ICollection currentValues) { var listItemBuilder = new HtmlContentBuilder(); // Make optionLabel the first item that gets rendered. if (optionLabel != null) { listItemBuilder.AppendLine(GenerateOption( new SelectListItem() { Text = optionLabel, Value = string.Empty, Selected = false, }, currentValues: null)); } var itemsList = selectList as IList; if (itemsList == null) { itemsList = selectList.ToList(); } // Group items in the SelectList if requested. // The worst case complexity of this algorithm is O(number of groups*n). // If there aren't any groups, it is O(n) where n is number of items in the list. var optionGenerated = new bool[itemsList.Count]; for (var i = 0; i < itemsList.Count; i++) { if (!optionGenerated[i]) { var item = itemsList[i]; var optGroup = item.Group; if (optGroup != null) { var groupBuilder = new TagBuilder("optgroup"); if (optGroup.Name != null) { groupBuilder.MergeAttribute("label", optGroup.Name); } if (optGroup.Disabled) { groupBuilder.MergeAttribute("disabled", "disabled"); } groupBuilder.InnerHtml.AppendLine(); for (var j = i; j < itemsList.Count; j++) { var groupItem = itemsList[j]; if (!optionGenerated[j] && object.ReferenceEquals(optGroup, groupItem.Group)) { groupBuilder.InnerHtml.AppendLine(GenerateOption(groupItem, currentValues)); optionGenerated[j] = true; } } listItemBuilder.AppendLine(groupBuilder); } else { listItemBuilder.AppendLine(GenerateOption(item, currentValues)); optionGenerated[i] = true; } } } return listItemBuilder; } private IHtmlContent GenerateOption(SelectListItem item, ICollection currentValues) { var selected = item.Selected; if (currentValues != null) { var value = item.Value ?? item.Text; selected = currentValues.Contains(value); } var tagBuilder = GenerateOption(item, item.Text, selected); return tagBuilder; } } }