microsoft / fluentui-blazor

Microsoft Fluent UI Blazor components library. For use with ASP.NET Core Blazor applications
https://www.fluentui-blazor.net
MIT License
3.78k stars 363 forks source link

Help: My FluentCheckbox value re-write by re-render #1290

Closed kamazheng closed 9 months ago

kamazheng commented 9 months ago

πŸ› Help

When I update the checkbox, it continue to re-render the page, repeat check and uncheck, why?

πŸ’» Repro or Code Sample


@using Kimi.FluentUiExtension.Components
@using Kimi.NetExtensions.Model.Identities
@inherits BaseCrud<Role>

@{
    base.BuildRenderTree(__builder);
}

<SectionContent SectionName="@UserActionSectionName">
</SectionContent>

<SectionContent SectionName="@FormControllerSectionName">
    <CascadingValue Value="@BaseCrudEditForm?.EditContext">

        <FluentGrid>
            <FluentGridItem md="12">
                <FluentTextField @bind-Value="@CurrentModel.Name" Label="@PropertyExtentions.GetDisplayLabel(()=>CurrentModel.Name)"></FluentTextField>
                <ValidationMessage For="@(()=>CurrentModel.Name)"></ValidationMessage>
            </FluentGridItem>

            <FluentGridItem md="12">
                <FluentGrid>
                    @if (AllPermissions?.Any() == true)
                    {
                        @foreach (var permisson in PermissionAllNames)
                        {
                            var isChecked = RoleWithPermissionNames.Contains(permisson);
                            <FluentGridItem xl="12" md="3">
                                <FluentCheckbox Label="@permisson"
                                                @bind-Value:get="@isChecked"
                                                @bind-Value:set="@(e => OnPermissionChecked(permisson, e))">
                                </FluentCheckbox>
                            </FluentGridItem>
                        }
                    }

                </FluentGrid>

            </FluentGridItem>

        </FluentGrid>

    </CascadingValue>
</SectionContent>
using CDU_OIS_NEW.Models.Default;
using CDU_OIS_NEW.Service;
using Kimi.NetExtensions.Model.Identities;
using Kimi.NetExtensions.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.EntityFrameworkCore;
using System.Collections.Immutable;
using System.Diagnostics;

namespace CDU_OIS_NEW.Components.Pages;

public partial class RolePage
{
    public bool isDefaultRole { get; set; }
    public List<Permission> AllPermissions { get; set; } = null!;
    private ImmutableArray<string> PermissionAllNames;

    private HashSet<string> RoleWithPermissionNames = new HashSet<string>();

    protected override async Task OnInitializedAsync()
    {
        if (_value != default && _value.RolePk != default)
        {
            CurrentModel = await new DefaultContext(_user).Roles.Include(r => r.RolePermissions).FirstOrDefaultAsync(r => r.RolePk == _value.RolePk);
        }
        else
        {
            CurrentModel = new Role();
        }
        OriginalModel = CurrentModel.JsonCopy();
        AllPermissions = ResourcePermission.GetAllResourcePermissions();
        PermissionAllNames = ImmutableArray.Create(AllPermissions.Select(p => p.Name).ToArray());
        var defaultRoles = typeof(DefaultRoles).GetProperties().Select(x => x.Name).ToList();
        isDefaultRole = defaultRoles.Contains(CurrentModel.Name);

        loadRolePermissions();
        await base.OnInitializedAsync();
    }

    public override Task SetParametersAsync(ParameterView parameters)
    {
        foreach (var parameter in parameters)
        {
            Debug.WriteLine($"{parameter.Name}={parameter.Value}");
        }
        return base.SetParametersAsync(parameters);
    }

    protected override Task OnAfterRenderAsync(bool firstRender)
    {
        return base.OnAfterRenderAsync(firstRender);
    }

    private void loadRolePermissions()
    {
        RoleWithPermissionNames = CurrentModel.RolePermissions
            .Select(r => r.PermissionName)
            .ToHashSet();
    }

    private async Task OnPermissionChecked(string permission, bool isChecked)
    {
        if (isChecked)
        {
            RoleWithPermissionNames.Add(permission);
        }
        else
        {
            RoleWithPermissionNames.Remove(permission);
        }
        await Task.Delay(5);
    }
}
using Blazored.FluentValidation;
using Kimi.FluentUiExtension.Localization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Http;
using Microsoft.FluentUI.AspNetCore.Components;
using System.Diagnostics;

namespace Kimi.FluentUiExtension.Components;

public partial class BaseCrud<T> : DataBindingBaseComponent<T>
{
    [Parameter]
    public EventCallback<BaseCrud<object>> AfterSubmitCallback { get; set; }

    [Parameter]
    public EventCallback<BaseCrud<object>> OnCloseCallback { get; set; }

    /// <summary>
    /// if generic type, need to pass in model type of create new
    /// </summary>
    [Parameter, EditorRequired]
    public Type InputModelType { get; set; } = null!;

    [Parameter]
    public object? UserData { get; set; } = null!;

    [Parameter]
    public bool ShowValidationSummary { get; set; } = false!;

    [Parameter]
    public bool AsDetailPage { get; set; } = false;

    [Inject]
    public KimiJsInterop JsInterop { get; set; } = null!;

    [Inject]
    public IToastService _toastService { get; set; } = null!;

    public EditForm? BaseCrudEditForm;
    public T CurrentModel { get; set; } = default!;
    public T OriginalModel = default!;

    protected string UserActionSectionName = Identifier.NewId();
    protected string FormControllerSectionName = Identifier.NewId();
    protected string userFormControllerDivId = Identifier.NewId();

    protected bool isWorking = false;
    protected bool isReadOnly = true;
    protected bool isNewModel = false;

    protected SemaphoreSlim semaphore { get; set; } = new SemaphoreSlim(1);

    protected string baseCrudFormId = string.Empty;
    protected FluentValidationValidator? _fluentValidationValidator;

    protected override void OnInitialized()
    {
        if (_value != null)
        {
            CurrentModel = (T)_value!.JsonCopy(InputModelType)!; 
            OriginalModel = (T)_value!.JsonCopy(InputModelType)!;
        }
        else
        {
            CurrentModel = _value = (T)Activator.CreateInstance(InputModelType)!;
            isReadOnly = false;
            isNewModel = true;
        }

        baseCrudFormId = Guid.NewGuid().ToString();
        base.OnInitialized();
    }

    public override Task SetParametersAsync(ParameterView parameters)
    {
        foreach (var parameter in parameters)
        {
            Debug.WriteLine($"{parameter.Name}={parameter.Value}");
        }
        return base.SetParametersAsync(parameters);
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JsInterop.setNotScrollMaxHeight(baseCrudFormId, 35);
            await JsInterop.setDivReadonly(userFormControllerDivId, isReadOnly);
        }
        base.OnAfterRender(firstRender);
    }

    private BaseCrud<object> toDynamicBaseCrud()
    {
        var baseCrudObject = new BaseCrud<object>()
        {
            //UserData = UserData,
            //_value = _value,
            //InputModelType = InputModelType,
            //OriginalModel = OriginalModel,
            //CurrentModel = CurrentModel,
        };
        //baseCrudObject.UserData = this.UserData;
        return baseCrudObject;
    }

    private async Task ValidHandlerAsync()
    {
        try
        {
            var waitSuccess = await semaphore.WaitAsync(0);
            if (!waitSuccess)
            {
                return;
            }
            if (CurrentModel.ToJson() == OriginalModel.ToJson())
            {
                return;
            }
            isWorking = true;

            await OnSubmit();
            if (AfterSubmitCallback.HasDelegate)
            {
                await AfterSubmitCallback.InvokeAsync(toDynamicBaseCrud());
            }
        }
        catch (Exception ex)
        {
            await _dialogService.ShowErrorAsync(ex.Message);
        }
        finally
        {
            isWorking = false;
            semaphore.Release();
        }
    }

    private async Task OnDeleteClicked()
    {
        try
        {
            isWorking = true;
            var waitSuccess = await semaphore.WaitAsync(0);
            if (!waitSuccess)
            {
                return;
            }
            await OnDelete();
        }
        catch (Exception ex)
        {
            await _dialogService.ShowErrorAsync(ex.Message);
        }
        finally
        {
            semaphore.Release();
            isWorking = false;
        }
    }

    protected virtual async Task OnEdit()
    {
        isReadOnly = false;
        await JsInterop.setDivReadonly(userFormControllerDivId, isReadOnly);
    }

    protected virtual async Task OnClose()
    {
        if (CurrentModel.ToJson() != OriginalModel.ToJson() && !isReadOnly)
        {
            var dialog = await _dialogService.ShowConfirmationAsync(L.TheUnsavingDataWillLost, L.Yes, L.No);
            var result = await dialog.Result;
            if (result.Cancelled)
            {
                return;
            }
        }
        if (OnCloseCallback.HasDelegate)
        {
            await OnCloseCallback.InvokeAsync(toDynamicBaseCrud());
        }
    }

    protected virtual async Task OnDelete()
    {
        var dialog = await _dialogService.ShowConfirmationAsync(L.AreYouSureToDelete, L.Yes, L.No);
        var result = await dialog.Result;
        if (result.Cancelled)
        {
            return;
        }
        var pkValue = Value.GetPkValue(_user)!;
        var delResult = await GenericTableTools.Delete(new RecordQuery
        {
            Id = pkValue!,
            TableClassFullName = Value.GetType().FullName!,
        }, _user);
        if (delResult.StatusCode == StatusCodes.Status200OK)
        {
            _toastService?.ShowSuccess($"{L.DeleteSuccess} - {Value.GetType().Name}:{pkValue}");
        }
        else
        {
            await _dialogService.ShowErrorAsync($"{Value.GetType().Name}:{pkValue}", L.DeleteFailed);
            return;
        }

        if (AfterSubmitCallback.HasDelegate)
        {
            await AfterSubmitCallback.InvokeAsync(toDynamicBaseCrud());
        }
    }

    protected virtual async Task OnSubmit()
    {
        var dialog = await _dialogService.ShowConfirmationAsync(L.AreYouSureToSubmit, L.Yes, L.No);
        var result = await dialog.Result;
        if (result.Cancelled)
        {
            return;
        }

        var pkValue = isNewModel ? 0 : Value.GetPkValue(_user)!;
        var subResult = await GenericTableTools.Upsert(new UpsertBody
        {
            TableClassFullName = InputModelType.FullName!,
            JsonRecord = CurrentModel.ToJson(),
        }, _user);

        if (subResult.StatusCode == StatusCodes.Status200OK)
        {
            if (isNewModel)
            {
                var returnObj = subResult.Value!.ToString()!.ToType(InputModelType);
                pkValue = returnObj.GetPkValue(_user)!;
            }
            _toastService?.ShowSuccess($"{L.Success} - {InputModelType.Name}:{pkValue}");
        }
        else
        {
            await _dialogService.ShowErrorAsync($"{InputModelType.Name}:{pkValue}", L.Failed);
            return;
        }

        if (AfterSubmitCallback.HasDelegate)
        {
            await AfterSubmitCallback.InvokeAsync(toDynamicBaseCrud());
        }
    }
}

πŸ€” Expected Behavior

😯 Current Behavior

Cannot update the check state, repeat re-render the page. OnAfterRenderAsync() and OnPermissionChecked() in dead loop. image

🌍 Your Environment

kamazheng commented 9 months ago

I move the FluentCheckBox outside of the EditForm, it works. Strange.

vnbaaij commented 9 months ago

There are examples available on how to handle a FluentCheckbox which do not exhibit your problem. What do you expect us to do with the code you posted? I'm sorry, but again there is no code with which we can reproduce your issue. Closing this.