1
votes

Issue Context

I have a blazor component to wrap bootstrap modal with some basic binding.

I have two components that wrap EditForm components that I expose via this modal component: ConversionForm.razor and ConversionInputForm.razor.

I then use these two form/modal components on a page with some simple buttons (not using bootstrap modal data binding).

Issue

Add Conversion Input button causes the modal to appear and works properly. The Add Conversion button causes the modal to appears then disappears and leaves the modal backdrop.

Troubleshooting

I added a Console.WriteLine to the OnAfterRenderAsync of the modal component to see if there is a difference and the only one I noticed is that the conversion-modal.OnAfterRenderAsync(True) is rendered after the modal.ShowAsync method on the one that disappears.

I'm wondering if there is something about the binding behavior that is causing that behavior as I believe the firstRender=true should only happen once during initial load of the page of the component.

To replicate this behavior, create a new blazorserver app dotnet new blazorserver and add/modify the files in this gist.

The console output for what works:

conversion-modal.OnAfterRenderAsync(True) [1]
conversion-input-modal.OnAfterRenderAsync(True) [1]
conversion-input-modal.ShowAsync()
conversion-input-modal.OnAfterRenderAsync(False) [2]
conversion-modal.OnAfterRenderAsync(False) [2]
conversion-input-modal.OnAfterRenderAsync(False) [3]

The console output for what is broken:

conversion-modal.OnAfterRenderAsync(True) [1]
conversion-input-modal.OnAfterRenderAsync(True) [1]
conversion-modal.ShowAsync()
conversion-modal.OnAfterRenderAsync(True) [1]  <<-------------
conversion-modal.OnAfterRenderAsync(False) [2]
conversion-input-modal.OnAfterRenderAsync(False) [2]

The 4th line conversion-modal.OnAfterRenderAsync(True) [1] is what I am suspicious of, it's like the component was reinitialized. Not sure why or if this is even part of the problem.

Help is appreciated.

Update

I added OnInitializedAsync console output to the Modal and confirmed that the modal is getting reinstantiated by the framework for some reason, again now sure this is causing the issue but is suspect till I find another avenue of troubleshooting.

Add Conversion Input (WORKING) Output

conversion-modal.OnInitializedAsync() [1]
conversion-input-modal.OnInitializedAsync() [1]
conversion-modal.OnInitializedAsync() [1]
conversion-input-modal.OnInitializedAsync() [1]
conversion-modal.OnAfterRenderAsync(True) [1]
conversion-input-modal.OnAfterRenderAsync(True) [1]
conversion-input-modal.ShowAsync()
conversion-input-modal.OnAfterRenderAsync(False) [2]
conversion-modal.OnAfterRenderAsync(False) [2]
conversion-input-modal.OnAfterRenderAsync(False) [3]

Add Conversion (BROKEN) Output

conversion-modal.OnInitializedAsync() [1]
conversion-input-modal.OnInitializedAsync() [1]
conversion-modal.OnInitializedAsync() [1]
conversion-input-modal.OnInitializedAsync() [1]
conversion-modal.OnAfterRenderAsync(True) [1]
conversion-input-modal.OnAfterRenderAsync(True) [1]
conversion-modal.ShowAsync()
conversion-modal.OnInitializedAsync() [1]  <<-------------
conversion-modal.OnAfterRenderAsync(True) [1]  <<---------
conversion-modal.OnAfterRenderAsync(False) [2]
conversion-input-modal.OnAfterRenderAsync(False) [2]
1
Just for kicks, I made ConversionForm look more like ConversionInputForm by removing the Conversion object and using the ConversionForm to bind to public properties and this "fixes" the issue, but why...Adam Weigert
I think this is the EditForm component recreating the child contents when the model changes as this changes the EditContext ...Adam Weigert
I am pretty sure because EditContext gets replaced when the model changes during render, this results in the modal being removed even though it was just shown, a new modal getting generating which is hidden by default, thus leaving a backdrop. Reproduced the issue by binding the EditContext instead of Model on the EditForm and replace the EditContext which each call.Adam Weigert

1 Answers

0
votes

The best way I have found around why the EditForm is rebuilding the child controls is to move the EditForm below the modal div. This way the modal div is not removed/added, only the body/footer of the modal, which is fine.

ModalEditForm.razor

@inherits ComponentBase
@inject IJSRuntime JSRuntime

<div class="modal" tabindex="-1" role="dialog" id="@Id" @attributes="Attributes">
    <div class="modal-dialog modal-dialog-centered" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">@Title</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">×</span>
                </button>
            </div>
            <EditForm EditContext="@EditContext" Model="@Model" OnSubmit="@OnSubmit" OnInvalidSubmit="@OnInvalidSubmit" OnValidSubmit="@OnValidSubmit">
                @if (!(Validators is null))
                {
                    @Validators
                }

                <div class="modal-body">
                    @Body
                </div>
                @if (!(Footer is null))
                {
                    <div class="modal-footer">
                        @Footer
                    </div>
                }
            </EditForm>
        </div>
    </div>
</div>

@code {
    [Parameter]
    public string Id { get; set; } = "modal";

    [Parameter]
    public string Title { get; set; } = string.Empty;

    [Parameter] 
    public object Model { get; set; }

    [Parameter] 
    public EditContext EditContext { get; set; }

    [Parameter]
    public RenderFragment Validators { get; set; }

    [Parameter]
    public RenderFragment Body { get; set; }

    [Parameter]
    public RenderFragment Footer { get; set; }

    [Parameter] 
    public EventCallback<EditContext> OnSubmit { get; set; }

    [Parameter]
    public EventCallback<EditContext> OnValidSubmit { get; set; }

    [Parameter]
    public EventCallback<EditContext> OnInvalidSubmit { get; set; }

    [Parameter(CaptureUnmatchedValues = true)]
    public Dictionary<string, object> Attributes { get; set; } = new Dictionary<string, object>();

    public async Task ShowAsync()
        => await JSRuntime.InvokeVoidAsync("showModal", Id).ConfigureAwait(false);

    public async Task HideAsync()
        => await JSRuntime.InvokeVoidAsync("hideModal", Id).ConfigureAwait(false);
}