Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion src/AutoCrudAdmin/Controllers/AutoCrudAdminController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ public override void OnActionExecuting(ActionExecutingContext context)
/// <param name="searchProperty">The property to search.</param>
/// <returns>The autocomplete results.</returns>
[HttpGet]
[Obsolete]
public virtual IEnumerable<DropDownViewModel> Autocomplete([FromQuery] string searchTerm, string searchProperty)
{
var entityType = typeof(TEntity);
Expand All @@ -244,6 +245,38 @@ public virtual IEnumerable<DropDownViewModel> Autocomplete([FromQuery] string se
return entities;
}

/// <summary>
/// Handles autocomplete extended searches.
/// </summary>
/// <param name="searchTerm">The search term.</param>
/// <param name="searchProperty">The property to search is configured by FormControlViewModel.</param>
/// <param name="maxResults">The max autocomplete result is configured by FormControlViewModel.</param>
/// <returns>The autocomplete results.</returns>
[HttpGet]
public IEnumerable<DropDownViewModel> 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<TEntity>(searchedProperty, searchTerm);
return this.FilterAutocompleteResults(maxResults, searchedProperty, keys, containsExpression);
}

/// <summary>
/// Shows the index view. Contains a grid with paging, sorting and filtering.
/// </summary>
Expand Down Expand Up @@ -720,7 +753,7 @@ protected IActionResult RedirectToActionWithStringFilter(
actionName,
gridStringFilterType.ToString());

/// <summary>
/// <summary>
/// Builds custom grid columns.
/// </summary>
/// <param name="columns">Page columns.</param>
Expand Down Expand Up @@ -836,6 +869,23 @@ private static void CopyFormPropertiesToExistingEntityFromNewEntity(TEntity exis
.ToList()
.ForEach(property => property.SetValue(existingEntity, property.GetValue(newEntity, null), null));

private IEnumerable<DropDownViewModel> FilterAutocompleteResults(int maxResults, PropertyInfo searchedProperty, IEnumerable<PropertyInfo> keys, Expression<Func<TEntity, bool>> 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<TEntity> GenerateGrid(IHtmlHelper<AutoCrudAdminIndexViewModel> htmlHelper)
=> htmlHelper
.Grid(this.GetQueryWithIncludes(this.MasterGridFilter))
Expand Down
5 changes: 5 additions & 0 deletions src/AutoCrudAdmin/Enumerations/FormControlType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ public enum FormControlType
/// Represents an autocomplete control, where the user's input is completed automatically as they type.
/// </summary>
Autocomplete = 4,

/// <summary>
/// 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.
/// </summary>
AutocompleteExtended = 5,
}
12 changes: 12 additions & 0 deletions src/AutoCrudAdmin/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/// <summary>
/// This method returns the MethodInfo object for the ToString method of the Object class.
/// </summary>
/// <returns>Returns MethodInfo? (object.ToString()).</returns>
public static MethodInfo? GetObjectToStringMethodInfo() => typeof(object).GetMethod(nameof(string.ToString));

/// <summary>
/// This method returns the MethodInfo object for the Contains method of the String class.
/// </summary>
/// <returns>Returns MethodInfo? (string.Contains()).</returns>
public static MethodInfo? GetStringContainsMethodInfo() => typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) });

private static DbContext? CreateDbContext(Type type)
{
try
Expand Down
36 changes: 36 additions & 0 deletions src/AutoCrudAdmin/Helpers/ExpressionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,42 @@ public static Func<TEntity, object> ForGetPropertyValue<TEntity>(PropertyInfo pr
instanceParam).Compile();
}

/// <summary>
/// Constructs a method for getting values that contains specific search term.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="property">The property for which we construct the method.</param>
/// <param name="searchTerm">The search term.</param>
/// <returns>All matching obejects.</returns>
public static Expression<Func<TEntity, bool>> ForGetPropertyContains<TEntity>(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<Func<TEntity, bool>>(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<Func<TEntity, bool>>(body, parameter);
}

private static Expression ForPrimaryKeySubExpression(
string name,
string value,
Expand Down
2 changes: 1 addition & 1 deletion src/AutoCrudAdmin/TagHelpers/FormInputTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
10 changes: 10 additions & 0 deletions src/AutoCrudAdmin/ViewModels/FormControlViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,14 @@ public string DisplayName
/// Gets or sets the entity id for the autocomplete feature of the form control.
/// </summary>
public string? FormControlAutocompleteEntityId { get; set; }

/// <summary>
/// Gets or sets the entity property that autocomplete will search by. (Autocomplete Extended).
/// </summary>
public string? SearchByPropertyAutocompleteConfiguration { get; set; }

/// <summary>
/// Gets or sets how much autocomplete results will be shown (20 by default). (Autocomplete Extended).
/// </summary>
public int NumberOfAutocompleteItemsShownConfiguration { get; set; } = 20;
}
Original file line number Diff line number Diff line change
@@ -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, "")

<script type="text/javascript">
$(function () {
const autocompleteId = $('#AutocompleteId').val();
const controllerName = $('#ControllerName').val();
const hiddenId = '@Model.FormControlAutocompleteEntityId';

$(autocompleteId).autocomplete({
source: function (request, response) {
$.ajax({
type: "GET",
url: `/${controllerName}/AutocompleteExtended`,
data: {
searchTerm: request.term,
searchProperty: '@Model.SearchByPropertyAutocompleteConfiguration',
maxResults: @Model.NumberOfAutocompleteItemsShownConfiguration,
},
success: function (data) {
response($.map(data, function (item) {
return { label: item.name, value: item.value };
}));
},
error: function (error) {
console.log(error);
},
});
},
select: function (event, ui) {
event.preventDefault();
$(`#${hiddenId}`).val(ui.item.value);
$(autocompleteId).val(ui.item.label);
},
});
});
</script>
Original file line number Diff line number Diff line change
@@ -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, "")

<script type="text/javascript">
$(function () {
const autocompleteId = $('#AutocompleteId').val();
const controllerName = $('#ControllerName').val();
const hiddenId = '@Model.FormControlAutocompleteEntityId';

$(autocompleteId).autocomplete({
source: function (request, response) {
$.ajax({
type: "GET",
url: `/${controllerName}/Autocomplete`,
data: {
searchTerm: request.term
},
success: function (data) {
response($.map(data, function (item) {
return { label: item.name, value: item.value };
}));
},
error: function (error) {
console.log(error);
},
});
},
select: function (event, ui) {
event.preventDefault();
$(`#${hiddenId}`).val(ui.item.value);
$(autocompleteId).val(ui.item.label);
},
});
});
</script>
43 changes: 5 additions & 38 deletions src/AutoCrudAdmin/Views/Shared/_EnityFormControlsPartial.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -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, "")

<script type="text/javascript">
$(function() {
const autocompleteId= $('#AutocompleteId').val();
const controllerName = $('#ControllerName').val();
const hiddenId = '@formControl.FormControlAutocompleteEntityId';

$(autocompleteId).autocomplete({
source: function(request, response) {
$.ajax({
type: "GET",

url: `/${controllerName}/Autocomplete`,
data: {
searchTerm: request.term
},
success: function (data) {
response($.map(data, function (item) {
return { label: item.name, value: item.value };
}));
},
error: function(error){
console.log(error);
},
});
},
select: function(event, ui) {
event.preventDefault();
$(`#${hiddenId}`).val(ui.item.value);
$(autocompleteId).val(ui.item.label);
},
});
});
</script>
@await Html.PartialAsync("Autocomplete/_AutocompletePartial", formControl);
}
else if (formControl.FormControlType == FormControlType.AutocompleteExtended)
{
@await Html.PartialAsync("Autocomplete/_AutocompleteExtendedPartial", formControl);
}
}

Expand Down