gustavnavar / Grid.Blazor

Grid component with CRUD for Blazor (client-side and server-side) and ASP.NET Core MVC
GNU Lesser General Public License v2.1
698 stars 135 forks source link

Combo Dropdown with remote lookup for large list #282

Open borisdj opened 3 years ago

borisdj commented 3 years ago

How could Dropdown field in a Form be configured for remote lookup. Is there somewhere example with this usage? If there would be dozens of thousand of Companies, to load in combo only say first 50 based on search input. So field would need some trigger on input change that would call method on server which will return top 50 to load combo. Is something like this supported with simple config, if not, is there a way to achieve it with custom setup? Thx in advance.

faina09 commented 3 years ago

I do this type of dropdown using RenderCrudComponentAs<>

It displays an <input> field where you enter a SearchTerm to populate the dropdown searching for values contaings SearchTerm.

If you want I can submit my code here

borisdj commented 3 years ago

Snipped with example would be nice, thx.

faina09 commented 3 years ago

To use my component I insert in class ColumnCollections something like this:

        private static Func<BLibro, string?> exprIdTown = (s) => { return s.IdTownNavigation == null ? "" : s.IdTownNavigation?.Nome; };
(..)

c.Add(o => o.IdTown).Titled("TownRlst").RenderValueAs(o => o.IdTown == null ? "" : o.IdTownNavigation.Nome)
                .RenderCrudComponentAs<RemoteDropDownComponent<BLibro, BTown>>(("IdTown", exprIdTown), false);

This is the component:

using GridBlazor;
using GridShared;
using GridShared.Columns;
using HayaiB;
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Bookstore.Pages.Components
{
    public partial class RemoteDropDownComponent<T, V> : ICustomGridComponent<T> where V : class, IGenericModel
    {
        [Parameter]
        public T Item { get; set; }

        [Parameter]
        public CGrid<T> Grid { get; set; }

        [Parameter]
        public object Object { get; set; }

        [Inject]
        private IGenericService<V> RlstService { get; set; }

        public IEnumerable<SelectItem> SelectedItems;
        public string selectedValue;
        private string searchTerm;
        public bool allowChange;
        private string _RlstField;
        private string _message;
        private Func<T, string?> _expr;
        private ModelExtension Model { get; set; }

        public string SearchTerm
        {
            get { return searchTerm; }
            set { searchTerm = value; OnSearchChange(); }
        }

        protected override void OnParametersSet()
        {
            if (Object.GetType() == typeof((string, Func<T, string?>)))
            {
                (_RlstField, _expr) = ((string, Func<T, string?>))Object;
                try
                {
                    Model = new ModelExtension(typeof(T), Item);
                    selectedValue = Model.GetValue($"{_RlstField}")?.ToString() ?? "";
                    SearchTerm = _expr(Item) ?? "";
                }
                catch (Exception e)
                {
                    throw new Exception("ERROR RemoteDropDownComponent must have (string[], Func<T, string?>) parameters and GetSelectedItems");
                }
            }

            string gridState = Grid.GetState();
            allowChange = Grid.Mode == GridMode.Update || Grid.Mode == GridMode.Create;
            if (!allowChange)
            {
                selectedValue = String.Concat(Model.GetValue($"{_RlstField}")?.ToString(), " - ", _expr(Item));
            }
        }

        public void OnSearchChange()
        {
            SelectedItems = RlstService.GetSelectedItems(searchTerm);
            _message = $"selezionare... [{SelectedItems.Count()}]";
        }

        private void ChangeValue(ChangeEventArgs e)
        {
            var value = e?.Value?.ToString();
            Model.SetValue($"{_RlstField}", value);
        }
    }
}

I developed and use here a ModelExtension class that is:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Reflection;

namespace HayaiB
{
    public class ModelExtension
    {
        public Type ModelType { get; }
        public object Item { get; set; }

        public ModelExtension(Type type, object item)
        {
            ModelType = type;
            Item = item;
        }

        public void SetValue(string columnName, object? value)
        {
            PropertyInfo[] properties = ModelType.GetProperties();
            foreach (PropertyInfo property in properties)
            {
                if (property.Name == columnName)
                {
                    if (Int32.TryParse((string?)value, out int ivalue))
                    {
                        property.SetValue(Item, ivalue);
                    }
                    else
                    {
                        property.SetValue(Item, value);
                    }
                    return;
                }
            }
        }

        public object? GetValue(string columnName)
        {
            PropertyInfo[] properties = ModelType.GetProperties();
            foreach (PropertyInfo property in properties)
            {
                if (property.Name == columnName)
                {
                    return property.GetValue(Item);
                }
            }
            return null;
        }

        public List<string> GetKeyNames()
        {
            PropertyInfo[] properties = ModelType.GetProperties();
            List<string> ret = new ();
            foreach (PropertyInfo property in properties)
            {
                if (Attribute.GetCustomAttribute(property, typeof(KeyAttribute)) is KeyAttribute)
                {
                    ret.Add(property.Name);
                }
            }
            return ret;
        }

        public List<object?> GetKeyValues()
        {
            PropertyInfo[] properties = ModelType.GetProperties();
            List<object?> ret = new ();
            foreach (PropertyInfo property in properties)
            {
                if (Attribute.GetCustomAttribute(property, typeof(KeyAttribute)) is KeyAttribute)
                {
                    ret.Add(GetValue(property.Name));
                }
            }
            return ret;
        }
    }
}
borisdj commented 3 years ago

I wasn't able to make this work, so now trying again. I've made a Demo project, created new BlazorServer simple app, with entities Order and Customer and their pages with CrudGrid. You can downloaded or fork it from here: GridBlazorDropDown Before starting the app Db should be created from migration with command update-database, and on first run Seed method will input data into Customer table. I have also added your classes ModelExtension and RemoteDropDownComponent but I am not sure how to connected them properly. Should this component make single remote combo or does it work with one text field and another connected combo? In Order.razor. there is:

c.Add(o => o.CustomerId, true).SetSelectField(true, o => o.Customer.Name, customerService.Get);
c.Add(o => o.Customer.Name).SetCrudHidden(true);
  // and commented
//c.Add(o => o.CustomerId).Titled("Customer")
// .RenderValueAs(o => o.Customer.Name)
// .RenderCrudComponentAs<RemoteDropDownComponent<Customer, Customer>>(("CustomerId", exprIdValue), false);

Could you take a look and applied it to this example. Thx in advance.

faina09 commented 3 years ago

sorry, indeed I forget this RemoteDropDownComponent.razor

@typeparam T
@typeparam V

<style>
    .row > div.col-md-10 > div.card.panel.panel-default { border: none }
        .row > div.col-md-10 > div.card.panel.panel-default > div.card-body.panel-body { padding: 0px; border: none }
</style>
@if (allowChange)
{
    <div class="row col-md-5">Find item starting by: <input class="form-control " @bind="SearchTerm" /></div>
    <div class="row col-md-5">
    <select id="Select_@_RlstField" name="@_RlstField" class="form-control" value="@selectedValue" @onchange="(e) => ChangeValue(e)">
        <option value="">@_message</option>
        @foreach (var selectItem in SelectedItems)
        {
            if (selectItem.Value == selectedValue)
            {
                <option value="@selectItem.Value" selected="selected">@selectItem.Title</option>
            }
            else
            {
                <option value="@selectItem.Value">@selectItem.Title</option>
            }
        }
    </select>
    </div>
}
else
{
    <input id="@_RlstField" name="@_RlstField" class="form-control" value="@selectedValue" disabled="disabled" />
}

and that is needed a new method in CustomerService.cs:

        // used by RemoteDropDownComponent
        public IEnumerable<SelectItem> GetSelectedItems(string searchTerm)
        {
            using var context = new ApplicationDbContext(ApplicationDbContext.GetOptions());
            return context.Customers
                .Where(o => o.Name.StartsWith(searchTerm)).Take(100)
                .Select(r => new SelectItem(r.CustomerId.ToString(), r.Name))
                .ToList();
        }
borisdj commented 3 years ago

Still struggling to connect this. Any change you could update working example in the linked Demo.

faina09 commented 3 years ago

can't write on your repo see my fork updated here: https://github.com/faina09/GridBlazorDropDown/tree/master

borisdj commented 3 years ago

I have managed to make it work. Big Thanks for the component. Linked repository GridBlazorDropDown is now updated with working example, in case someone else would need it. Services are also changed so that they inherit IGenericService to easily use generic component.

borisdj commented 3 years ago

@faina09 one more Q. I now have DropDown Remote Lookup with (Key,Value) not being (Int/String) but (Guid/String). And the is issue that when on Form Adding new record, on component RemoteDropDownComponent.razor.cs in method:

private void ChangeValue(ChangeEventArgs e)
{
    var value = e?.Value?.ToString();
    Model.SetValue($"{_RlstField}", value);
}

value is not Guid but comes as int (seems it is sequence number of select list) and can not be parsed into Guid.

For example if having 2 element in List: [('CBB291BC-38CF-41B4-D753-08D9695EB894', 'Item 1'), ('35476E01-0C21-4EAE-D754-08D9695EB894', 'Second Item')] So when select the second one instead of '35476E01-0C21-4EAE-D754-08D9695EB894' value is '2'.

Any idea how to fix it? I could update linked repos from previous post with example if it could help.

borisdj commented 3 years ago

Actually I think I have solved the problem, seems the issue was something else (wrong Service for lookup). Will check it further.