[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>
{
private IList<SelectListGroup> _groups;
private IList<SelectListItem> _selectListItems;
public MultiSelectList(IEnumerable items)
: this(items, selectedValues: null)
@ -111,10 +112,15 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
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)) ?
GetListItemsWithValueField() :
@ -126,20 +132,29 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
var selectedValues = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (SelectedValues != null)
{
selectedValues.UnionWith(from object value in SelectedValues
select Convert.ToString(value, CultureInfo.CurrentCulture));
foreach (var value in SelectedValues)
{
var stringValue = Convert.ToString(value, CultureInfo.CurrentCulture);
selectedValues.Add(stringValue);
}
}
var listItems = from object item in Items
let value = Eval(item, DataValueField)
select new SelectListItem
{
Group = GetGroup(item),
Value = value,
Text = Eval(item, DataTextField),
Selected = selectedValues.Contains(value)
};
return listItems.ToList();
var listItems = new List<SelectListItem>();
foreach (var item in Items)
{
var value = Eval(item, DataValueField);
var newListItem = new SelectListItem
{
Group = GetGroup(item),
Value = value,
Text = Eval(item, DataTextField),
Selected = selectedValues.Contains(value),
};
listItems.Add(newListItem);
}
return listItems;
}
private IList<SelectListItem> GetListItemsWithoutValueField()
@ -150,14 +165,20 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
selectedValues.UnionWith(SelectedValues.Cast<object>());
}
var listItems = from object item in Items
select new SelectListItem
{
Group = GetGroup(item),
Text = Eval(item, DataTextField),
Selected = selectedValues.Contains(item)
};
return listItems.ToList();
var listItems = new List<SelectListItem>();
foreach (var item in Items)
{
var newListItem = new SelectListItem
{
Group = GetGroup(item),
Text = Eval(item, DataTextField),
Selected = selectedValues.Contains(item),
};
listItems.Add(newListItem);
}
return listItems;
}
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
// 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)
{
group = new SelectListGroup() { Name = groupName };

View File

@ -556,13 +556,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
modelExplorer = modelExplorer ??
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.
var listItemBuilder = GenerateGroupsAndOptions(optionLabel, selectList);
var listItemBuilder = GenerateGroupsAndOptions(optionLabel, selectList, currentValues);
var tagBuilder = new TagBuilder("select");
tagBuilder.InnerHtml.SetHtmlContent(listItemBuilder);
@ -1032,6 +1028,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// Not used directly in HtmlHelper. Exposed for use in DefaultDisplayTemplates.
/// </remarks>
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");
tagBuilder.InnerHtml.SetContent(text);
@ -1041,7 +1042,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
tagBuilder.Attributes["value"] = item.Value;
}
if (item.Selected)
if (selected)
{
tagBuilder.Attributes["selected"] = "selected";
}
@ -1433,82 +1434,81 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
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 />
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();
// Make optionLabel the first item that gets rendered.
if (optionLabel != null)
{
listItemBuilder.AppendLine(GenerateOption(new SelectListItem()
{
Text = optionLabel,
Value = string.Empty,
Selected = false,
}));
listItemBuilder.AppendLine(GenerateOption(
new SelectListItem()
{
Text = optionLabel,
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.
// 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<SelectListItem, int>(
item => (item.Group == null) ? item.GetHashCode() : item.Group.GetHashCode());
foreach (var group in groupedSelectList)
// 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++)
{
var optGroup = group.First().Group;
if (optGroup != null)
if (!optionGenerated[i])
{
var groupBuilder = new TagBuilder("optgroup");
if (optGroup.Name != null)
var item = itemsList[i];
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)
{
groupBuilder.MergeAttribute("disabled", "disabled");
}
if (optGroup.Disabled)
{
groupBuilder.MergeAttribute("disabled", "disabled");
}
groupBuilder.InnerHtml.AppendLine();
foreach (var item in group)
{
groupBuilder.InnerHtml.AppendLine(GenerateOption(item));
}
groupBuilder.InnerHtml.AppendLine();
listItemBuilder.AppendLine(groupBuilder);
}
else
{
foreach (var item in group)
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));
listItemBuilder.AppendLine(GenerateOption(item, currentValues));
optionGenerated[i] = true;
}
}
}
@ -1516,9 +1516,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
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;
}
}