[Perf] Reduce SelectListItem and other allocations when generating HTML for select lists
Fixes #3953
This commit is contained in:
parent
30ace9f35a
commit
cba4d1dd0c
|
|
@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
public class MultiSelectList : IEnumerable<SelectListItem>
|
public class MultiSelectList : IEnumerable<SelectListItem>
|
||||||
{
|
{
|
||||||
private IList<SelectListGroup> _groups;
|
private IList<SelectListGroup> _groups;
|
||||||
|
private IList<SelectListItem> _selectListItems;
|
||||||
|
|
||||||
public MultiSelectList(IEnumerable items)
|
public MultiSelectList(IEnumerable items)
|
||||||
: this(items, selectedValues: null)
|
: this(items, selectedValues: null)
|
||||||
|
|
@ -111,10 +112,15 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
|
|
||||||
public virtual IEnumerator<SelectListItem> GetEnumerator()
|
public virtual IEnumerator<SelectListItem> GetEnumerator()
|
||||||
{
|
{
|
||||||
return GetListItems().GetEnumerator();
|
if (_selectListItems == null)
|
||||||
|
{
|
||||||
|
_selectListItems = GetListItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _selectListItems.GetEnumerator();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal IList<SelectListItem> GetListItems()
|
private IList<SelectListItem> GetListItems()
|
||||||
{
|
{
|
||||||
return (!string.IsNullOrEmpty(DataValueField)) ?
|
return (!string.IsNullOrEmpty(DataValueField)) ?
|
||||||
GetListItemsWithValueField() :
|
GetListItemsWithValueField() :
|
||||||
|
|
@ -126,20 +132,29 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
var selectedValues = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var selectedValues = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
if (SelectedValues != null)
|
if (SelectedValues != null)
|
||||||
{
|
{
|
||||||
selectedValues.UnionWith(from object value in SelectedValues
|
foreach (var value in SelectedValues)
|
||||||
select Convert.ToString(value, CultureInfo.CurrentCulture));
|
{
|
||||||
|
var stringValue = Convert.ToString(value, CultureInfo.CurrentCulture);
|
||||||
|
selectedValues.Add(stringValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var listItems = from object item in Items
|
var listItems = new List<SelectListItem>();
|
||||||
let value = Eval(item, DataValueField)
|
foreach (var item in Items)
|
||||||
select new SelectListItem
|
{
|
||||||
{
|
var value = Eval(item, DataValueField);
|
||||||
Group = GetGroup(item),
|
var newListItem = new SelectListItem
|
||||||
Value = value,
|
{
|
||||||
Text = Eval(item, DataTextField),
|
Group = GetGroup(item),
|
||||||
Selected = selectedValues.Contains(value)
|
Value = value,
|
||||||
};
|
Text = Eval(item, DataTextField),
|
||||||
return listItems.ToList();
|
Selected = selectedValues.Contains(value),
|
||||||
|
};
|
||||||
|
|
||||||
|
listItems.Add(newListItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return listItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IList<SelectListItem> GetListItemsWithoutValueField()
|
private IList<SelectListItem> GetListItemsWithoutValueField()
|
||||||
|
|
@ -150,14 +165,20 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
selectedValues.UnionWith(SelectedValues.Cast<object>());
|
selectedValues.UnionWith(SelectedValues.Cast<object>());
|
||||||
}
|
}
|
||||||
|
|
||||||
var listItems = from object item in Items
|
var listItems = new List<SelectListItem>();
|
||||||
select new SelectListItem
|
foreach (var item in Items)
|
||||||
{
|
{
|
||||||
Group = GetGroup(item),
|
var newListItem = new SelectListItem
|
||||||
Text = Eval(item, DataTextField),
|
{
|
||||||
Selected = selectedValues.Contains(item)
|
Group = GetGroup(item),
|
||||||
};
|
Text = Eval(item, DataTextField),
|
||||||
return listItems.ToList();
|
Selected = selectedValues.Contains(item),
|
||||||
|
};
|
||||||
|
|
||||||
|
listItems.Add(newListItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return listItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string Eval(object container, string expression)
|
private static string Eval(object container, string expression)
|
||||||
|
|
@ -187,7 +208,16 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
|
|
||||||
// We use StringComparison.CurrentCulture because the group name is used to display as the value of
|
// We use StringComparison.CurrentCulture because the group name is used to display as the value of
|
||||||
// optgroup HTML tag's label attribute.
|
// optgroup HTML tag's label attribute.
|
||||||
var group = _groups.FirstOrDefault(g => string.Equals(g.Name, groupName, StringComparison.CurrentCulture));
|
SelectListGroup group = null;
|
||||||
|
for (var index = 0; index < _groups.Count; index++)
|
||||||
|
{
|
||||||
|
if (string.Equals(_groups[index].Name, groupName, StringComparison.CurrentCulture))
|
||||||
|
{
|
||||||
|
group = _groups[index];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (group == null)
|
if (group == null)
|
||||||
{
|
{
|
||||||
group = new SelectListGroup() { Name = groupName };
|
group = new SelectListGroup() { Name = groupName };
|
||||||
|
|
|
||||||
|
|
@ -556,13 +556,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
|
|
||||||
modelExplorer = modelExplorer ??
|
modelExplorer = modelExplorer ??
|
||||||
ExpressionMetadataProvider.FromStringExpression(expression, viewContext.ViewData, _metadataProvider);
|
ExpressionMetadataProvider.FromStringExpression(expression, viewContext.ViewData, _metadataProvider);
|
||||||
if (currentValues != null)
|
|
||||||
{
|
|
||||||
selectList = UpdateSelectListItemsWithDefaultValue(modelExplorer, selectList, currentValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert each ListItem to an <option> tag and wrap them with <optgroup> if requested.
|
// Convert each ListItem to an <option> tag and wrap them with <optgroup> if requested.
|
||||||
var listItemBuilder = GenerateGroupsAndOptions(optionLabel, selectList);
|
var listItemBuilder = GenerateGroupsAndOptions(optionLabel, selectList, currentValues);
|
||||||
|
|
||||||
var tagBuilder = new TagBuilder("select");
|
var tagBuilder = new TagBuilder("select");
|
||||||
tagBuilder.InnerHtml.SetHtmlContent(listItemBuilder);
|
tagBuilder.InnerHtml.SetHtmlContent(listItemBuilder);
|
||||||
|
|
@ -1032,6 +1028,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
/// Not used directly in HtmlHelper. Exposed for use in DefaultDisplayTemplates.
|
/// Not used directly in HtmlHelper. Exposed for use in DefaultDisplayTemplates.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal static TagBuilder GenerateOption(SelectListItem item, string text)
|
internal static TagBuilder GenerateOption(SelectListItem item, string text)
|
||||||
|
{
|
||||||
|
return GenerateOption(item, text, item.Selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static TagBuilder GenerateOption(SelectListItem item, string text, bool selected)
|
||||||
{
|
{
|
||||||
var tagBuilder = new TagBuilder("option");
|
var tagBuilder = new TagBuilder("option");
|
||||||
tagBuilder.InnerHtml.SetContent(text);
|
tagBuilder.InnerHtml.SetContent(text);
|
||||||
|
|
@ -1041,7 +1042,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
tagBuilder.Attributes["value"] = item.Value;
|
tagBuilder.Attributes["value"] = item.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.Selected)
|
if (selected)
|
||||||
{
|
{
|
||||||
tagBuilder.Attributes["selected"] = "selected";
|
tagBuilder.Attributes["selected"] = "selected";
|
||||||
}
|
}
|
||||||
|
|
@ -1433,82 +1434,81 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
return selectList;
|
return selectList;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<SelectListItem> UpdateSelectListItemsWithDefaultValue(
|
|
||||||
ModelExplorer modelExplorer,
|
|
||||||
IEnumerable<SelectListItem> selectList,
|
|
||||||
ICollection<string> currentValues)
|
|
||||||
{
|
|
||||||
// Perform deep copy of selectList to avoid changing user's Selected property values.
|
|
||||||
var newSelectList = new List<SelectListItem>();
|
|
||||||
foreach (SelectListItem item in selectList)
|
|
||||||
{
|
|
||||||
var value = item.Value ?? item.Text;
|
|
||||||
var selected = currentValues.Contains(value);
|
|
||||||
var copy = new SelectListItem
|
|
||||||
{
|
|
||||||
Disabled = item.Disabled,
|
|
||||||
Group = item.Group,
|
|
||||||
Selected = selected,
|
|
||||||
Text = item.Text,
|
|
||||||
Value = item.Value,
|
|
||||||
};
|
|
||||||
|
|
||||||
newSelectList.Add(copy);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newSelectList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IHtmlContent GenerateGroupsAndOptions(string optionLabel, IEnumerable<SelectListItem> selectList)
|
public IHtmlContent GenerateGroupsAndOptions(string optionLabel, IEnumerable<SelectListItem> selectList)
|
||||||
|
{
|
||||||
|
return GenerateGroupsAndOptions(optionLabel: optionLabel, selectList: selectList, currentValues: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IHtmlContent GenerateGroupsAndOptions(
|
||||||
|
string optionLabel,
|
||||||
|
IEnumerable<SelectListItem> selectList,
|
||||||
|
ICollection<string> currentValues)
|
||||||
{
|
{
|
||||||
var listItemBuilder = new HtmlContentBuilder();
|
var listItemBuilder = new HtmlContentBuilder();
|
||||||
|
|
||||||
// Make optionLabel the first item that gets rendered.
|
// Make optionLabel the first item that gets rendered.
|
||||||
if (optionLabel != null)
|
if (optionLabel != null)
|
||||||
{
|
{
|
||||||
listItemBuilder.AppendLine(GenerateOption(new SelectListItem()
|
listItemBuilder.AppendLine(GenerateOption(
|
||||||
{
|
new SelectListItem()
|
||||||
Text = optionLabel,
|
{
|
||||||
Value = string.Empty,
|
Text = optionLabel,
|
||||||
Selected = false,
|
Value = string.Empty,
|
||||||
}));
|
Selected = false,
|
||||||
|
},
|
||||||
|
currentValues: null));
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemsList = selectList as IList<SelectListItem>;
|
||||||
|
if (itemsList == null)
|
||||||
|
{
|
||||||
|
itemsList = selectList.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group items in the SelectList if requested.
|
// Group items in the SelectList if requested.
|
||||||
// Treat each item with Group == null as a member of a unique group
|
// The worst case complexity of this algorithm is O(number of groups*n).
|
||||||
// so they are added according to the original order.
|
// If there aren't any groups, it is O(n) where n is number of items in the list.
|
||||||
var groupedSelectList = selectList.GroupBy<SelectListItem, int>(
|
var optionGenerated = new bool[itemsList.Count];
|
||||||
item => (item.Group == null) ? item.GetHashCode() : item.Group.GetHashCode());
|
for (var i = 0; i < itemsList.Count; i++)
|
||||||
foreach (var group in groupedSelectList)
|
|
||||||
{
|
{
|
||||||
var optGroup = group.First().Group;
|
if (!optionGenerated[i])
|
||||||
if (optGroup != null)
|
|
||||||
{
|
{
|
||||||
var groupBuilder = new TagBuilder("optgroup");
|
var item = itemsList[i];
|
||||||
if (optGroup.Name != null)
|
var optGroup = item.Group;
|
||||||
|
if (optGroup != null)
|
||||||
{
|
{
|
||||||
groupBuilder.MergeAttribute("label", optGroup.Name);
|
var groupBuilder = new TagBuilder("optgroup");
|
||||||
}
|
if (optGroup.Name != null)
|
||||||
|
{
|
||||||
|
groupBuilder.MergeAttribute("label", optGroup.Name);
|
||||||
|
}
|
||||||
|
|
||||||
if (optGroup.Disabled)
|
if (optGroup.Disabled)
|
||||||
{
|
{
|
||||||
groupBuilder.MergeAttribute("disabled", "disabled");
|
groupBuilder.MergeAttribute("disabled", "disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
groupBuilder.InnerHtml.AppendLine();
|
groupBuilder.InnerHtml.AppendLine();
|
||||||
foreach (var item in group)
|
|
||||||
{
|
|
||||||
groupBuilder.InnerHtml.AppendLine(GenerateOption(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
listItemBuilder.AppendLine(groupBuilder);
|
for (var j = i; j < itemsList.Count; j++)
|
||||||
}
|
{
|
||||||
else
|
var groupItem = itemsList[j];
|
||||||
{
|
|
||||||
foreach (var item in group)
|
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));
|
listItemBuilder.AppendLine(GenerateOption(item, currentValues));
|
||||||
|
optionGenerated[i] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1516,9 +1516,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
return listItemBuilder;
|
return listItemBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IHtmlContent GenerateOption(SelectListItem item)
|
private IHtmlContent GenerateOption(SelectListItem item, ICollection<string> currentValues)
|
||||||
{
|
{
|
||||||
var tagBuilder = GenerateOption(item, item.Text);
|
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;
|
return tagBuilder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue