Jinjinov
Jinjinov

Reputation: 2683

List of Blazor components that implement an interface

I am building a reusable Razor class library for Blazor components.

I have an interface that any Blazor component could implement:

public interface IControllableComponent
{
    void SetSomeValue(int someValue);
}

How can I get a List of all components that implement that interface?

public interface IControllableComponentManager
{
    void SetSomeValueForAllControllableComponents(int someValue);
}

public class ControllableComponentManager : IControllableComponentManager
{
    IList<IControllableComponent> _controllableComponentList;

    public ControllableComponentManager()
    {
        _controllableComponentList = ??? // how to populate this list?
    }

    public void SetSomeValueForAllControllableComponents(int someValue)
    {
        foreach (var controllableComponent in _controllableComponentList)
        {
            controllableComponent.SetSomeValue(someValue);
        }
    }
}

I want for my reusable Razor class library to be used in the code behind in different projects:

public class MyControllableComponentBase : ComponentBase, IControllableComponent
{
    protected int _someValue;

    public void SetSomeValue(int someValue)
    {
        _someValue = someValue;
    }
}

Is there a way to populate _controllableComponentList in ControllableComponentManager?

I would like to do this in a clean, proper way, without using reflection. Preferably in the style of ASP.NET Core with dependency injection.

The problem is that Blazor components are instantiated in Razor markup as <Component></Component> so I don't know how to get their references to add them to the List<>.

Upvotes: 3

Views: 4636

Answers (3)

enet
enet

Reputation: 45626

I guess you can do it as the following:

  1. Define a service that stores a collection of the interface type
  2. Expose a method to add a component, a method to remove a component, an event delegate to notify of addition, removal, etc.
  3. Inject the service into the component you want to add itself to the service, and in the OnInitialized method do something like this:

    protected override void OnInitialized()
    {
        MyComponents.AddComponent(this);
        this.MyProperty = "I was born with the sun...";
    }  
    

Upvotes: 4

Jinjinov
Jinjinov

Reputation: 2683

You can get a Blazor component reference using @ref but you can do that only within Razor markup.

To use a reference to a Blazor component outside Razor markup, the component must register itself.

To do that, you have to use a partial class for the component in the code behind:

public partial class ComponentContainer : ComponentBase, IComponentContainer
{
    public Type ComponentType { get; protected set; }

    public RenderFragment Component { get; set; }

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

    [Inject]
    protected IComponentContainerManager ComponentContainerManager { get; set; }

    public void SetComponentType(Type componentType)
    {
        ComponentType = componentType;

        StateHasChanged();
    }

    protected override void OnInitialized()
    {
        ComponentContainerManager.RegisterComponentContainer(Name, this);

        Component = builder =>
        {
            if (ComponentType != null)
            {
                builder.OpenComponent(0, ComponentType);
                builder.CloseComponent();
            }
        };
    }
}

Use [Inject] attribute to use dependency injection to inject a service where the component can register.

Use the protected override void OnInitialized() of the ComponentBase to register the component in the registration service.

public interface IComponentContainer
{
    Type ComponentType { get; }
    void SetComponentType(Type componentType);
}

public interface IComponentContainerManager
{
    void RegisterComponentContainer(string componentContainerName, IComponentContainer componentContainer);
    void SetComponentType(string componentContainerName, Type componentType);
    Type GetComponentType(string componentContainerName);
}

public class ComponentContainerManager : IComponentContainerManager
{
    readonly IDictionary<string, IComponentContainer> _componentContainerDict = new Dictionary<string, IComponentContainer>();

    public void RegisterComponentContainer(string componentContainerName, IComponentContainer componentContainer)
    {
        _componentContainerDict[componentContainerName] = componentContainer;
    }

    public void SetComponentType(string componentContainerName, Type componentType)
    {
        _componentContainerDict[componentContainerName].SetComponentType(componentType);
    }

    public Type GetComponentType(string componentContainerName)
    {
        return _componentContainerDict[componentContainerName].ComponentType;
    }
}

Now you can use IComponentContainerManager anywhere in your code.

Upvotes: 4

poke
poke

Reputation: 387677

If you want to dynamically create components, then you can use the RenderFragment concept for this. A render fragment is essentially a function that uses a RenderTreeBuilder to create a component. This is what your Razor components are compiled to when you author .razor files.

For example, the following code creates a RenderFragment that just renders a single component:

Type componentType = typeof(MyControllableComponentBase);
RenderFragment fragment = builder =>
{
    builder.OpenComponent(0, componentType);
    builder.CloseComponent();
};

You could then assign that fragment to a property and render it dynamically within your Razor component:

<div>
   @Fragment
</div>

@code {
    public RenderFragment Fragment
    { get; set; }
}

This will solve the issue how to create components dynamically when you just know their type. However, it will not solve the problem that you want to call SetSomeValue on the component instances. For that, you will have to understand that you don’t really have control over the component instances: The render tree builder is responsible for creating the components from that virtual markup, so you don’t ever call new ComponentType(). Instead, you rely on the renderer to create the instance for you and then you could take a reference to the used instance and work with that.

You can use the @ref directive to capture references to rendered component instances:

<MyControllableComponent @ref="controllableComponent" />

@code {
    private MyControllableComponent controllableComponent;

    private void OnSomething()
    {
        controllableComponent.SetSomeValue(123);
    }
}

Upvotes: 1

Related Questions