MC9000
MC9000

Reputation: 2423

Blazor UI not refreshing after return from dialog component

This has probably been asked before, but I'm at a loss as to what's going on here: I have a Blazor page that calls a dialog component to edit, insert, delete a record in the database. After either Deleting or Inserting a record (not editing - no need), I want to simply refresh a grid (table) with the new results. So I get a return value from the component when it is closed that is either True or False : True = refresh parent/caller page with the new results, False = do nothing (when just changing a record). The problem is the UI is never updated (even if calling StateHasChanged()), and I understand that won't work as it is only a request to update the GUI, not a forced refresh. What I don't understand is exactly HOW to achieve this goal. MS BOL is confusing as heck and no actual working examples with a razor component. I know how to send data back to the caller - that's not the problem. In fact, that's working perfectly, but the Main page acts like it needs another event (though I want it to refresh automatically), but I'm not sure how to invoke a dummy event to get it to refresh properly. Here's the main/index page code:

    @page "/"
@inject IConfiguration config
@inject DialogService dialog
@inject NotificationService notification

<PageTitle>Memo Master</PageTitle>
<RadzenButton Click="GetMemos" Text="Get Memos" ButtonStyle="ButtonStyle.Primary" ButtonType="ButtonType.Submit" />
<RadzenTextBox @ref="searchBox" Name="SearchPhrase" @bind-Value=@SearchString MaxLength="400" @oninput=@(args => SearchString = args.Value.ToString()) @onkeydown=@Enter />  @*searchString value continually updated for onkeydown to work*@
<RadzenButton Click="() => OpenMemo(0)" Text="New Memo" Icon="add_circle_outline" ButtonStyle="ButtonStyle.Secondary" />

<br />
<br />
@if (FoundMemos != null && busy == false)
{
    <RadzenDataGrid Data="@FoundMemos" TItem="MemoSearch" AllowFiltering="true" AllowSorting="true" AllowColumnResize="true" AllowPaging="true" PageSize=20
                FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive" SelectionMode="DataGridSelectionMode.Single" @bind-Value="@SelectedMemos" RowClick="@OnRowClicked">
        <Columns>
            <RadzenDataGridColumn TItem="MemoSearch" Title="Index" Width="70px" Filterable="false" TextAlign="TextAlign.Left">
                <Template Context="m">
                    <RadzenText TextStyle="TextStyle.Caption">@m.Idx.ToString()</RadzenText>
                </Template>
            </RadzenDataGridColumn>
            <RadzenDataGridColumn TItem="MemoSearch" Property="Title" Title="Title">
            </RadzenDataGridColumn>
            <RadzenDataGridColumn TItem="MemoSearch" Title="Modified" Width="140px" TextAlign="TextAlign.Right">
                <Template Context="m">
                    <RadzenText TextStyle="TextStyle.Caption">@m.ModifiedOn.ToString("MM/dd/yyyy hh:mm tt")</RadzenText>
                </Template>
            </RadzenDataGridColumn>
        </Columns>
    </RadzenDataGrid>
}
else
{
    <DisplaySpinner />
}
<br />
<br />
<RadzenButton Click="Reset" Text="Reset" ButtonStyle="ButtonStyle.Secondary" />

@code {
    List<MemoSearch> FoundMemos = new();
    private string SearchString = "";
    private string DBConnStr { get; set; } = "";
    public DB dB = new();
    IList<MemoSearch>? SelectedMemos;
    RadzenTextBox searchBox = new();
    private bool busy;

    async Task OpenMemo(int Idx)
    {
        string DialogTitle = (Idx == 0) ? "Create New Memo" : $"Edit Memo {Idx.xToStr()}";

        bool RefreshResults = await dialog.OpenAsync<MemoDetails>(DialogTitle, new Dictionary<string, object>() { { "Idx", Idx } });
        if (RefreshResults)
        {
            await GetMemos(); //this method is called, but no page refresh 
            StateHasChanged(); //this isn't causing the page to refresh
        }
    }

    protected override async Task OnInitializedAsync() => dB.DBConnStr = config.GetConnectionString("DBConnStr");

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender) await searchBox.Element.FocusAsync(); //NOTE: this is for Radzen "elements"
    }

    public async Task GetMemos()
    {
        busy = true;
        FoundMemos = await dB.MemoSearchByPageFilterSortAsync(SearchString, PageSize: 9999);
        busy = false;
    }

    public void Reset()
    {
        FoundMemos = new();
        SearchString = "";
    }

    public async void Enter(KeyboardEventArgs e)
    {
        if (e.Code == "Enter" || e.Code == "NumpadEnter" || e.Key == "Enter")
        {
            await GetMemos();
            StateHasChanged();
        }
    }

    async Task OnRowClicked(Radzen.DataGridRowMouseEventArgs<MemoSearch> args)
    {
        if (args != null)
        {
            await OpenMemo(args.Data.Idx);
        }
    }
}

The Details razor component code:

    @inject IConfiguration config
@inject DialogService dialog
@inject NotificationService notification

@if (memo != null)
{
    <RadzenTemplateForm TItem="Memo" Data=@memo Submit=@OnSubmit>
        <p>
            <RadzenLabel Component="Title" Text="Title" />
            <RadzenTextBox id="MemoTitle" Name="Title" @[email protected] MaxLength="400" />
            <RadzenRequiredValidator Component="Title" Text="Title is required!" />
        </p>
        <p>
            <RadzenLabel Component="Body" Text="Memo" />
            <RadzenTextArea id="MemoBody" Name="Body" @[email protected] Rows="18" />
        </p>
        <p>
            <RadzenLabel Component="Keywords" Text="Key Words" />
            <RadzenTextBox id="MemoKeywords" Name="Keywords" @[email protected] MaxLength="400" />
        </p>
        <RadzenButton ButtonType="ButtonType.Submit" ButtonStyle="ButtonStyle.Success" Icon="save" Text="Save" BusyText="Saving ..." IsBusy=@busy />
        <RadzenButton ButtonType="ButtonType.Button" ButtonStyle="ButtonStyle.Danger" Icon="delete" Text="Delete" Click="@((args) => DeleteMemo(memo.Idx))" @onclick:stopPropagation="true"></RadzenButton>
        <RadzenButton Text="Close" Click="() => dialog.Close(false)" ButtonStyle="ButtonStyle.Light" />
    </RadzenTemplateForm>
}

@code {
    [Parameter]
    public int Idx { get; set; }
    public DB dB = new();
    Memo? memo;
    bool busy;

    protected override async void OnInitialized()
    {
        dB.DBConnStr = config.GetConnectionString("DBConnStr");
        memo = (Idx == 0) ? new Memo() : await GetMemoByIdx(Idx);
        await InvokeAsync(() => StateHasChanged()).ConfigureAwait(false); //IMPORTANT!!
    }

    public async Task<Memo> GetMemoByIdx(int Idx) => await dB.MemoSelectByIdxAsync(Idx);

    async Task OnSubmit(Memo memo)
    {
        busy = true;
        int Result;
        bool RefreshResults = false;
        if (memo.ModifiedOn == DateTime.MinValue) memo.ModifiedOn = DateTime.Now;
        string NotificationDetailMessage = memo.Idx == 0 ? "New Memo has been created." : $"Memo {memo.Idx} has been saved.";
        Result = await dB.MemoUpsertAsync(memo);
        if (Result < -1)
        {
            notification.Notify(new NotificationMessage { Severity = NotificationSeverity.Error, Summary = "Error Saving", Detail = "An error saving this record has occured!\n" + dB.LastErrorMsg, Duration = 4000 });
        }
        else
        {
            notification.Notify(new NotificationMessage { Severity = NotificationSeverity.Success, Summary = "Save Success", Detail = NotificationDetailMessage, Duration = 2000 });
        }
        busy = false;
        RefreshResults = (memo.Idx == 0 && Result >= -1) ? true : false; //send the refresh message only if it's new memo AND it's successful
        dialog.Close(RefreshResults);
    }


    async Task DeleteMemo(int Idx)
    {
        busy = true;
        int Result;
        bool RefreshResults = false;
        var confirmResult = await dialog.Confirm("Are you sure?", "Confirm Memo Deletion");
        if (confirmResult.HasValue && confirmResult.Value)
        {
            Result = await dB.MemoDeleteByIdxAsync(Idx);
            if (Result < -1)
            {
                notification.Notify(new NotificationMessage { Severity = NotificationSeverity.Error, Summary = "Error Deleting", Detail = "An error deleting this record has occured!\n" + dB.LastErrorMsg, Duration = 4000 });
            }
            else
            {
                notification.Notify(new NotificationMessage { Severity = NotificationSeverity.Success, Summary = "Deletion Success", Detail = $"Memo {Idx} has been deleted.", Duration = 2000 });
                RefreshResults = true;
            }
        }
        busy = false;
        dialog.Close(RefreshResults);
    }

}

As can be seen, a boolean value is returned (it works) from the dialog call inside the OpenMemo method, and I wish to refresh the UI if it's set to true, which I then call the GetMemos method and call StateHasChanged. I am able to step through the debugger and everything is working fine (FoundMemos has records, including the newly created one or minus a deleted one!), even the markup test is working, yet it doesn't display. Oddly, SOMETIMES when stepping through the code, the page refreshes! I can't figure out what is going on and why this is happening (and more importantly, how to fix it). I've tried the hacky Task.Delay(somenumber) in between the GetMemos call and the StateHasChanged, with no joy. What am I missing?

This is a Server Side Blazor app on DotNet7.0 (for in-house use)


************* New Code - Works except when searchbox has a value in it **

Index.razor:

@page "/"
@inject IConfiguration config
@inject DialogService dialog
@inject NotificationService notification

<PageTitle>Memo Master</PageTitle>
<RadzenButton Click="() => GetMemos()" Text="Get Memos" ButtonStyle="ButtonStyle.Primary" ButtonType="ButtonType.Submit" />
<RadzenTextBox @ref="searchBox" Name="SearchPhrase" @bind-Value=@SearchString MaxLength="400" @oninput=@(args => SearchString = args.Value.ToString()) @onkeydown=@Enter />
<RadzenButton Click="() => OpenMemo(0)" Text="New Memo" Icon="add_circle_outline" ButtonStyle="ButtonStyle.Secondary" />
<br />
<br />
@if (FoundMemos != null && !busy)
{
    <RadzenDataGrid @ref=this.grid Data="@FoundMemos" TItem="MemoSearch" AllowFiltering="true" AllowSorting="true" AllowColumnResize="true" AllowPaging="true" PageSize=20
                FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive" SelectionMode="DataGridSelectionMode.Single" @bind-Value="@SelectedMemos" RowClick="@OnRowClicked">
        <Columns>
            <RadzenDataGridColumn TItem="MemoSearch" Title="Index" Width="70px" Filterable="false" TextAlign="TextAlign.Left">
                <Template Context="m">
                    <RadzenText TextStyle="TextStyle.Caption">@m.Idx.ToString()</RadzenText>
                </Template>
            </RadzenDataGridColumn>
            <RadzenDataGridColumn TItem="MemoSearch" Property="Title" Title="Title">
            </RadzenDataGridColumn>
            <RadzenDataGridColumn TItem="MemoSearch" Title="Modified" Width="140px" TextAlign="TextAlign.Right">
                <Template Context="m">
                    <RadzenText TextStyle="TextStyle.Caption">@m.ModifiedOn.ToString("MM/dd/yyyy hh:mm tt")</RadzenText>
                </Template>
            </RadzenDataGridColumn>
        </Columns>
    </RadzenDataGrid>
}
else
{
    <DisplaySpinner />
}
<br />
<br />
<RadzenButton Click="Reset" Text="Reset" ButtonStyle="ButtonStyle.Secondary" />

@code {
    [Parameter]
    public string? SearchString { get; set; }

    List<MemoSearch> FoundMemos = new();
    private string DBConnStr { get; set; } = "";
    public DB dB = new();
    IList<MemoSearch>? SelectedMemos;
    RadzenTextBox? searchBox;
    private bool busy;
    private RadzenDataGrid<MemoSearch>? grid; //reference to grid, so forced reloading can happen - though it doesn't actually work

    async Task OpenMemo(int Idx)
    {
        string DialogTitle = (Idx == 0) ? "Create New Memo" : $"Edit Memo {Idx.xToStr()}";
        object? RefreshResults = await dialog.OpenAsync<MemoDetails>(DialogTitle, new Dictionary<string, object>() { { "Idx", Idx } });
        RefreshResults = (RefreshResults == null) ? false : RefreshResults;
        if (RefreshResults.xToBoo())
        {
            await GetMemos();
        }
        await ReturnFocus();
    }

    protected override async Task OnInitializedAsync()
    {
        dB.DBConnStr = config.GetConnectionString("DBConnStr");
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender) await ReturnFocus();  //NOTE: this is for Radzen "elements"
    }

    public async Task GetMemos()
    {
        busy = true;
        FoundMemos = await dB.MemoSearchByPageFilterSortAsync(SearchString, PageSize: 9999);   
        busy = false;
    }

    public async Task Reset()
    {
        FoundMemos = new();
        SearchString = "";
        await ReturnFocus();
    }

    public async void Enter(KeyboardEventArgs e)
    {
        if (e.Code == "Enter" || e.Code == "NumpadEnter" || e.Key == "Enter")
        {
            await GetMemos();
            StateHasChanged(); //need to call this here after keypress, lest you get a continual spinner
        }
    }

    async Task OnRowClicked(Radzen.DataGridRowMouseEventArgs<MemoSearch> args)
    {
        if (args != null)
        {
            await OpenMemo(args.Data.Idx);
        }
    }

    async Task ReturnFocus()
    {
        await searchBox.Element.FocusAsync();
    }
}

MemoDetails.razor:

    @inject IConfiguration config
@inject DialogService dialog
@inject NotificationService notification

@if (memo != null)
{
    <RadzenTemplateForm TItem="Memo" Data=@memo Submit=@OnSubmit>
        <p>
            <RadzenLabel Component="Title" Text="Title" />
            <RadzenTextBox id="MemoTitle" Name="Title" @[email protected] MaxLength="400" />
            <RadzenRequiredValidator Component="Title" Text="Title is required!" />
        </p>
        <p>
            <RadzenLabel Component="Body" Text="Memo" />
            <RadzenTextArea id="MemoBody" Name="Body" @[email protected] Rows="18" />
        </p>
        <p>
            <RadzenLabel Component="Keywords" Text="Key Words" />
            <RadzenTextBox id="MemoKeywords" Name="Keywords" @[email protected] MaxLength="400" />
        </p>
        <RadzenButton ButtonType="ButtonType.Submit" ButtonStyle="ButtonStyle.Success" Icon="save" Text="Save" BusyText="Saving ..." IsBusy=@busy />
        @if (Idx > 0)
        {
            <RadzenButton ButtonType="ButtonType.Button" ButtonStyle="ButtonStyle.Danger" Icon="delete" Text="Delete" Click="@((args) => DeleteMemo(memo.Idx))" @onclick:stopPropagation="true"></RadzenButton>
        }
        <RadzenButton Text="Close" Click="() => dialog.Close(false)" ButtonStyle="ButtonStyle.Light" />
    </RadzenTemplateForm>
}

@code {
    [Parameter]
    public int Idx { get; set; } = 0;

    public DB dB = new();
    Memo? memo;
    bool busy;

    protected override async void OnInitialized()
    {
        dB.DBConnStr = config.GetConnectionString("DBConnStr");
        memo = (Idx == 0) ? new Memo() : await GetMemoByIdx(Idx);
        await InvokeAsync(() => StateHasChanged()).ConfigureAwait(false); //IMPORTANT!!
    }

    public async Task<Memo> GetMemoByIdx(int Idx) => await dB.MemoSelectByIdxAsync(Idx);

    async Task OnSubmit(Memo memo)
    {
        int Result;
        bool RefreshResults = false;
        if (memo.ModifiedOn == DateTime.MinValue) memo.ModifiedOn = DateTime.Now;
        string NotificationDetailMessage = memo.Idx == 0 ? "New Memo has been created." : $"Memo {memo.Idx} has been saved.";
        busy = true;
        Result = await dB.MemoUpsertAsync(memo);
        if (Result < -1)
        {
            notification.Notify(new NotificationMessage { Severity = NotificationSeverity.Error, Summary = "Error Saving", Detail = "An error saving this record has occured!\n" + dB.LastErrorMsg, Duration = 4000 });
        }
        else
        {
            notification.Notify(new NotificationMessage { Severity = NotificationSeverity.Success, Summary = "Save Success", Detail = NotificationDetailMessage, Duration = 2000 });
            RefreshResults = true;
        }
        busy = false;
        dialog.Close(RefreshResults);
    }

    async Task DeleteMemo(int Idx)
    {  
        int Result;
        bool RefreshResults = false;
        var confirmResult = await dialog.Confirm("Are you sure?", "Confirm Memo Deletion");
        if (confirmResult.HasValue && confirmResult.Value)
        {
            busy = true;
            Result = await dB.MemoDeleteByIdxAsync(Idx);
            if (Result < -1)
            {
                notification.Notify(new NotificationMessage { Severity = NotificationSeverity.Error, Summary = "Error Deleting", Detail = "An error deleting this record has occured!\n" + dB.LastErrorMsg, Duration = 4000 });
            }
            else
            {
                notification.Notify(new NotificationMessage { Severity = NotificationSeverity.Success, Summary = "Deletion Success", Detail = $"Memo {Idx} has been deleted.", Duration = 2000 });
                RefreshResults = true;
            }
        }
        busy = false;
        dialog.Close(RefreshResults);
    }

}

Upvotes: 1

Views: 2665

Answers (3)

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30177

Here's a demo page to show one grid updating and the other not updating.

Note the one updating is referenced with @ref and the button event calls it's Reload method.

@page "/"
<PageTitle>Index</PageTitle>

<h1>Radzen Grid Demo!</h1>

<div class="m-3 text-end">
    <button class="btn btn-primary" @onclick=OnAddCountry>Add USA</button>
</div>

<h3>UpDating Grid</h3>
<RadzenDataGrid Data="@Countries" TItem="Country" @ref=this.grid >
    <Columns>
        <RadzenDataGridColumn TItem="Country" Property="Name" Title="Name" />
        <RadzenDataGridColumn TItem="Country" Property="Continent" Title="Continent" />
    </Columns>
</RadzenDataGrid>

<h3 class="mt-3" >Not Updating Grid</h3>
<RadzenDataGrid Data="@Countries" TItem="Country">
    <Columns>
        <RadzenDataGridColumn TItem="Country" Property="Name" Title="Name" />
        <RadzenDataGridColumn TItem="Country" Property="Continent" Title="Continent" />
    </Columns>
</RadzenDataGrid>

@code {
    private RadzenDataGrid<Country>? grid;
    private Task OnAddCountry()
    {
        Countries.Add(new() { Name = "USA", Continent = "North America" });
        grid?.Reload();
        return Task.CompletedTask;
    }

    public List<Country> Countries = new()
    {
        new Country { Name="UK", Continent="Europe"},   
        new Country { Name="France", Continent="Europe"},
        new Country { Name="Spain", Continent="Europe"},
        new Country { Name="Portugal", Continent="Europe"},
    };

    public record Country
    {
        public string Name { get; init; } = "Not Set";
        public string Continent { get; init; } = "Not Set";
    }
}

Why?

When a component renders, the Renderer decides which sub-components it needs to call SetParametersAsync on based on whether it considers one or more of their parameters have changed. Normal ComponentBase components will re-render because SetParametersAsync makes one or two calls to StateHasChanged.

More complex components such the Radzen grid don't inherit from ComponentBase and have different more complex mechanisms for re-rendering. They need to be told when their base data set has changed because after the first render they don't re-render when SetParametersAsync is called. They are driven by internal events, such as clicking on a sort column, paging,...

Your Code

Your code will need to look something like this:

    <RadzenDataGrid Data="@FoundMemos" TItem="MemoSearch" AllowFiltering="true" AllowSorting="true" AllowColumnResize="true" AllowPaging="true" PageSize=20
                FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive" SelectionMode="DataGridSelectionMode.Single" @bind-Value="@SelectedMemos" RowClick="@OnRowClicked" 
                @ref=this.grid >

//.....

    private RadzenDataGrid<MemoSearch>? grid;


    async Task OpenMemo(int Idx)
    {
        string DialogTitle = (Idx == 0) ? "Create New Memo" : $"Edit Memo {Idx.xToStr()}";

        bool RefreshResults = await dialog.OpenAsync<MemoDetails>(DialogTitle, new Dictionary<string, object>() { { "Idx", Idx } });
        if (RefreshResults)
        {
            await GetMemos(); //this method is called, but no page refresh 
            grid?.Reload();
        }
    }

Upvotes: 0

clamchoda
clamchoda

Reputation: 4961

I believe the problem is here.

    public async Task GetMemos()
    {
        busy = true;
        FoundMemos = await dB.MemoSearchByPageFilterSortAsync(SearchString, PageSize: 9999);
        busy = false;
    }

busy is declared as false and here it get's chagned to true then back to false. Therefore @if (FoundMemos != null && busy == false) never gets satisfied.

Try putting a await Task.Yield() and/or StateHasChanged after you change the first busy=true. Force Blazor to render these changes.

Upvotes: 0

Fitri Halim
Fitri Halim

Reputation: 739

For this method

public async Task GetMemos()
{
        busy = true;
        FoundMemos = await dB.MemoSearchByPageFilterSortAsync(SearchString, PageSize: 9999);
        busy = false;
}

Try changing it to this :

public async Task GetMemos()
{
    busy = true;
    FoundMemos.Clear();
    FoundMemos.AddRange(await dB.MemoSearchByPageFilterSortAsync(SearchString, PageSize: 9999));
    busy = false;
}

You might as well modify other function such as Reset() as well.

    public void Reset()
    {
        FoundMemos.Clear();
        SearchString = "";
    }

Upvotes: 0

Related Questions