Search code examples
c#razorblazorblazor-webassembly

How to dynamically build parent and child components with rendertreebuilder?


I need help with dynamically building/generating multistep component with rendertreebuilder.

I have a two components: MultistepComponent (parent) and MultiStepNavigation (child). I also have a component builder class to build these components.

Rendering components does not work, I get an error:

Object reference not set to an instance of an object.

That is because the MultiStepComponent property in MultiStepNavigation is empty. Using a component in razor page does not give this error.

I will be posting a lot of code, but you don't have to read every detail. I just want to give all the information.

MultiStepComponent.razor

<CascadingValue Value="this">
    <div id="@Id">
        <ul class="nav nav-pills nav-justified">
            @foreach (var step in Steps)
            {
                <li id="step-@(StepsIndex(step) + 1)" class="nav-item">
                    <a class="nav-link @((ActiveStep == step) ? "active" : "")" href="javascript: void(0)"
                       @onclick="@(e=> SetActive(step))">@step.Name</a>
                </li>
            }
        </ul>
        <div id="container-fluid">
            <div class="navigatingBtns">
                <button class="btn btn-primary btn-lg" type="button"
                        disabled="@(ActiveStepIndex == 0)" @onclick="GoBack">
                    Previous
                </button>
                <button class="btn btn-primary btn-lg"
                        type="@(IsLastStep ? "submit" : "button")" @onclick="GoNext">
                    @(IsLastStep ? "Submit" : "Next")
                </button>
            </div>
            @ChildContent
        </div>
        </div>
</CascadingValue>

MultiStepComponent.razor.cs

public partial class MultiStepComponent { 

        protected internal List<MultiStepNavigation> Steps = new List<MultiStepNavigation>();

        [Parameter]
        public string Id { get; set; }

        [Parameter]
        public RenderFragment? ChildContent { get; set; }

        [Parameter]
        public MultiStepNavigation? ActiveStep { get; set; }

        [Parameter]
        public int ActiveStepIndex { get; set; }

        public bool IsLastStep { get; set; }

        protected internal void GoBack()
        {
            if (ActiveStepIndex > 0)
            {
                SetActive(Steps[ActiveStepIndex - 1]);
            }
        }

        protected internal void GoNext()
        {
            if (ActiveStepIndex < Steps.Count - 1)
            {
                SetActive(Steps[(Steps.IndexOf(ActiveStep) + 1)]);
            }
        }

        protected internal void SetActive(MultiStepNavigation step)
        {
            ActiveStepIndex = StepsIndex(step);
            if (ActiveStepIndex == Steps.Count - 1)
            {
                IsLastStep = true;
            }
            else
            {
                IsLastStep = false;
            }
        }

        public int StepsIndex(MultiStepNavigation step) => StepsIndexInternal(step);
        protected int StepsIndexInternal(MultiStepNavigation step)
        {
            return Steps.IndexOf(step);
        }

        protected internal void AddStep(MultiStepNavigation step)
        {
            Steps.Add(step);
        }

        protected override void OnAfterRender(bool firstRender)
        {
            if (firstRender)
            {
                SetActive(Steps[0]);
                StateHasChanged();
            }
        }
    }

MultiStepNavigation.razor

@if (MultiStepComponent.ActiveStep == this) { 
    <div id="step-@(MultiStepComponent.StepsIndex(this) + 1)">
        @ChildContent
    </div>
}

MultiStepNavigation.razor.cs

public partial class MultiStepNavigation
    {
        [CascadingParameter]
        protected internal MultiStepComponent MultiStepComponent { get; set; }

        [Parameter]
        public RenderFragment ChildContent { get; set; } = default!;

        [Parameter]
        public string Name { get; set; } = "";

        protected override void OnInitialized()
        {
            if(MultiStepComponent != null)
            {
                MultiStepComponent.AddStep(this);
            }
        }
    }

code to build componnts:

renderFragment = b =>
            {
                b.OpenComponent<MultiStepComponent>(0);
                b.AddAttribute(1, "id", "MultiStepContainer");
                b.OpenComponent<MultiStepNavigation>(2);
                b.AddAttribute(3, "Name", "First Step");
                b.CloseComponent();
                b.CloseComponent();
            };

Making a component on razor page myself does not give an error. It looks like this:

<MultiStepComponent Id="MultiStepContainer">
                <MultiStepNavigation Name="First Step"></MultiStepNavigation>    
            </MultiStepComponent>

What did I do wrong??


Solution

  • You code doesn't take account of the fact that MultiStepNavigation is child content of MultiStepComponent.

    Look in /obj/debug/net5.0/razor/..., find your razor version of your component and look at the code.

    You probably need to do something like this:

    private RenderFragment _MultiStepNavigationFragment => b =>
    {
            b.OpenComponent<MultiStepNavigation>(0);
            b.AddAttribute(1, "Name", "First Step");
            b.AddAttribute(2, "ChildContent", this.ChildContent);
            b.CloseComponent();
    };
    
    private RenderFragment _MultiStepComponentFragment => b =>
    {
            b.OpenComponent<MultiStepComponent>(0);
            b.AddAttribute(1, "id", "MultiStepContainer");
            b.AddAttribute(2, "ChildContent", this._MultiStepNavigationFragment);
            b.CloseComponent();
    };