Blazor store view in cache to reuse

Viewed 27

I created my own modal dialog component to show a modal component on top the main page's content. The dialog's content is renderd as a RenderFragment inside its main <div> section and created on the fly.

I sometimes show another dialog on top of the already existing one - I store the actually rendered RenderFragment and replace it with the new one - so I can go back to the previous dialog once the top-most is closed.

However, when I re-inject the stored component, the component is partially destroyed: some sub-components are missing or re-initialized (e.g. grid columns or data bindings are lost if Content contains a grid). Or if I register events in the component via the attributes, the seem not to operate on the newly displayed component...

Here the code structure:

<div class="modal-body mod-dlg-content" Id="_ModalBody_">
    @Content
</div>

@code {
    public async Task ShowModal(Type contentType, Dictionary<string, object> attributes) {
        // -- if already showing a modal windows => store content (restored when closing new window)
        await StoreCurrentView();
        // --- create instance...
        Content = new RenderFragment(
            x => {
                x.OpenComponent(0, contentType);
                int iCount = 1;
                if (attributes != null) {
                    foreach (var attr in attributes) {
                        x.AddAttribute(iCount, attr.Key, attr.Value);
                        ++iCount;
                    }
                }
                x.AddComponentReferenceCapture(iCount, inst => { ContentRef = (ComponentBase)inst; });
                x.CloseComponent();
        });
        StateHasChanged();
    }
    // --- called when user closes the component
    public async Task OnClose() {
        await RestoreCurrentView();
        // --- set to hidden if content is empty here...
        StateHasChanged();
    }
    // ---- internal class to store modal content
    public class ModalContent {
        protected RenderFragment Content;
        protected ComponentBase ContentRef;

         public void RestoreView(ModalDialog dlg) {
             dlg.Content = this.Content;
             dlg.ContentRef = this.ContentRef;
         }
    }
    protected RenderFragment Content { get; set; }
    protected ComponentBase ContentRef;
    List<ModalContent> _ModalStack = new List<ModalContent>();
    // --- called when showing a modal component on top of the actual one
    async Task StoreCurrentView() {
        await SomeStuff();
        var modalContent = new ModalContent();
        modalContent.ContentRef = dlg.ContentRef;
        modalContent.modalDisplay = dlg.modalDisplay;
        _ModalStack.Add(modalContent);
    }
    // --- called when closing a modal window
    async Task RestoreCurrentView() {
        await SomeStuff();
        int lastId = _ModalStack.Count - 1;
        if (lastId >= 0) {
            ModalContent restore = _ModalStack[lastId];
            _ModalStack.RemoveAt(lastId);
            restore.RestoreView(this);
        }
        else {
            // --- clear previous...
            Content = null;
            ContentRef = null;
        }
    }
}

All works seamlessly prior to storing the view, but after restoring it, although display looks fine, events and bindings are messed up!

Why would my components be re-initialized or re-created? All bindings are at the Content level so I would expect them not be destroyed... Or is there some specific Blazor visual component GC involved here?

1 Answers

Your code is not real, and has inconsistencies, so excuse any assumptions made here.

A RenderFragment is not data, it

Represents a segment of UI content, implemented as a delegate that writes the content to a RenderTreeBuilder.

When you StoreCurrentView all you are doing is storing the delegate method - to be called later - when it will create new content on the RenderTree. The ContentRef will not be valid as the component is no longer in the RenderTree.

Your simplest option would be to render a stack of dialogs, so that you don't have to try and stash and restore like this.

You could always use CSS to hide individual items in the stack if you prefer, but you need to bear in mind that when something is removed from the RenderTree like you are doing, it is going to be disposed.

Related