[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>
|
||||
{
|
||||
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 };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue