From d33bffcba957cec67616d715ce47c75ae1dd7194 Mon Sep 17 00:00:00 2001 From: MomchilSt Date: Wed, 8 Nov 2023 18:20:45 +0200 Subject: [PATCH 1/2] refactor autocomplete to be flexible and reusable --- .../Controllers/AutoCrudAdminController.cs | 129 +++++++++++++----- .../ViewModels/FormControlViewModel.cs | 10 ++ .../Shared/_EnityFormControlsPartial.cshtml | 6 +- 3 files changed, 107 insertions(+), 38 deletions(-) diff --git a/src/AutoCrudAdmin/Controllers/AutoCrudAdminController.cs b/src/AutoCrudAdmin/Controllers/AutoCrudAdminController.cs index 1e0f27f..059ced6 100644 --- a/src/AutoCrudAdmin/Controllers/AutoCrudAdminController.cs +++ b/src/AutoCrudAdmin/Controllers/AutoCrudAdminController.cs @@ -216,32 +216,70 @@ public override void OnActionExecuting(ActionExecutingContext context) /// Handles autocomplete searches. /// /// The search term. - /// The property to search. + /// The property to search is configured by FormControlViewModel. + /// The max autocomplete result is configured by FormControlViewModel. /// The autocomplete results. [HttpGet] - public virtual IEnumerable Autocomplete([FromQuery] string searchTerm, string searchProperty) + public IEnumerable Autocomplete( + [FromQuery] string searchTerm, + string searchProperty, + int maxResults) { + if (string.IsNullOrWhiteSpace(searchProperty)) + { + throw new ArgumentException("No search property added!"); + } + var entityType = typeof(TEntity); var searchedProperty = entityType.GetProperty(searchProperty); - var idProperty = entityType.GetProperty("Id"); - if (searchedProperty == null || idProperty == null) + var keys = entityType.GetPrimaryKeyPropertyInfos(); + + if (searchedProperty == null || !keys.Any()) { throw new ArgumentException("No such property exists on the entity!"); } - var entities = this.Set - .AsNoTracking() - .Where(e => EF.Property(e, searchProperty).Contains(searchTerm)) - .Select(x => new DropDownViewModel + var parameter = Expression.Parameter(typeof(TEntity), "e"); + var propertyAccess = Expression.Property(parameter, searchedProperty); + var searchTermExpression = Expression.Constant(searchTerm, typeof(string)); + + Expression body; + + if (searchedProperty.PropertyType == typeof(string)) + { + // If the property type is string, use Contains method + var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) }); + if (containsMethod != null) { - Value = idProperty.GetValue(x) !.ToString() !, - Name = searchedProperty.GetValue(x) !.ToString() !, - }) - .Take(20) - .ToList(); + body = Expression.Call(propertyAccess, containsMethod, searchTermExpression); + var lambda = Expression.Lambda>(body, parameter); + return this.FilterAutocompleteResults(maxResults, searchedProperty, keys, lambda); + } + } + else + { + // If the property type is not string, convert it to string and then use Contains method + var toStringMethod = typeof(object).GetMethod("ToString"); + MethodCallExpression? toStringCall = null; + if (toStringMethod != null) + { + toStringCall = Expression.Call(propertyAccess, toStringMethod); + } + + var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) }); + var toStringToLowerMethod = typeof(string).GetMethod("ToLower", Type.EmptyTypes); + if (containsMethod != null && toStringToLowerMethod != null) + { + var searchTermToLowerExpression = Expression.Call(searchTermExpression, toStringToLowerMethod); + var toStringToLowerCall = Expression.Call(toStringCall, toStringToLowerMethod); + body = Expression.Call(toStringToLowerCall, containsMethod, searchTermToLowerExpression); + var lambda = Expression.Lambda>(body, parameter); + return this.FilterAutocompleteResults(maxResults, searchedProperty, keys, lambda); + } + } - return entities; + return new List(); } /// @@ -370,33 +408,33 @@ protected virtual async Task GetEntityForm( switch (action) { case EntityAction.Create: - { - var shownFormControls = this.ShownFormControlNames.Concat(this.ShownFormControlNamesOnCreate); - var hiddenFormControls = this.HiddenFormControlNames.Concat(this.HiddenFormControlNamesOnCreate); - formControls = SetFormControlsVisibility(formControls, shownFormControls, hiddenFormControls) - .ToList(); - break; - } + { + var shownFormControls = this.ShownFormControlNames.Concat(this.ShownFormControlNamesOnCreate); + var hiddenFormControls = this.HiddenFormControlNames.Concat(this.HiddenFormControlNamesOnCreate); + formControls = SetFormControlsVisibility(formControls, shownFormControls, hiddenFormControls) + .ToList(); + break; + } case EntityAction.Edit: - { - var shownFormControls = this.ShownFormControlNames.Concat(this.ShownFormControlNamesOnEdit); - var hiddenFormControls = this.HiddenFormControlNames.Concat(this.HiddenFormControlNamesOnEdit); - formControls = SetFormControlsVisibility(formControls, shownFormControls, hiddenFormControls) - .ToList(); - break; - } + { + var shownFormControls = this.ShownFormControlNames.Concat(this.ShownFormControlNamesOnEdit); + var hiddenFormControls = this.HiddenFormControlNames.Concat(this.HiddenFormControlNamesOnEdit); + formControls = SetFormControlsVisibility(formControls, shownFormControls, hiddenFormControls) + .ToList(); + break; + } case EntityAction.Delete: - { - formControls.ForEach(fc => fc.IsReadOnly = true); - break; - } + { + formControls.ForEach(fc => fc.IsReadOnly = true); + break; + } default: - { - throw new ArgumentOutOfRangeException(nameof(action), action, null); - } + { + throw new ArgumentOutOfRangeException(nameof(action), action, null); + } } return this.View( @@ -720,7 +758,7 @@ protected IActionResult RedirectToActionWithStringFilter( actionName, gridStringFilterType.ToString()); - /// + /// /// Builds custom grid columns. /// /// Page columns. @@ -836,6 +874,25 @@ private static void CopyFormPropertiesToExistingEntityFromNewEntity(TEntity exis .ToList() .ForEach(property => property.SetValue(existingEntity, property.GetValue(newEntity, null), null)); + private IEnumerable FilterAutocompleteResults(int maxResults, PropertyInfo searchedProperty, IEnumerable keys, Expression> lambda) + { + var entities = this.Set + .AsNoTracking() + .Where(lambda) + .Take(maxResults) + .ToList(); + + return entities + .Select(x => new DropDownViewModel + { + Value = keys!.Count() == 1 + ? keys.First().GetValue(x) !.ToString() ! + : keys.Select(k => k.GetValue(x) !.ToString()), + Name = searchedProperty.GetValue(x) !.ToString() !, + }) + .ToList(); + } + private IHtmlGrid GenerateGrid(IHtmlHelper htmlHelper) => htmlHelper .Grid(this.GetQueryWithIncludes(this.MasterGridFilter)) diff --git a/src/AutoCrudAdmin/ViewModels/FormControlViewModel.cs b/src/AutoCrudAdmin/ViewModels/FormControlViewModel.cs index 9de97d7..83ee13e 100644 --- a/src/AutoCrudAdmin/ViewModels/FormControlViewModel.cs +++ b/src/AutoCrudAdmin/ViewModels/FormControlViewModel.cs @@ -82,4 +82,14 @@ public string DisplayName /// Gets or sets the entity id for the autocomplete feature of the form control. /// public string? FormControlAutocompleteEntityId { get; set; } + + /// + /// Gets or sets the entity property that autocomplete will search by. + /// + public string? SearchByPropertyAutocompleteConfiguration { get; set; } + + /// + /// Gets or sets how much autocomplete results will be shown (20 by default). + /// + public int NumberOfAutocompleteItemsShownConfiguration { get; set; } = 20; } \ No newline at end of file diff --git a/src/AutoCrudAdmin/Views/Shared/_EnityFormControlsPartial.cshtml b/src/AutoCrudAdmin/Views/Shared/_EnityFormControlsPartial.cshtml index 5c8c657..9aa48b2 100644 --- a/src/AutoCrudAdmin/Views/Shared/_EnityFormControlsPartial.cshtml +++ b/src/AutoCrudAdmin/Views/Shared/_EnityFormControlsPartial.cshtml @@ -73,8 +73,10 @@ type: "GET", url: `/${controllerName}/Autocomplete`, - data: { - searchTerm: request.term + data: { + searchTerm: request.term, + searchProperty: '@formControl.SearchByPropertyAutocompleteConfiguration', + maxResults: @formControl.NumberOfAutocompleteItemsShownConfiguration, }, success: function (data) { response($.map(data, function (item) { From fbdc85233aa872a724bf2701224b7fac775fc842 Mon Sep 17 00:00:00 2001 From: MomchilSt Date: Fri, 24 Nov 2023 23:20:20 +0200 Subject: [PATCH 2/2] rewrite autocomplete extended --- .../Controllers/AutoCrudAdminController.cs | 127 +++++++++--------- .../Enumerations/FormControlType.cs | 5 + .../Extensions/TypeExtensions.cs | 12 ++ .../Helpers/ExpressionsBuilder.cs | 36 +++++ .../TagHelpers/FormInputTagHelper.cs | 2 +- .../ViewModels/FormControlViewModel.cs | 4 +- .../_AutocompleteExtendedPartial.cshtml | 44 ++++++ .../Autocomplete/_AutocompletePartial.cshtml | 42 ++++++ .../Shared/_EnityFormControlsPartial.cshtml | 45 +------ 9 files changed, 207 insertions(+), 110 deletions(-) create mode 100644 src/AutoCrudAdmin/Views/Shared/Autocomplete/_AutocompleteExtendedPartial.cshtml create mode 100644 src/AutoCrudAdmin/Views/Shared/Autocomplete/_AutocompletePartial.cshtml diff --git a/src/AutoCrudAdmin/Controllers/AutoCrudAdminController.cs b/src/AutoCrudAdmin/Controllers/AutoCrudAdminController.cs index 059ced6..a7de897 100644 --- a/src/AutoCrudAdmin/Controllers/AutoCrudAdminController.cs +++ b/src/AutoCrudAdmin/Controllers/AutoCrudAdminController.cs @@ -216,11 +216,44 @@ public override void OnActionExecuting(ActionExecutingContext context) /// Handles autocomplete searches. /// /// The search term. + /// The property to search. + /// The autocomplete results. + [HttpGet] + [Obsolete] + public virtual IEnumerable Autocomplete([FromQuery] string searchTerm, string searchProperty) + { + var entityType = typeof(TEntity); + var searchedProperty = entityType.GetProperty(searchProperty); + var idProperty = entityType.GetProperty("Id"); + + if (searchedProperty == null || idProperty == null) + { + throw new ArgumentException("No such property exists on the entity!"); + } + + var entities = this.Set + .AsNoTracking() + .Where(e => EF.Property(e, searchProperty).Contains(searchTerm)) + .Select(x => new DropDownViewModel + { + Value = idProperty.GetValue(x) !.ToString() !, + Name = searchedProperty.GetValue(x) !.ToString() !, + }) + .Take(20) + .ToList(); + + return entities; + } + + /// + /// Handles autocomplete extended searches. + /// + /// The search term. /// The property to search is configured by FormControlViewModel. /// The max autocomplete result is configured by FormControlViewModel. /// The autocomplete results. [HttpGet] - public IEnumerable Autocomplete( + public IEnumerable AutocompleteExtended( [FromQuery] string searchTerm, string searchProperty, int maxResults) @@ -240,46 +273,8 @@ public IEnumerable Autocomplete( throw new ArgumentException("No such property exists on the entity!"); } - var parameter = Expression.Parameter(typeof(TEntity), "e"); - var propertyAccess = Expression.Property(parameter, searchedProperty); - var searchTermExpression = Expression.Constant(searchTerm, typeof(string)); - - Expression body; - - if (searchedProperty.PropertyType == typeof(string)) - { - // If the property type is string, use Contains method - var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) }); - if (containsMethod != null) - { - body = Expression.Call(propertyAccess, containsMethod, searchTermExpression); - var lambda = Expression.Lambda>(body, parameter); - return this.FilterAutocompleteResults(maxResults, searchedProperty, keys, lambda); - } - } - else - { - // If the property type is not string, convert it to string and then use Contains method - var toStringMethod = typeof(object).GetMethod("ToString"); - MethodCallExpression? toStringCall = null; - if (toStringMethod != null) - { - toStringCall = Expression.Call(propertyAccess, toStringMethod); - } - - var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) }); - var toStringToLowerMethod = typeof(string).GetMethod("ToLower", Type.EmptyTypes); - if (containsMethod != null && toStringToLowerMethod != null) - { - var searchTermToLowerExpression = Expression.Call(searchTermExpression, toStringToLowerMethod); - var toStringToLowerCall = Expression.Call(toStringCall, toStringToLowerMethod); - body = Expression.Call(toStringToLowerCall, containsMethod, searchTermToLowerExpression); - var lambda = Expression.Lambda>(body, parameter); - return this.FilterAutocompleteResults(maxResults, searchedProperty, keys, lambda); - } - } - - return new List(); + var containsExpression = ExpressionsBuilder.ForGetPropertyContains(searchedProperty, searchTerm); + return this.FilterAutocompleteResults(maxResults, searchedProperty, keys, containsExpression); } /// @@ -408,33 +403,33 @@ protected virtual async Task GetEntityForm( switch (action) { case EntityAction.Create: - { - var shownFormControls = this.ShownFormControlNames.Concat(this.ShownFormControlNamesOnCreate); - var hiddenFormControls = this.HiddenFormControlNames.Concat(this.HiddenFormControlNamesOnCreate); - formControls = SetFormControlsVisibility(formControls, shownFormControls, hiddenFormControls) - .ToList(); - break; - } + { + var shownFormControls = this.ShownFormControlNames.Concat(this.ShownFormControlNamesOnCreate); + var hiddenFormControls = this.HiddenFormControlNames.Concat(this.HiddenFormControlNamesOnCreate); + formControls = SetFormControlsVisibility(formControls, shownFormControls, hiddenFormControls) + .ToList(); + break; + } case EntityAction.Edit: - { - var shownFormControls = this.ShownFormControlNames.Concat(this.ShownFormControlNamesOnEdit); - var hiddenFormControls = this.HiddenFormControlNames.Concat(this.HiddenFormControlNamesOnEdit); - formControls = SetFormControlsVisibility(formControls, shownFormControls, hiddenFormControls) - .ToList(); - break; - } + { + var shownFormControls = this.ShownFormControlNames.Concat(this.ShownFormControlNamesOnEdit); + var hiddenFormControls = this.HiddenFormControlNames.Concat(this.HiddenFormControlNamesOnEdit); + formControls = SetFormControlsVisibility(formControls, shownFormControls, hiddenFormControls) + .ToList(); + break; + } case EntityAction.Delete: - { - formControls.ForEach(fc => fc.IsReadOnly = true); - break; - } + { + formControls.ForEach(fc => fc.IsReadOnly = true); + break; + } default: - { - throw new ArgumentOutOfRangeException(nameof(action), action, null); - } + { + throw new ArgumentOutOfRangeException(nameof(action), action, null); + } } return this.View( @@ -874,20 +869,18 @@ private static void CopyFormPropertiesToExistingEntityFromNewEntity(TEntity exis .ToList() .ForEach(property => property.SetValue(existingEntity, property.GetValue(newEntity, null), null)); - private IEnumerable FilterAutocompleteResults(int maxResults, PropertyInfo searchedProperty, IEnumerable keys, Expression> lambda) + private IEnumerable FilterAutocompleteResults(int maxResults, PropertyInfo searchedProperty, IEnumerable keys, Expression> expression) { var entities = this.Set .AsNoTracking() - .Where(lambda) + .Where(expression) .Take(maxResults) .ToList(); return entities .Select(x => new DropDownViewModel { - Value = keys!.Count() == 1 - ? keys.First().GetValue(x) !.ToString() ! - : keys.Select(k => k.GetValue(x) !.ToString()), + Value = keys.Select(k => k.GetValue(x) !.ToString()), Name = searchedProperty.GetValue(x) !.ToString() !, }) .ToList(); diff --git a/src/AutoCrudAdmin/Enumerations/FormControlType.cs b/src/AutoCrudAdmin/Enumerations/FormControlType.cs index 85b68c2..2b95ce9 100644 --- a/src/AutoCrudAdmin/Enumerations/FormControlType.cs +++ b/src/AutoCrudAdmin/Enumerations/FormControlType.cs @@ -29,4 +29,9 @@ public enum FormControlType /// Represents an autocomplete control, where the user's input is completed automatically as they type. /// Autocomplete = 4, + + /// + /// Represents an autocomplete control, where the user's input is completed automatically as they type. Extended to work with more then just strings and configure results received. + /// + AutocompleteExtended = 5, } diff --git a/src/AutoCrudAdmin/Extensions/TypeExtensions.cs b/src/AutoCrudAdmin/Extensions/TypeExtensions.cs index 37f30a1..27432a6 100644 --- a/src/AutoCrudAdmin/Extensions/TypeExtensions.cs +++ b/src/AutoCrudAdmin/Extensions/TypeExtensions.cs @@ -149,6 +149,18 @@ public static bool IsNavigationProperty(this Type type) public static bool IsEnumerableExceptString(this Type type) => typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string); + /// + /// This method returns the MethodInfo object for the ToString method of the Object class. + /// + /// Returns MethodInfo? (object.ToString()). + public static MethodInfo? GetObjectToStringMethodInfo() => typeof(object).GetMethod(nameof(string.ToString)); + + /// + /// This method returns the MethodInfo object for the Contains method of the String class. + /// + /// Returns MethodInfo? (string.Contains()). + public static MethodInfo? GetStringContainsMethodInfo() => typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) }); + private static DbContext? CreateDbContext(Type type) { try diff --git a/src/AutoCrudAdmin/Helpers/ExpressionsBuilder.cs b/src/AutoCrudAdmin/Helpers/ExpressionsBuilder.cs index ecd96c6..1f04872 100644 --- a/src/AutoCrudAdmin/Helpers/ExpressionsBuilder.cs +++ b/src/AutoCrudAdmin/Helpers/ExpressionsBuilder.cs @@ -112,6 +112,42 @@ public static Func ForGetPropertyValue(PropertyInfo pr instanceParam).Compile(); } + /// + /// Constructs a method for getting values that contains specific search term. + /// + /// The type of the entity. + /// The property for which we construct the method. + /// The search term. + /// All matching obejects. + public static Expression> ForGetPropertyContains(PropertyInfo property, string searchTerm) + { + var entityType = typeof(TEntity); + var parameter = Expression.Parameter(entityType, "model"); + + var propertyAccess = Expression.Property(parameter, property); + var searchTermExpression = Expression.Constant(searchTerm, typeof(string)); + + var containsMethod = Extensions.TypeExtensions.GetStringContainsMethodInfo(); + + if (property.PropertyType == typeof(string)) + { + // model.Property.Contains(searchTerm) + var containsCall = Expression.Call(propertyAccess, containsMethod !, searchTermExpression); + + // model => model.Property.Contains(searchTerm) + return Expression.Lambda>(containsCall, parameter); + } + + // If the property type is not string, convert it to string and then use Contains method + var toStringMethod = Extensions.TypeExtensions.GetObjectToStringMethodInfo(); + + MethodCallExpression? toStringCall = null; + toStringCall = Expression.Call(propertyAccess, toStringMethod !); + + var body = Expression.Call(toStringCall, containsMethod !, searchTermExpression); + return Expression.Lambda>(body, parameter); + } + private static Expression ForPrimaryKeySubExpression( string name, string value, diff --git a/src/AutoCrudAdmin/TagHelpers/FormInputTagHelper.cs b/src/AutoCrudAdmin/TagHelpers/FormInputTagHelper.cs index 43ee4bc..1e9e878 100644 --- a/src/AutoCrudAdmin/TagHelpers/FormInputTagHelper.cs +++ b/src/AutoCrudAdmin/TagHelpers/FormInputTagHelper.cs @@ -138,7 +138,7 @@ public override Task ProcessAsync(TagHelperContext context, TagHelperOutput outp { this.PrepareExpandableMultiChoiceCheckBox(output); } - else if (this.FormControlType == FormControlType.Autocomplete) + else if (this.FormControlType == FormControlType.Autocomplete || this.FormControlType == FormControlType.AutocompleteExtended) { this.PrepareAutocompleteDropdown(output); } diff --git a/src/AutoCrudAdmin/ViewModels/FormControlViewModel.cs b/src/AutoCrudAdmin/ViewModels/FormControlViewModel.cs index 83ee13e..51cf7dd 100644 --- a/src/AutoCrudAdmin/ViewModels/FormControlViewModel.cs +++ b/src/AutoCrudAdmin/ViewModels/FormControlViewModel.cs @@ -84,12 +84,12 @@ public string DisplayName public string? FormControlAutocompleteEntityId { get; set; } /// - /// Gets or sets the entity property that autocomplete will search by. + /// Gets or sets the entity property that autocomplete will search by. (Autocomplete Extended). /// public string? SearchByPropertyAutocompleteConfiguration { get; set; } /// - /// Gets or sets how much autocomplete results will be shown (20 by default). + /// Gets or sets how much autocomplete results will be shown (20 by default). (Autocomplete Extended). /// public int NumberOfAutocompleteItemsShownConfiguration { get; set; } = 20; } \ No newline at end of file diff --git a/src/AutoCrudAdmin/Views/Shared/Autocomplete/_AutocompleteExtendedPartial.cshtml b/src/AutoCrudAdmin/Views/Shared/Autocomplete/_AutocompleteExtendedPartial.cshtml new file mode 100644 index 0000000..fcd0e7a --- /dev/null +++ b/src/AutoCrudAdmin/Views/Shared/Autocomplete/_AutocompleteExtendedPartial.cshtml @@ -0,0 +1,44 @@ +@model AutoCrudAdmin.ViewModels.FormControlViewModel + +@{ + var controllerName = Model.FormControlAutocompleteController; +} + +@Html.Hidden("AutocompleteId", "#" + Model.Name + "Id") +@Html.Hidden("ControllerName", controllerName) +@Html.Hidden(Model.FormControlAutocompleteEntityId, "") + + \ No newline at end of file diff --git a/src/AutoCrudAdmin/Views/Shared/Autocomplete/_AutocompletePartial.cshtml b/src/AutoCrudAdmin/Views/Shared/Autocomplete/_AutocompletePartial.cshtml new file mode 100644 index 0000000..ae716c9 --- /dev/null +++ b/src/AutoCrudAdmin/Views/Shared/Autocomplete/_AutocompletePartial.cshtml @@ -0,0 +1,42 @@ +@model AutoCrudAdmin.ViewModels.FormControlViewModel + +@{ + var controllerName = Model.FormControlAutocompleteController; +} + +@Html.Hidden("AutocompleteId", "#" + Model.Name + "Id") +@Html.Hidden("ControllerName", controllerName) +@Html.Hidden(Model.FormControlAutocompleteEntityId, "") + + \ No newline at end of file diff --git a/src/AutoCrudAdmin/Views/Shared/_EnityFormControlsPartial.cshtml b/src/AutoCrudAdmin/Views/Shared/_EnityFormControlsPartial.cshtml index 9aa48b2..1b9babf 100644 --- a/src/AutoCrudAdmin/Views/Shared/_EnityFormControlsPartial.cshtml +++ b/src/AutoCrudAdmin/Views/Shared/_EnityFormControlsPartial.cshtml @@ -56,46 +56,11 @@ if (formControl.FormControlType == FormControlType.Autocomplete) { - @Html.Hidden("AutocompleteId", "#" + formControl.Name + "Id") - var controllerName = formControl.FormControlAutocompleteController; - @Html.Hidden("ControllerName", controllerName) - @Html.Hidden(formControl.FormControlAutocompleteEntityId, "") - - + @await Html.PartialAsync("Autocomplete/_AutocompletePartial", formControl); + } + else if (formControl.FormControlType == FormControlType.AutocompleteExtended) + { + @await Html.PartialAsync("Autocomplete/_AutocompleteExtendedPartial", formControl); } }