diff --git a/src/AutoCrudAdmin/Controllers/AutoCrudAdminController.cs b/src/AutoCrudAdmin/Controllers/AutoCrudAdminController.cs index 1e0f27f..a7de897 100644 --- a/src/AutoCrudAdmin/Controllers/AutoCrudAdminController.cs +++ b/src/AutoCrudAdmin/Controllers/AutoCrudAdminController.cs @@ -219,6 +219,7 @@ public override void OnActionExecuting(ActionExecutingContext context) /// The property to search. /// The autocomplete results. [HttpGet] + [Obsolete] public virtual IEnumerable Autocomplete([FromQuery] string searchTerm, string searchProperty) { var entityType = typeof(TEntity); @@ -244,6 +245,38 @@ public virtual IEnumerable Autocomplete([FromQuery] string se 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 AutocompleteExtended( + [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 keys = entityType.GetPrimaryKeyPropertyInfos(); + + if (searchedProperty == null || !keys.Any()) + { + throw new ArgumentException("No such property exists on the entity!"); + } + + var containsExpression = ExpressionsBuilder.ForGetPropertyContains(searchedProperty, searchTerm); + return this.FilterAutocompleteResults(maxResults, searchedProperty, keys, containsExpression); + } + /// /// Shows the index view. Contains a grid with paging, sorting and filtering. /// @@ -720,7 +753,7 @@ protected IActionResult RedirectToActionWithStringFilter( actionName, gridStringFilterType.ToString()); - /// + /// /// Builds custom grid columns. /// /// Page columns. @@ -836,6 +869,23 @@ 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> expression) + { + var entities = this.Set + .AsNoTracking() + .Where(expression) + .Take(maxResults) + .ToList(); + + return entities + .Select(x => new DropDownViewModel + { + Value = 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/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 9de97d7..51cf7dd 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. (Autocomplete Extended). + /// + public string? SearchByPropertyAutocompleteConfiguration { get; set; } + + /// + /// 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 5c8c657..1b9babf 100644 --- a/src/AutoCrudAdmin/Views/Shared/_EnityFormControlsPartial.cshtml +++ b/src/AutoCrudAdmin/Views/Shared/_EnityFormControlsPartial.cshtml @@ -56,44 +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); } }