using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace KeudellCoding.Blazor.AdvancedBlazorSelect2 { public partial class Select2 : ComponentBase where TSource : IEnumerable { private const string JS_COMMAND_PREFIX = "KeudellCoding_Blazor_AdvancedBlazorSelect2Component"; private const string HTML_ID_PREFIX = "select2-input-"; //=========================================================================================================================================== [Inject] public IJSRuntime JSRuntime { get; set; } //=========================================================================================================================================== [Parameter, Required] public Func IdSelector { get; set; } [Parameter, Required] public Func TextSelector { get; set; } [Parameter, Required] public Func>> FilterFunction { get; set; } [Parameter, Required] public Func> GetElementById { get; set; } [Parameter, Required] public TSource Datasource { get; set; } [Parameter] public List Value { get; set; } [Parameter] public EventCallback OnValueChanged { get; set; } = EventCallback.Empty; [Parameter] public Func OptionLayout { get; set; } [Parameter] public InputSelect2Options Select2Options { get; set; } = new InputSelect2Options(); [Parameter] public ushort MaxItemsPerPage { get; set; } = 50; [Parameter] public bool Multiselect { get => Select2Options.Multiple; set => Select2Options.Multiple = value; } //[Parameter] public IMemoryCache Cache { get; set; } = null; [Parameter(CaptureUnmatchedValues = true)] public IDictionary AdditionalAttributes { get; set; } //=========================================================================================================================================== private readonly string inputGuid = Guid.NewGuid().ToString(); private CancellationTokenSource searchCancellationToken = null; private DotNetObjectReference> dotNetRef; //=========================================================================================================================================== protected override void OnInitialized() { base.OnInitialized(); if (Datasource.Count() > MaxItemsPerPage && Select2Options.MinimumInputLength < 1) Select2Options.MinimumInputLength = 1; dotNetRef = DotNetObjectReference.Create(this); } protected override async Task OnAfterRenderAsync(bool firstRender) { base.OnAfterRender(firstRender); if (firstRender) { await JSRuntime.InvokeVoidAsync($"{JS_COMMAND_PREFIX}.init", HTML_ID_PREFIX + inputGuid, JsonConvert.SerializeObject(Select2Options), dotNetRef); await JSRuntime.InvokeVoidAsync($"{JS_COMMAND_PREFIX}.onChange", HTML_ID_PREFIX + inputGuid, dotNetRef); await updateSelect2Async(false); } await checkIfUpdateNeededAsync(); } //=========================================================================================================================================== private async Task updateSelect2Async(bool triggerChange) { checkValueForLimit(); await JSRuntime.InvokeVoidAsync($"{JS_COMMAND_PREFIX}.updateSelected", HTML_ID_PREFIX + inputGuid, getFromTItem(Value.AsQueryable()), triggerChange); } private async Task checkIfUpdateNeededAsync() { Value.RemoveAll(i => i == null); var selectedIds = await JSRuntime.InvokeAsync($"{JS_COMMAND_PREFIX}.getSelectedIds", HTML_ID_PREFIX + inputGuid); if (!new HashSet(selectedIds).SetEquals(Value.Select(IdSelector))) { await updateSelect2Async(true); } } private Select2Item getFromTItem(TItem item) { return new Select2Item { Id = IdSelector(item), Text = TextSelector(item), FormatedResult = OptionLayout == null ? TextSelector(item) : OptionLayout(item).ToString() }; } private IEnumerable getFromTItem(IEnumerable items) => items.Select(getFromTItem); private object getCacheKey(string id) => new { Namespace = "KeudellCoding.Blazor.AdvancedBlazorSelect2", Type = "SearchCache", Select2Id = inputGuid, ObjId = id }; private object getCacheKey(TItem item) => getCacheKey(IdSelector(item)); private void checkValueForLimit() { if (Value == null) Value = new List { }; if (!Multiselect && Value.Count > 1) Value.RemoveRange(1, Value.Count - 1); } private async Task getByIdAsync(string id) { if (string.IsNullOrEmpty(id)) return default; //if (Cache != null && Cache.TryGetValue(getCacheKey(id), out TItem value)) // return value; else { var result = await GetElementById(Datasource, id, default); //if (Cache != null) // Cache.Set(getCacheKey(id), result); return result; } } private async Task> getByIdAsync(IEnumerable ids) => (await Task.WhenAll(ids.Select(getByIdAsync))).AsQueryable(); private List getPageOfResult(List select2Items, int page, out bool hasNextPage) { page = page < 1 ? 1 : page; var elementsFromPage = select2Items.Where(e => e != null).Skip((page - 1) * MaxItemsPerPage).Take(MaxItemsPerPage + 1).ToList(); hasNextPage = elementsFromPage.Count() > MaxItemsPerPage; if (hasNextPage) elementsFromPage.RemoveAt(elementsFromPage.Count() - 1); return elementsFromPage; } //=========================================================================================================================================== [JSInvokable("Select2OnChange")] public async Task OnSelectionChangedAsync(IEnumerable newSelectedIds) { Value.Clear(); var selectedElements = await getByIdAsync(newSelectedIds.Where(i => i != null)); if (!Multiselect) selectedElements = selectedElements.Take(1); Value.AddRange(selectedElements); _ = OnValueChanged.InvokeAsync(this); } [JSInvokable("Select2OnSearch")] public async Task OnSearchAsync(string searchTerm, int page) { if (searchCancellationToken != null) searchCancellationToken.Cancel(); searchCancellationToken = new CancellationTokenSource(); var returnItem = new SearchServerResponse { }; var hasMoreResults = false; var filteredDatasource = await FilterFunction(Datasource, searchTerm, searchCancellationToken.Token); var searchResults = await Task.Run(() => getPageOfResult(filteredDatasource, page, out hasMoreResults), searchCancellationToken.Token); if (searchCancellationToken.IsCancellationRequested) return returnItem; //if (Cache != null) // foreach (var searchResult in searchResults) // Cache.Set(getCacheKey(searchResult), searchResult); returnItem.Results = getFromTItem(searchResults.AsQueryable()); returnItem.Pagination.More = hasMoreResults; return returnItem; } } }