[Perf] Reduce SelectListItem and other allocations when generating HTML for select lists

Fixes #3953
This commit is contained in:
mnltejaswini 2016-04-27 14:34:51 -07:00
parent 30ace9f35a
commit cba4d1dd0c
2 changed files with 126 additions and 89 deletions

View File

@ -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 };

View File

@ -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;
} }
} }