Search code examples
c#razorblazor-server-siderazor-pages

With Blazor access a component from another when they are not parent/child


On a page there are 3 components :

  • One Component1
  • Two Component2

They are NOT parent/child but side by side. I'd like from Component1 hide one or both Component2 via some buttons not present in the code below

Component1:

public partial class Component1 : ComponentBase
{
    [Parameter]
    public Component2 Component2A { get; set; }

    [Parameter]
    public Component2 Component2B { get; set; }
}

Component2

public partial class Component2 : ComponentBase
{
    private bool IsVisible = false;

    public void ToggleVisibility(bool isVisible)
    {
        IsVisible = isVisible;
    }
}

In the page

<Component1 Component2A=rightComponent1 Component2B=component2B />

<Component2 @ref="component2A" />
<Component2 @ref="component2B" />

I get these errors :

The name 'component2A' does not exist in the current context
The name 'component2B' does not exist in the current context

Am I missed something ? I have to define @ref="component2A" and @ref="component2A" somewhere ?

Update 1 based on "MrC aka Shaun Curtis". I'd like pass data (MyModel) to components. When I change a value in the value propagate to all components.

I do this :

My Model

public class MyModel
{
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
}

Page

<CascadingValue Value=_context IsFixed>
    <Component1 ComponentA=this.ComponentAUid ComponentB=this.ComponentBUid />
    <Component2 Uid=this.ComponentAUid><div class="alert alert-danger m-2">Hello from component 1</div></Component2>
    <Component2 Uid=this.ComponentBUid><div class="alert alert-success m-2">Hello from component 2</div></Component2>
</CascadingValue>

@code {

    private XPageContext _context = new XPageContext(new MyModel { FirstName = "MyFirstName" });

    private Guid ComponentAUid = Guid.NewGuid();
    private Guid ComponentBUid = Guid.NewGuid();

}

Record

public record RegisteredComponent(Guid Uid, Func<bool, Task> Show, MyModel MyModel);

Component2

@if(_isVisible)
{
    @this.ChildContent
}

<EditForm Model="MyModel">
<InputText @bind-Value=MyModel.FirstName />
</EditForm>



@code {
    [Parameter] public Guid Uid { get; set; } = Guid.NewGuid();

    [CascadingParameter] private XPageContext _context { get; set; } = default!;

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

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

    private bool _isVisible = false;

    protected override void OnInitialized()
    {
        ArgumentNullException.ThrowIfNull(_context);
        MyModel = _context._myModel;
        _context.Register(Uid, this.ToggleVisibilityAsync);
    }

    public Task ToggleVisibilityAsync(bool isVisible)
    {
        _isVisible = isVisible;
        // Called as this is not a UI event and therefore doesn't cause an automatic render
        StateHasChanged();
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _context.DeRegister(Uid);
    }
}

In the InputText, I see the firstName value in the 2 Component2 but when I change in one there no impact on the second


Solution

  • Firstly, you shouldn't pass around component references: you have no control over their lifecycles and can easily have a reference to a "dead" component.

    You need to create a shared comtext shared by all the components. This solution may seem a litle complex, but it demonstrates the principles of complex inter component communications. You can share a Scoped service, but this doesn't match the scope of a page. Here I use a standard object and pass it down through a cascade.

    The shared Context:

    
    public class XPageContext
    {
        private List<RegisteredComponent> _registeredComponents = new();
    
        public void Register(Guid uid, Func<bool, Task> show)
        {
            if (!_registeredComponents.Any(item => item.Uid == uid))
                _registeredComponents.Add(new RegisteredComponent(uid, show));
        }
    
        public void DeRegister(Guid uid)
        {
            var component = _registeredComponents.FirstOrDefault(item => item.Uid == uid);
    
            if(component != null)
                _registeredComponents.Remove(component);
        }
    
        public async Task ShowComponentAsync(Guid uid, bool show)
        {
            var component = _registeredComponents.FirstOrDefault(item => item.Uid == uid);
    
            if (component != null)
                await component.Show.Invoke(show);
        }
    }
    
    public record RegisteredComponent(Guid Uid, Func<bool, Task> Show);
    

    Component2

    @implements IDisposable
    
    @if(_isVisible)
    {
        @this.ChildContent
    }
    
    @code {
        [Parameter] public Guid Uid { get; set; } = Guid.NewGuid();
        [CascadingParameter] private XPageContext _context { get; set; } = default!;
        [Parameter] public RenderFragment? ChildContent { get; set; }
    
        private bool _isVisible = false;
    
        protected override void OnInitialized()
        {
            ArgumentNullException.ThrowIfNull(_context);
            _context.Register(Uid, this.ToggleVisibilityAsync);
        }
    
        public Task ToggleVisibilityAsync(bool isVisible)
        {
            _isVisible = isVisible;
            // Called as this is not a UI event and therefore doesn't cause an automatic render
            StateHasChanged();
            return Task.CompletedTask;
        }
    
        public void Dispose()
        {
            _context.DeRegister(Uid);
        }
    }
    

    Component1

    <h3>Component1</h3>
    
    <button class="btn btn-dark" @onclick=this.ToggleComponentA>Toggle Component A</button>
    <button class="btn btn-primary" @onclick=this.ToggleComponentB>Toggle Component B</button>
    
    @code {
        [CascadingParameter] private XPageContext _context { get; set; } = default!;
        [Parameter, EditorRequired] public Guid ComponentA { get; set; }
        [Parameter, EditorRequired] public Guid ComponentB { get; set; }
    
        private bool _componentAState;
        private bool _componentBState;
    
        protected override void OnInitialized()
        {
            ArgumentNullException.ThrowIfNull(_context);
        }
    
        private async Task ToggleComponentA()
        {
            _componentAState = !_componentAState;
            await _context.ShowComponentAsync(ComponentA, _componentAState);
        }
     
        private async Task ToggleComponentB()
        {
            _componentBState = !_componentBState;
            await _context.ShowComponentAsync(ComponentB, _componentBState);
        }
    }
    

    Index

    @page "/"
    
    <PageTitle>Index</PageTitle>
    
    <h1>Hello, world!</h1>
    
    Welcome to your new app.
    
    <CascadingValue Value=_context IsFixed>
        <Component1 ComponentA=this.ComponentAUid ComponentB=this.ComponentBUid />
        <Component2 Uid=this.ComponentAUid><div class="alert alert-danger m-2">Hello from component 1</div></Component2>
        <Component2 Uid=this.ComponentBUid><div class="alert alert-success m-2">Hello from component 2</div></Component2>
    </CascadingValue>
    
    @code {
        private XPageContext _context = new();
        private Guid ComponentAUid = Guid.NewGuid();
        private Guid ComponentBUid = Guid.NewGuid();
    }