Select2.razor.cs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. using Microsoft.AspNetCore.Components;
  2. using Microsoft.JSInterop;
  3. using Newtonsoft.Json;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.ComponentModel.DataAnnotations;
  7. using System.Linq;
  8. using System.Threading;
  9. using System.Threading.Tasks;
  10. namespace KeudellCoding.Blazor.AdvancedBlazorSelect2 {
  11. public partial class Select2<TItem, TSource> : ComponentBase where TSource : IEnumerable<TItem> {
  12. private const string JS_COMMAND_PREFIX = "KeudellCoding_Blazor_AdvancedBlazorSelect2Component";
  13. private const string HTML_ID_PREFIX = "select2-input-";
  14. //===========================================================================================================================================
  15. [Inject] public IJSRuntime JSRuntime { get; set; }
  16. //===========================================================================================================================================
  17. [Parameter, Required] public Func<TItem, string> IdSelector { get; set; }
  18. [Parameter, Required] public Func<TItem, string> TextSelector { get; set; }
  19. [Parameter, Required] public Func<TSource, string, CancellationToken, Task<List<TItem>>> FilterFunction { get; set; }
  20. [Parameter, Required] public Func<TSource, string, CancellationToken, Task<TItem>> GetElementById { get; set; }
  21. [Parameter, Required] public TSource Datasource { get; set; }
  22. [Parameter] public List<TItem> Value { get; set; }
  23. [Parameter] public EventCallback OnValueChanged { get; set; } = EventCallback.Empty;
  24. [Parameter] public Func<TItem, MarkupString> OptionLayout { get; set; }
  25. [Parameter] public InputSelect2Options Select2Options { get; set; } = new InputSelect2Options();
  26. [Parameter] public ushort MaxItemsPerPage { get; set; } = 50;
  27. [Parameter] public bool Multiselect { get => Select2Options.Multiple; set => Select2Options.Multiple = value; }
  28. //[Parameter] public IMemoryCache Cache { get; set; } = null;
  29. [Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> AdditionalAttributes { get; set; }
  30. //===========================================================================================================================================
  31. private readonly string inputGuid = Guid.NewGuid().ToString();
  32. private CancellationTokenSource searchCancellationToken = null;
  33. private DotNetObjectReference<Select2<TItem, TSource>> dotNetRef;
  34. //===========================================================================================================================================
  35. protected override void OnInitialized() {
  36. base.OnInitialized();
  37. if (Datasource.Count() > MaxItemsPerPage && Select2Options.MinimumInputLength < 1)
  38. Select2Options.MinimumInputLength = 1;
  39. dotNetRef = DotNetObjectReference.Create(this);
  40. }
  41. protected override async Task OnAfterRenderAsync(bool firstRender) {
  42. base.OnAfterRender(firstRender);
  43. if (firstRender) {
  44. await JSRuntime.InvokeVoidAsync($"{JS_COMMAND_PREFIX}.init", HTML_ID_PREFIX + inputGuid, JsonConvert.SerializeObject(Select2Options), dotNetRef);
  45. await JSRuntime.InvokeVoidAsync($"{JS_COMMAND_PREFIX}.onChange", HTML_ID_PREFIX + inputGuid, dotNetRef);
  46. await updateSelect2Async(false);
  47. }
  48. await checkIfUpdateNeededAsync();
  49. }
  50. //===========================================================================================================================================
  51. private async Task updateSelect2Async(bool triggerChange) {
  52. checkValueForLimit();
  53. await JSRuntime.InvokeVoidAsync($"{JS_COMMAND_PREFIX}.updateSelected", HTML_ID_PREFIX + inputGuid, getFromTItem(Value.AsQueryable()), triggerChange);
  54. }
  55. private async Task checkIfUpdateNeededAsync() {
  56. Value.RemoveAll(i => i == null);
  57. var selectedIds = await JSRuntime.InvokeAsync<string[]>($"{JS_COMMAND_PREFIX}.getSelectedIds", HTML_ID_PREFIX + inputGuid);
  58. if (!new HashSet<string>(selectedIds).SetEquals(Value.Select(IdSelector))) {
  59. await updateSelect2Async(true);
  60. }
  61. }
  62. private Select2Item getFromTItem(TItem item) {
  63. return new Select2Item {
  64. Id = IdSelector(item),
  65. Text = TextSelector(item),
  66. FormatedResult = OptionLayout == null ? TextSelector(item) : OptionLayout(item).ToString()
  67. };
  68. }
  69. private IEnumerable<Select2Item> getFromTItem(IEnumerable<TItem> items) => items.Select(getFromTItem);
  70. private object getCacheKey(string id) => new { Namespace = "KeudellCoding.Blazor.AdvancedBlazorSelect2", Type = "SearchCache", Select2Id = inputGuid, ObjId = id };
  71. private object getCacheKey(TItem item) => getCacheKey(IdSelector(item));
  72. private void checkValueForLimit() {
  73. if (Value == null)
  74. Value = new List<TItem> { };
  75. if (!Multiselect && Value.Count > 1)
  76. Value.RemoveRange(1, Value.Count - 1);
  77. }
  78. private async Task<TItem> getByIdAsync(string id) {
  79. if (string.IsNullOrEmpty(id))
  80. return default;
  81. //if (Cache != null && Cache.TryGetValue(getCacheKey(id), out TItem value))
  82. // return value;
  83. else {
  84. var result = await GetElementById(Datasource, id, default);
  85. //if (Cache != null)
  86. // Cache.Set(getCacheKey(id), result);
  87. return result;
  88. }
  89. }
  90. private async Task<IEnumerable<TItem>> getByIdAsync(IEnumerable<string> ids) => (await Task.WhenAll(ids.Select(getByIdAsync))).AsQueryable();
  91. private List<TItem> getPageOfResult(List<TItem> select2Items, int page, out bool hasNextPage) {
  92. page = page < 1 ? 1 : page;
  93. var elementsFromPage = select2Items.Where(e => e != null).Skip((page - 1) * MaxItemsPerPage).Take(MaxItemsPerPage + 1).ToList();
  94. hasNextPage = elementsFromPage.Count() > MaxItemsPerPage;
  95. if (hasNextPage)
  96. elementsFromPage.RemoveAt(elementsFromPage.Count() - 1);
  97. return elementsFromPage;
  98. }
  99. //===========================================================================================================================================
  100. [JSInvokable("Select2OnChange")]
  101. public async Task OnSelectionChangedAsync(IEnumerable<string> newSelectedIds) {
  102. Value.Clear();
  103. var selectedElements = await getByIdAsync(newSelectedIds.Where(i => i != null));
  104. if (!Multiselect)
  105. selectedElements = selectedElements.Take(1);
  106. Value.AddRange(selectedElements);
  107. _ = OnValueChanged.InvokeAsync(this);
  108. }
  109. [JSInvokable("Select2OnSearch")]
  110. public async Task<SearchServerResponse> OnSearchAsync(string searchTerm, int page) {
  111. if (searchCancellationToken != null)
  112. searchCancellationToken.Cancel();
  113. searchCancellationToken = new CancellationTokenSource();
  114. var returnItem = new SearchServerResponse { };
  115. var hasMoreResults = false;
  116. var filteredDatasource = await FilterFunction(Datasource, searchTerm, searchCancellationToken.Token);
  117. var searchResults = await Task.Run(() => getPageOfResult(filteredDatasource, page, out hasMoreResults), searchCancellationToken.Token);
  118. if (searchCancellationToken.IsCancellationRequested)
  119. return returnItem;
  120. //if (Cache != null)
  121. // foreach (var searchResult in searchResults)
  122. // Cache.Set(getCacheKey(searchResult), searchResult);
  123. returnItem.Results = getFromTItem(searchResults.AsQueryable());
  124. returnItem.Pagination.More = hasMoreResults;
  125. return returnItem;
  126. }
  127. }
  128. }