Megabit / Blazorise

Blazorise is a component library built on top of Blazor with support for CSS frameworks like Bootstrap, Tailwind, Bulma, AntDesign, and Material.
https://blazorise.com/
Other
3.3k stars 533 forks source link

AutoComplete with ReadData going into infinite loop #4762

Closed husseinshaib1 closed 1 year ago

husseinshaib1 commented 1 year ago

Hello guys, I am trying to use AutoComplete control with read data in order to load data after user search from the api because I have lot of data to retrieve.

<Blazorise.Components.Autocomplete TItem="DataPointDto"
                                   TValue="string"
                                   Data="@Temp"
                                   TextField="@(( item ) => item.DisplayKey)"
                                   ValueField="@(( item ) => item.Key)"
                                   Placeholder="Search..."
                                   SelectionMode="AutocompleteSelectionMode.Checkbox"
                                   ReadData="@OnHandleReadData"
                                   @bind-SelectedValues="multipleSelectionData"
                                   @bind-SelectedTexts="multipleSelectionTexts">
</Blazorise.Components.Autocomplete>

and here is the code behind:

public List<DataPointDto> Temp = new List<DataPointDto>();
        public List<DataPointDto> DataPoints = new List<DataPointDto>();
        private Random random = new();
        List<string> multipleSelectionData;
        List<string> multipleSelectionTexts;
        protected override async Task OnInitializedAsync()
        {

            DataPoints = new List<DataPointDto>();
            DataPoints.Add(new DataPointDto() { Key = "1", DisplayKey = "dsadsa 1" });
            DataPoints.Add(new DataPointDto() { Key = "2", DisplayKey = "dsagfdhfgdsa 1" });
            DataPoints.Add(new DataPointDto() { Key = "3", DisplayKey = "gfhgf 1" });
            DataPoints.Add(new DataPointDto() { Key = "4", DisplayKey = "dsafdfdgdfdsa 1" });
            await base.OnInitializedAsync();
        }

        private void OnHandleReadData(AutocompleteReadDataEventArgs autocompleteReadDataEventArgs)
        {
            if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested)
            {
                Task.Delay(random.Next(100));
                if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested)
                {
                    Temp = DataPoints.Take(2).ToList();

                }
            }

        }

the problem is when I enter any character inside the AutoComplete the event OnHandleReadData is called infinitely. This will cause overhead on the Api.

Any suggestions please?

David-Moreira commented 1 year ago

@husseinshaib1 Hello, I am unable to reproduce the issue on latest version with code similar to yours. Can you provide more details? If it's called repeatedly, the app would just straight crash or be completely unresponsive.

ReadData should be called on most key strokes which is what is happening.

Here's the code I used to test:

<Blazorise.Components.Autocomplete TItem="DataPointDto"
                                   TValue="string"
                                   Data="@Temp"
                                   TextField="@(( item ) => item.DisplayKey)"
                                   ValueField="@(( item ) => item.Key)"
                                   Placeholder="Search..."
                                   SelectionMode="AutocompleteSelectionMode.Checkbox"
                                   ReadData="@OnHandleReadData"
                                   @bind-SelectedValues="multipleSelectionData"
                                   @bind-SelectedTexts="multipleSelectionTexts">
</Blazorise.Components.Autocomplete>
@code {
    public class DataPointDto
    {
        public string Key { get; set; }
        public string DisplayKey { get; set; }
    }
    public List<DataPointDto> Temp = new List<DataPointDto>();
    public List<DataPointDto> DataPoints = new List<DataPointDto>();
    private Random random = new();
    List<string> multipleSelectionData;
    List<string> multipleSelectionTexts;
    protected override async Task OnInitializedAsync()
    {

        DataPoints = new List<DataPointDto>();
        DataPoints.Add( new DataPointDto() { Key = "1", DisplayKey = "dsadsa 1" } );
        DataPoints.Add( new DataPointDto() { Key = "2", DisplayKey = "dsagfdhfgdsa 1" } );
        DataPoints.Add( new DataPointDto() { Key = "3", DisplayKey = "gfhgf 1" } );
        DataPoints.Add( new DataPointDto() { Key = "4", DisplayKey = "dsafdfdgdfdsa 1" } );
        await base.OnInitializedAsync();
    }

    private void OnHandleReadData( AutocompleteReadDataEventArgs autocompleteReadDataEventArgs )
    {
        Console.WriteLine( "OnHandleReadData" );
        if ( !autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested )
        {
            Task.Delay( random.Next( 100 ) );
            if ( !autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested )
            {
                Temp = DataPoints.Take( 2 ).ToList();

            }
        }

    }
}
github-actions[bot] commented 1 year ago

Hello @husseinshaib1, thank you for your submission. The issue was labeled "Status: Repro Missing", as you have not provided a way to reproduce the issue quickly. Most problems already solve themselves when isolated, but we would like you to provide us with a reproducible code to make it easier to investigate a possible bug.

husseinshaib1 commented 1 year ago

I tried again the issue is still there with the same code, but what I noticed is that when I open the developer tool and place a break point on the read data handler in the code behind it works. however when I run without opening the developer tool and placing the breakpoint on the handler function the issue occurs. I know this is very weird and makes no sense but this is what I noticed!

David-Moreira commented 1 year ago

Please provide a repro I must be missing something on my tests.

husseinshaib1 commented 1 year ago

image here is the packages I am using in my demo project.

@using Blazorise
@using Blazorise.Components;

<Blazorise.Components.Autocomplete TItem="DataPointDto"
                                   TValue="string"
                                   Data="@Temp"
                                   TextField="@(( item ) => item.DisplayKey)"
                                   ValueField="@(( item ) => item.Key)"
                                   Placeholder="Search..."
                                   SelectionMode="AutocompleteSelectionMode.Checkbox"
                                   ReadData="@OnHandleReadData"
                                   @bind-SelectedValues="multipleSelectionData"
                                   @bind-SelectedTexts="multipleSelectionTexts">
</Blazorise.Components.Autocomplete>

this is the blazor code followed by the code behind

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using System.Net.Http;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Virtualization;
using Microsoft.AspNetCore.Components.WebAssembly.Http;
using Microsoft.JSInterop;
using BlazorApp1;
using BlazorApp1.Shared;
using Blazorise;
using Blazorise.Components;

namespace BlazorApp1.Pages
{
    public partial class Counter
    {
        public class DataPointDto
        {
            public string Key { get; set; }
            public string DisplayKey { get; set; }
        }
        public List<DataPointDto> Temp = new List<DataPointDto>();
        public List<DataPointDto> DataPoints = new List<DataPointDto>();
        private Random random = new();
        List<string> multipleSelectionData;
        List<string> multipleSelectionTexts;
        protected override async Task OnInitializedAsync()
        {

            DataPoints = new List<DataPointDto>();
            DataPoints.Add(new DataPointDto() { Key = "1", DisplayKey = "dsadsa 1" });
            DataPoints.Add(new DataPointDto() { Key = "2", DisplayKey = "dsagfdhfgdsa 1" });
            DataPoints.Add(new DataPointDto() { Key = "3", DisplayKey = "gfhgf 1" });
            DataPoints.Add(new DataPointDto() { Key = "4", DisplayKey = "dsafdfdgdfdsa 1" });
            await base.OnInitializedAsync();
        }

        private void OnHandleReadData(AutocompleteReadDataEventArgs autocompleteReadDataEventArgs)
        {
            Console.WriteLine("OnHandleReadData");
            if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested)
            {
                Task.Delay(random.Next(100));
                if (!autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested)
                {
                    Temp = DataPoints.Take(2).ToList();

                }
            }

        }
    }

}

this is the whole code I have. just to explain once again the issue by steps:

  1. put a breakpoint on the OnHandleReadData function
  2. Run the application
  3. click on the autocomplete to have focus
  4. type any letter inside the textbox
  5. the breakpoint is hit
  6. press f5 without adding any letters inside the box
  7. then notice that the breakpoint is hit again automatically

As I said before, if you type inside the textbox with developer tools open the breakpoint will be hit only once.

hope this will help, I have nothing more to elaborate or explain

David-Moreira commented 1 year ago

@husseinshaib1 Ahh now you're giving me more precise info on what you're doing. :) This seems to be a combination of debugging and when you press F5 the window refocus on the browser and retriggers ReadData. I don't think this is an example of real world usage of Autocomplete. Debugging on WASM has some quirkness sometimes because under the curtains the browser is syncing/sending dotnet debugging info to visual studio.

If you take off the breakpoint and just take a look at the Developer tools you can keep track of how many times it is being called and when due to the Console.WriteLine( "OnHandleReadData" ); and it seems normal to me.

image

Again, if the behaviour you mentioned was happening on regular usage, the Autocomplete would most likely make the page unresponsive or crash if it kept calling ReadData continously.

I understand that the debugging behavior might not be the best, but I would say that's just a quirky of wasm debugging. Just let me know if you're ok with this response, or if you still feel like there's something quite not right.

husseinshaib1 commented 1 year ago

this is true, the issue is now clarified. It is just debugging issue. I have another question, is there a way to do lazy loading from api. when I read the docs I found that lazy loading only loads data incrementally on UI not getting back to the API every time to get more data. If this possible please let me know

David-Moreira commented 1 year ago

So load data on demand, but from an UI perspective? Virtualization? Check https://blazorise.com/docs/extensions/autocomplete Virtualize example. Or https://bootstrapdemo.blazorise.com/tests/autocomplete at the bottom, Virtualize examples Is that what you're looking for?

husseinshaib1 commented 1 year ago

in the virtualization section the code is as below:

<Autocomplete TItem="Country"
              TValue="string"
              Data="@Countries"
              TextField="@(( item ) => item.Name)"
              ValueField="@((item) => item.Iso)"
              @bind-SelectedValue="selectedSearchValue"
              Placeholder="Search..."
              Virtualize>
    <NotFoundContent> Sorry... @context was not found! :( </NotFoundContent>
</Autocomplete>
@code {
    [Inject]
    public CountryData CountryData { get; set; }
    public IEnumerable<Country> Countries;

    protected override async Task OnInitializedAsync()
    {
        Countries = await CountryData.GetDataAsync();
        await base.OnInitializedAsync();
    }

    public string selectedSearchValue { get; set; }
}

in this code what is being done is as follows: 1- get data on initialization 2-the control will load data by chunks on UI without going back each time to API

what I need is: 1-get data from api on first load(for ex, 10 items) 2- when user scrolls down in the drop down list I should hit the api again and say hey I need the next 10 items and so on

Is this feasible?

David-Moreira commented 1 year ago

in the virtualization section the code is as below:

<Autocomplete TItem="Country"
              TValue="string"
              Data="@Countries"
              TextField="@(( item ) => item.Name)"
              ValueField="@((item) => item.Iso)"
              @bind-SelectedValue="selectedSearchValue"
              Placeholder="Search..."
              Virtualize>
    <NotFoundContent> Sorry... @context was not found! :( </NotFoundContent>
</Autocomplete>
@code {
    [Inject]
    public CountryData CountryData { get; set; }
    public IEnumerable<Country> Countries;

    protected override async Task OnInitializedAsync()
    {
        Countries = await CountryData.GetDataAsync();
        await base.OnInitializedAsync();
    }

    public string selectedSearchValue { get; set; }
}

in this code what is being done is as follows: 1- get data on initialization 2-the control will load data by chunks on UI without going back each time to API

what I need is: 1-get data from api on first load(for ex, 10 items) 2- when user scrolls down in the drop down list I should hit the api again and say hey I need the next 10 items and so on

Is this feasible?

ReadData code should work by enabling Virtualize. You will find arguments you can use with the Virtualize offset, example taken off the demo page source code: image

    private async Task HandleVirtualizeReadData( AutocompleteReadDataEventArgs autocompleteReadDataEventArgs )
    {
        if ( !autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested )
        {
            IEnumerable<Country> response = ( await GetDataFromExternalSource( autocompleteReadDataEventArgs.SearchValue, autocompleteReadDataEventArgs.VirtualizeOffset, autocompleteReadDataEventArgs.VirtualizeCount ) );

            if ( !autocompleteReadDataEventArgs.CancellationToken.IsCancellationRequested )
            {
                VirtualizeReadDataCountries = response;
                VirtualizeTotalReadDataCountries = await GetTotalFromExternalSource( autocompleteReadDataEventArgs.SearchValue );
            }
        }
    }

https://github.com/Megabit/Blazorise/blob/master/Demos/Blazorise.Demo/Pages/Tests/AutocompletesPage.razor

husseinshaib1 commented 1 year ago

Thank you bro for the help I think this is what I need.

David-Moreira commented 1 year ago

Going ahead and closing the issue. We have a discord channel if you want to join for more recurrent questions, https://discord.io/blazorise