villainoustourist / Blazor.Pagination

A reusable pagination component for Blazor.
MIT License
38 stars 18 forks source link

Problem with UI refresh #3

Closed mquarino closed 3 years ago

mquarino commented 3 years ago

Hi, I'm testing the component but I can't get the UI to refresh. I followed the instructions you provide but still I couldn't

I leave you my code to see if you can help me

@page "/test"
@using BlazorPagination
@using System.Text.Json

@inject IJSRuntime js
@inject HttpClient http

<h3>TEST</h3>
@if (testList == null)
{
    <p>Loading....</p>
}
else
{
        <table data-toggle="table" id="table">
            <thead class="font-weight-bold">
                <tr>
                    <th scope="col" data-field="Id" data-sortable="true">Id</th>
                    <th scope="col" data-field="Text" data-sortable="true">Text</th>
                    <th scope="col" data-field="Value" data-sortable="true">Value</th>

                </tr>
            </thead>
            <tbody>
                @foreach (var t in _data.Results)
                {
                    <tr>
                        <td scope="row">@t.Id</td>
                        <td>@t.Text</td>
                        <td>@t.Value</td>
                    </tr>
                }
            </tbody>
        </table>
        <JSFunction></JSFunction>

        <BlazorPager CurrentPage="@_data.CurrentPage"
                     PageCount="@_data.PageCount"
                     OnPageChanged="(async e => { _page = e; LoadTest(); })"
                     ShowFirstLast="false"
                     ShowPageNumbers="true"
                     VisiblePages="10"
                     FirstText="First"
                     LastText="Last" />
}

@code {
    List<Common.TestEntity> testList;

    private PagedResult<Common.TestEntity> _data;
    private string _filter;
    private int _page = 1;

    protected override void OnInitialized()
    {
        LoadTest();
        base.OnInitialized();
    }

    void LoadTest()
    {
        HttpClient http = new HttpClient();

        var httpResponse = http.GetAsync($"https://localhost:44348/api/test").GetAwaiter().GetResult();
        if (httpResponse.IsSuccessStatusCode)
        {
            var responseString = httpResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult();
            testList = JsonSerializer.Deserialize<List<Common.TestEntity>>(responseString,
                new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });

            _data = testList.AsQueryable().ToPagedResult(_page, 10);
        }
        else
        {
            // handle error
        }

        this.StateHasChanged();
    }
}
villainoustourist commented 3 years ago

I've ran your code (mocking the API call) and it seems to work. Note that I've changed the calls to async/await where possible.

@code {
    private PagedResult<TestEntity> _data;
    private int _page = 1;

    protected override async Task OnInitializedAsync()
    {
        await LoadTest();
    }

    private async Task LoadTest()
    {

        var testList = await Http.GetFromJsonAsync<TestEntity[]>("http://localhost:44348/api/test")
            .ConfigureAwait(false);

        _data = testList.AsQueryable()
            .ToPagedResult(_page, 10);
        StateHasChanged();
    }
}

Can you send some same code for the API?

In my test, I used

[HttpGet] public TestEntity[] Get()

villainoustourist commented 3 years ago

It's possible with a mix of synchronous and asynchronous calls, the call is returned before the data is populated.

mquarino commented 3 years ago

Yes, the API code is this. Basically it connects to a DB and obtains a list of records (no more than 50 rows)

[HttpGet]
[AllowAnonymous]
public async Task<List<Common.TestEntity> Get([FromQuery] PaginationDTO pagination, [FromQuery] string lastName)
{
    IOperationsService service = (IOperationsService)this.Provider.GetService(typeof(IOperationsService));
    return await service.GetList<Common.TestEntity>();
}
villainoustourist commented 3 years ago

Great, thanks. I suspect the issue is:

OnPageChanged="(async e => { _page = e; LoadTest(); })"

The async call without an await will cause the code to return before LoadPage is done. Just to prove that out, can you change your code to something like this (_mainly changing LoadTest to async and changing the null check to data) :

@page "/test"
@using BlazorPagination
@using System.Text.Json

@inject IJSRuntime js
@inject HttpClient http

<h3>TEST</h3>
@if (_data == null)
{
    <p>Loading....</p>
}
else
{
    <table data-toggle="table" id="table">
        <thead class="font-weight-bold">
            <tr>
                <th scope="col" data-field="Id" data-sortable="true">Id</th>
                <th scope="col" data-field="Text" data-sortable="true">Text</th>
                <th scope="col" data-field="Value" data-sortable="true">Value</th>

            </tr>
        </thead>
        <tbody>
            @foreach (var t in _data.Results)
            {
                <tr>
                    <td scope="row">@t.Id</td>
                    <td>@t.Text</td>
                    <td>@t.Value</td>
                </tr>
            }
        </tbody>
    </table>
    <JSFunction></JSFunction>

    <BlazorPager CurrentPage="@_data.CurrentPage"
                 PageCount="@_data.PageCount"
                 OnPageChanged="(async e => { _page = e; await LoadTest(); })"
                 ShowFirstLast="false"
                 ShowPageNumbers="true"
                 VisiblePages="10"
                 FirstText="First"
                 LastText="Last" />
}

@code {
    private PagedResult<Common.TestEntity> _data;
    private int _page = 1;

    protected override async Task OnInitializedAsync()
    {
        await LoadTest();
    }

    async Task LoadTest()
    {
        var httpResponse = await http.GetAsync($"https://localhost:44348/api/test");
        if (httpResponse.IsSuccessStatusCode)
        {
            var responseString = await httpResponse.Content.ReadAsStringAsync();
            var testList = JsonSerializer.Deserialize<List<Common.TestEntity>>(responseString,
                new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
            _data = testList.AsQueryable().ToPagedResult(_page, 10);
        }
        else
        {
            // handle error
        }
        this.StateHasChanged();
    }
}

If that works, then we know where the issue is. If so, then we can add a OnPageChangedAsync vs OnPageChanged to allow you to use synchronous methods.

mquarino commented 3 years ago

I made the suggested changes but it still doesn't work for me

    List<Common.TestEntity> testList;

    private PagedResult<Common.TestEntity> _data;
    private string _filter;
    private int _page = 1;

    protected override async Task OnInitializedAsync()
    {
        await LoadTest();
    }

    async Task LoadTest()
    {
        HttpClient http = new HttpClient();

        testList = await http.GetFromJsonAsync<List<Common.TestEntity>>($"https://localhost:44348/api/test");
        _data = testList.AsQueryable().ToPagedResult(_page, 10);

        await InvokeAsync(StateHasChanged);
    }
villainoustourist commented 3 years ago

And these parts? The last one being the most important.

@if (**_data** == null)
{
    <p>Loading....</p>
}
else
{

... OnPageChanged="(async e => { _page = e; **await** LoadTest(); })"

mquarino commented 3 years ago

Yes, I even tried removing all the async and awaits

page "/test"
@using BlazorPagination
@using System.Net.Http.Json

@inject IJSRuntime js
@inject HttpClient http

<h3>CBU</h3>

    <table data-toggle="table" id="table">
         <thead class="font-weight-bold">
            <tr>
                <th scope="col" data-field="Id" data-sortable="true">Id</th>
                <th scope="col" data-field="Text" data-sortable="true">Text</th>
                <th scope="col" data-field="Value" data-sortable="true">Value</th>

            </tr>
        </thead>
        <tbody>
            @foreach (var t in _data.Results)
            {
                <tr>
                    <td scope="row">@t.Id</td>
                    <td>@t.Text</td>
                    <td>@t.Value</td>
                </tr>
            }
        </tbody>
    </table>

    <BlazorPager CurrentPage="@_data.CurrentPage"
                 PageCount="@_data.PageCount"
                 OnPageChanged="(async e => { _page = e; LoadTest(); })"
                 ShowFirstLast="false"
                 ShowPageNumbers="true"
                 VisiblePages="10"
                 FirstText="First"
                 LastText="Last" />

    <JSFunction></JSFunction>

@code {
    List<Common.TestEntity> testList;

    private PagedResult<Common.TestEntity> _data = new PagedResult<Common.TestEntity>();
    private string _filter;
    private int _page = 1;

    protected override void OnInitialized()
    {
        LoadTest();
    }
    void LoadTest()
    {
        testList = http.GetFromJsonAsync<List<Common.TestEntity>>($"https://localhost:44348/api/test").GetAwaiter().GetResult();
        _data = testList.AsQueryable().ToPagedResult(_page, 10);

        this.StateHasChanged();
    }
}
villainoustourist commented 3 years ago

I still see this: OnPageChanged="(async e => { _page = e; LoadTest(); })"

Can you add the await there? i.e. : OnPageChanged="(async e => { _page = e; await LoadTest(); })"

To fix that, the we may need to add a synchronous version, but I want to verify that this is the culprit first.

mquarino commented 3 years ago

With this change throw a NullReference in the foreach since _data or _data.Results are null

I guess I miss something

@page "/test"
@using BlazorPagination
@using System.Net.Http.Json

@inject IJSRuntime js
@inject HttpClient http

    <table data-toggle="table" id="table">
         <thead class="font-weight-bold">
            <tr>
                <th scope="col" data-field="Id" data-sortable="true">Id</th>
                <th scope="col" data-field="Text" data-sortable="true">Text</th>
                <th scope="col" data-field="Value" data-sortable="true">Value</th>

            </tr>
        </thead>
        <tbody>
            @foreach (var t in _data.Results)
            {
                <tr>
                    <td scope="row">@t.Id</td>
                    <td>@t.Text</td>
                    <td>@t.Value</td>
                </tr>
            }
        </tbody>
    </table>
        <BlazorPager CurrentPage="@_data.CurrentPage"
                     PageCount="@_data.PageCount"
                     OnPageChanged="(async e => { _page = e; await LoadTest(); })"
                     ShowFirstLast="false"
                     ShowPageNumbers="true"
                     VisiblePages="10"
                     FirstText="First"
                     LastText="Last" />

        <JSFunction></JSFunction>

@code {

    List<Common.TestEntity> testList;

    private PagedResult<Common.TestEntity> _data = new PagedResult<Common.TestEntity>();
    private int _page = 1;

    protected override async Task OnInitializedAsync()
    {
        await LoadTest();
    }

    async Task LoadTest()
    {
        testList = await http.GetFromJsonAsync<List<Common.TestEntity>>($"https://localhost:44348/api/test");
        _data = testList.AsQueryable().ToPagedResult(_page, 10);

        //await InvokeAsync(StateHasChanged);
        this.StateHasChanged();
    }
}
villainoustourist commented 3 years ago

Looks good, almost there, just add that null check back

@if (_data == null)
{
    <p>Loading....</p>
}
else
{
etc ...
}
mquarino commented 3 years ago

Still not refreshing the UI

villainoustourist commented 3 years ago

I uploaded up test project here - if you want to take a look: https://github.com/villainoustourist/BlazorPaginationTest

The specific page that I'm testing is here: https://github.com/villainoustourist/BlazorPaginationTest/blob/master/Client/Pages/Index.razor

It sounds like the projects are at least similar - let's check environments. You're using Blazor WASM (I assume based on code - although it works server-side as well), on a current browser (not IE)? I'm running the 3.1.403 version of the .NET Core SDK testing on the Edge browser version 86.

mquarino commented 3 years ago

The problem is here. If remove the JSFunction tag, paging works, it only remains for me to see how to implement bootstrap-table

<JSFunction></JSFunction>

ect IJSRuntime jsRuntime

@code{
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await jsRuntime.InvokeVoidAsync(identifier: "test");
        }
    }
}
<script>
        function test() {
            $(function () {
                $('#table').bootstrapTable()
            })
        }
    </script>
villainoustourist commented 3 years ago

Glad you figured it out. Sometimes JS + Blazor causes some challenges.