From 0f6702b5ed24c2e6b4f7832a048699054101425b Mon Sep 17 00:00:00 2001 From: Zonciu Liang Date: Sun, 28 Feb 2021 00:13:13 +0800 Subject: [PATCH] refactor(module: select): use Func to get or set value instead of reflection (#1168) * refactor(module: select): use Func to get or set value instead of reflection * refactor(module: select): add delegate cache * chore: add DelegateCacheKeyComparer Co-authored-by: James Yeung --- components/select/Select.razor.cs | 106 ++++++--- .../internal/SelectItemPropertyHelper.cs | 224 ++++++++++++++++++ 2 files changed, 302 insertions(+), 28 deletions(-) create mode 100644 components/select/internal/SelectItemPropertyHelper.cs diff --git a/components/select/Select.razor.cs b/components/select/Select.razor.cs index 17957d2b..55147c28 100644 --- a/components/select/Select.razor.cs +++ b/components/select/Select.razor.cs @@ -38,10 +38,32 @@ namespace AntDesign } } [Parameter] public bool Disabled { get; set; } - [Parameter] public string DisabledName { get; set; } + + [Parameter] + public string DisabledName + { + get => _disabledName; + set + { + _getDisabled = string.IsNullOrWhiteSpace(value) ? null : SelectItemPropertyHelper.CreateGetDisabledFunc(value); + _disabledName = value; + } + } + [Parameter] public Func DropdownRender { get; set; } [Parameter] public bool EnableSearch { get; set; } - [Parameter] public string GroupName { get; set; } = string.Empty; + + [Parameter] + public string GroupName + { + get => _groupName; + set + { + _getGroup = string.IsNullOrWhiteSpace(value) ? null : SelectItemPropertyHelper.CreateGetGroupFunc(value); + _groupName = value; + } + } + [Parameter] public bool HideSelected { get; set; } [Parameter] public bool IgnoreItemChanges { get; set; } = true; [Parameter] public RenderFragment ItemTemplate { get; set; } @@ -51,7 +73,18 @@ namespace AntDesign /// [Parameter] public bool LabelInValue { get; set; } - [Parameter] public string LabelName { get; set; } + [Parameter] + public string LabelName + { + get => _labelName; + set + { + _getLabel = SelectItemPropertyHelper.CreateGetLabelFunc(value); + _setLabel = SelectItemPropertyHelper.CreateSetLabelFunc(value); + _labelName = value; + } + } + [Parameter] public RenderFragment LabelTemplate { get; set; } [Parameter] public bool Loading { get; set; } [Parameter] public string Mode { get; set; } = "default"; @@ -86,7 +119,19 @@ namespace AntDesign [Parameter] public RenderFragment PrefixIcon { get; set; } [Parameter] public char[] TokenSeparators { get; set; } [Parameter] public override EventCallback ValueChanged { get; set; } - [Parameter] public string ValueName { get; set; } + + [Parameter] + public string ValueName + { + get => _valueName; + set + { + _getValue = SelectItemPropertyHelper.CreateGetValueFunc(value); + _setValue = SelectItemPropertyHelper.CreateSetValueFunc(value); + _valueName = value; + } + } + [Parameter] public EventCallback> ValuesChanged { get; set; } /// @@ -340,7 +385,28 @@ namespace AntDesign } } + private string _labelName; + + private Func _getLabel; + + private Action _setLabel; + + private string _groupName = string.Empty; + + private Func _getGroup; + + private string _disabledName; + + private Func _getDisabled; + + private string _valueName; + + private Func _getValue; + + private Action _setValue; + #endregion Properties + private static bool IsSimpleType(Type type) { return @@ -462,7 +528,7 @@ namespace AntDesign foreach (var item in _datasource) { - TItemValue value = GetPropertyValueAsTItemValue(item, ValueName); + TItemValue value = _getValue(item); var exists = false; SelectOptionItem selectOption; @@ -481,13 +547,13 @@ namespace AntDesign var disabled = false; var groupName = string.Empty; - var label = GetPropertyValueAsObject(item, LabelName)?.ToString(); + var label = _getLabel(item); if (!string.IsNullOrWhiteSpace(DisabledName)) - disabled = (bool)GetPropertyValueAsObject(item, DisabledName); + disabled = _getDisabled(item); if (!string.IsNullOrWhiteSpace(GroupName)) - groupName = GetPropertyValueAsObject(item, GroupName)?.ToString(); + groupName = _getGroup(item); if (!exists) { @@ -1001,8 +1067,8 @@ namespace AntDesign else { item = Activator.CreateInstance(); - typeof(TItem).GetProperty(LabelName).SetValue(item, _searchValue); - typeof(TItem).GetProperty(ValueName).SetValue(item, value); + _setLabel(item, _searchValue); + _setValue(item, value); } return new SelectOptionItem() { Label = label, Value = value, Item = item, IsActive = isActive, IsSelected = false, IsAddedTag = true }; } @@ -1086,22 +1152,6 @@ namespace AntDesign await _dropDown.GetOverlayComponent().UpdatePosition(); } - private static object GetPropertyValueAsObject(object obj, string propertyName) - { - return obj.GetType().GetProperties() - .Single(p => p.Name == propertyName) - .GetValue(obj, null); - } - - private static TItemValue GetPropertyValueAsTItemValue(object obj, string propertyName) - { - var result = obj.GetType().GetProperties() - .Single(p => p.Name == propertyName) - .GetValue(obj, null); - - return (TItemValue)TypeDescriptor.GetConverter(typeof(TItemValue)).ConvertFromInvariantString(result.ToString()); - } - #region Events /// @@ -1398,8 +1448,8 @@ namespace AntDesign } else { - typeof(TItem).GetProperty(LabelName).SetValue(CustomTagSelectOptionItem.Item, _searchValue); - typeof(TItem).GetProperty(ValueName).SetValue(CustomTagSelectOptionItem.Item, value); + _setLabel(CustomTagSelectOptionItem.Item, _searchValue); + _setValue(CustomTagSelectOptionItem.Item, value); } } } diff --git a/components/select/internal/SelectItemPropertyHelper.cs b/components/select/internal/SelectItemPropertyHelper.cs new file mode 100644 index 00000000..a1a532d1 --- /dev/null +++ b/components/select/internal/SelectItemPropertyHelper.cs @@ -0,0 +1,224 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace AntDesign.Select.Internal +{ + internal static class SelectItemPropertyHelper + { + private const string ToStringMethodName = "ToString"; + + private const string Nullable_HasValue = "HasValue"; + + private const string Nullable_Value = "Value"; + + private static readonly ConcurrentDictionary _getValueDelegateCache = new(new DelegateCacheKeyComparer()); + + private static readonly ConcurrentDictionary _setValuedDelegateCache = new(new DelegateCacheKeyComparer()); + + internal static Func CreateGetValueFunc(string valueName) + { + var key = new DelegateCacheKey(typeof(TItem), typeof(TItemValue), valueName); + var func = _getValueDelegateCache.GetOrAdd( + key, + _ => + { + var itemParamExp = Expression.Parameter(typeof(TItem)); + var labelExp = Expression.Property(itemParamExp, valueName); + var labelPropType = (labelExp.Member as PropertyInfo)?.PropertyType ?? throw new InvalidOperationException($"Member '{valueName}' should be Property"); + if (labelPropType.IsClass) + { + var funExp = Expression.Lambda>(labelExp, itemParamExp); + return funExp.Compile(); + } + + var labelIsNullable = labelPropType.IsValueType && Nullable.GetUnderlyingType(labelPropType) != null; + if (labelIsNullable) + { + var test = Expression.IsTrue(Expression.Property(labelExp, Nullable_HasValue)); + var trueResult = Expression.Convert(Expression.Property(labelExp, Nullable_Value), labelExp.Type); + var falseResult = Expression.Constant(null, labelExp.Type); + var resultExp = Expression.Condition(test, trueResult, falseResult); + var funExp = Expression.Lambda>(resultExp, itemParamExp); + return funExp.Compile(); + } + else + { + var valueIsNullable = typeof(TItemValue).IsValueType && Nullable.GetUnderlyingType(typeof(TItemValue)) != null; + Expression valueExp = valueIsNullable ? Expression.New(typeof(Nullable<>).MakeGenericType(labelExp.Type).GetConstructors()[0], labelExp) : labelExp; + var funExp = Expression.Lambda>(valueExp, itemParamExp); + return funExp.Compile(); + } + }); + return (Func)func; + } + + internal static Func CreateGetLabelFunc(string labelName) + { + var key = new DelegateCacheKey(typeof(TItem), typeof(string), labelName); + var func = _getValueDelegateCache.GetOrAdd( + key, + _ => + { + var itemParamExp = Expression.Parameter(typeof(TItem)); + var labelExp = Expression.Property(itemParamExp, labelName); + var labelPropType = (labelExp.Member as PropertyInfo)?.PropertyType ?? throw new InvalidOperationException($"Member '{labelName}' should be Property"); + if (labelPropType == typeof(string)) + { + var funExp = Expression.Lambda>(labelExp, itemParamExp); + return funExp.Compile(); + } + else + { + var resultExp = Expression.Call(labelExp, labelPropType.GetMethod(ToStringMethodName) ?? throw new MissingMethodException(labelPropType.Name, ToStringMethodName)); + var funExp = Expression.Lambda>(resultExp, itemParamExp); + return funExp.Compile(); + } + }); + return (Func)func; + } + + internal static Func CreateGetGroupFunc(string groupName) + { + var key = new DelegateCacheKey(typeof(TItem), typeof(string), groupName); + var func = _getValueDelegateCache.GetOrAdd( + key, + _ => + { + var itemParamExp = Expression.Parameter(typeof(TItem)); + var groupExp = Expression.Property(itemParamExp, groupName); + var groupPropType = (groupExp.Member as PropertyInfo)?.PropertyType ?? throw new InvalidOperationException($"Member '{groupName}' should be Property"); + if (groupPropType == typeof(string)) + { + var funExp = Expression.Lambda>(groupExp, itemParamExp); + return funExp.Compile(); + } + else + { + var resultExp = Expression.Call(groupExp, groupPropType.GetMethod(ToStringMethodName) ?? throw new MissingMethodException(groupPropType.Name, ToStringMethodName)); + var funExp = Expression.Lambda>(resultExp, itemParamExp); + return funExp.Compile(); + } + }); + return (Func)func; + } + + internal static Func CreateGetDisabledFunc(string disabledName) + { + var key = new DelegateCacheKey(typeof(TItem), typeof(bool), disabledName); + var func = _getValueDelegateCache.GetOrAdd( + key, + _ => + { + var itemParamExp = Expression.Parameter(typeof(TItem)); + var disabledExp = Expression.Property(itemParamExp, disabledName); + var disabledPropType = (disabledExp.Member as PropertyInfo)?.PropertyType ?? throw new InvalidOperationException($"Member '{disabledName}' should be Property"); + if (disabledPropType == typeof(bool)) + { + var funExp = Expression.Lambda>(disabledExp, itemParamExp); + return funExp.Compile(); + } + else + { + throw new InvalidOperationException($"Member '{disabledName}' should be type of 'bool'"); + } + }); + return (Func)func; + } + + internal static Action CreateSetLabelFunc(string labelName) + { + var key = new DelegateCacheKey(typeof(TItem), typeof(string), labelName); + var action = _setValuedDelegateCache.GetOrAdd( + key, + _ => + { + var itemParamExp = Expression.Parameter(typeof(TItem)); + var labelParamExp = Expression.Parameter(typeof(string)); + var labelExp = Expression.Property(itemParamExp, labelName); + var setLabelExp = Expression.Assign(labelExp, labelParamExp); + var funExp = Expression.Lambda>(setLabelExp, itemParamExp, labelParamExp); + return funExp.Compile(); + }); + return (Action)action; + } + + internal static Action CreateSetValueFunc(string valueName) + { + var key = new DelegateCacheKey(typeof(TItem), typeof(TItemValue), valueName); + var action = _setValuedDelegateCache.GetOrAdd( + key, + _ => + { + var itemParamExp = Expression.Parameter(typeof(TItem)); + var valueParamExp = Expression.Parameter(typeof(TItemValue)); + var valueExp = Expression.Property(itemParamExp, valueName); + var valuePropType = (valueExp.Member as PropertyInfo)?.PropertyType ?? throw new InvalidOperationException($"Member '{valueName}' should be Property"); + if (valuePropType.IsClass) + { + var setLabelExp = Expression.Assign(valueExp, valueParamExp); + var funExp = Expression.Lambda>(setLabelExp, itemParamExp, valueParamExp); + return funExp.Compile(); + } + + var valueIsNullable = valuePropType.IsValueType && Nullable.GetUnderlyingType(valuePropType) != null; + var valueParamIsNullable = valueParamExp.Type.IsValueType && Nullable.GetUnderlyingType(valueParamExp.Type) != null; + if (valueIsNullable) + { + Expression nullableValueExp = valueParamIsNullable + ? valueParamExp + : Expression.New(valuePropType.GetConstructors()[0], valueParamExp); + var setLabelExp = Expression.Assign(valueExp, nullableValueExp); + var funExp = Expression.Lambda>(setLabelExp, itemParamExp, valueParamExp); + return funExp.Compile(); + } + else + { + var setLabelExp = valueParamIsNullable + ? Expression.Assign(valueExp, Expression.Property(valueParamExp, Nullable_Value)) + : Expression.Assign(valueExp, valueParamExp); + var funExp = Expression.Lambda>(setLabelExp, itemParamExp, valueParamExp); + return funExp.Compile(); + } + }); + return (Action)action; + } + + private readonly struct DelegateCacheKey + { + public readonly Type ItemType; + + public readonly Type ValueType; + + public readonly string PropertyName; + + public DelegateCacheKey(Type itemType, Type valueType, string propertyName) + { + ItemType = itemType; + ValueType = valueType; + PropertyName = propertyName; + } + } + + private class DelegateCacheKeyComparer : IEqualityComparer + { + public bool Equals(DelegateCacheKey x, DelegateCacheKey y) + { + return x.ItemType == y.ItemType + && x.ValueType == y.ValueType + && x.PropertyName == y.PropertyName; + } + + public int GetHashCode(DelegateCacheKey obj) + { + return HashCode.Combine(obj.ItemType, obj.ValueType, obj.PropertyName); + } + } + } +}