Dennis Jones
Dennis Jones

Reputation: 195

Blazor: How to use a component from 2 different pages with 2 different states

In a Blazor application, I want to use a component from two different pages. The component uses a service to maintain its state. But each page needs to have the component use a different state. Here's how I thought I would do it (using the default Counter component to demonstrate).

CounterService.cs

namespace TestTwoInterfaceToOneService.Shared
{
    public interface ICounterServiceA
    {
        int CurrentCount { get; set; }
        void IncrementCount();
    }

    public interface ICounterServiceB
    {
        int CurrentCount { get; set; }
        void IncrementCount();
    }

    public class CounterService : ICounterServiceA, ICounterServiceB
    {
        public int CurrentCount { get; set; }
        public void IncrementCount()
        {
            CurrentCount++;
        }
    }
}

In Program.cs add:

builder.Services.AddScoped<ICounterServiceA, CounterService>();
builder.Services.AddScoped<ICounterServiceB, CounterService>();

Counter.razor

<h1>Counter</h1>
<p>Current count: @CounterService.CurrentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    [Parameter]
    public CounterService CounterService { get; set; }

    private void IncrementCount()
    {
        CounterService.CurrentCount++;
    }
}

PageA.razor

@page "/pagea"
@inject ICounterServiceA CounterServiceA

<h3>Page A</h3>
<Counter CounterService="CounterServiceA" /> @*<-- Error: Cannot convert from ICounterServiceB to CounterService*@

PageB.razor

@page "/pageb"
@inject ICounterServiceB CounterServiceB

<h3>Page B</h3>
<Counter CounterService="CounterServiceB" /> @*<-- Error: Cannot convert from ICounterServiceB to CounterService*@

I get the error 'Cannot convert from ICounterServiceB to CounterService' when I try to pass the service to the component. I've discovered that using 2 identical interfaces pointing to the same concrete implementation does indeed give me 2 scoped instances. However, I cannot figure out how to pass those instances into the component.

Is there a piece that I'm missing? Or, should this be done some other way?

SOLUTION

Using a combination of the answer from Henk and the comments from Brian, the solution I came up with is:

CounterService.cs

namespace TestTwoInterfaceToOneService.Shared
{
    public class CounterService
    {
        public int CurrentCount { get; set; }
        public void IncrementCount()
        {
            CurrentCount++;
        }
        public void ResetCount()
        {
            CurrentCount = 0;
        }
    }
}

CounterStateService.cs

using System.Collections.Generic;
namespace TestTwoInterfaceToOneService.Shared
{
    public interface ICounterStateService
    {
        CounterService this[string key] { get; }
    }

    public class CounterStateService : ICounterStateService
    {
        private Dictionary<string, CounterService> counterServices = new();
        public CounterService this[string key]
        {
            get
            {
                if (!counterServices.ContainsKey(key))
                {
                    counterServices.Add(key, new CounterService());
                }
                return counterServices[key];
            }
        }
    }
}

In Program.cs, add

    builder.Services.AddScoped<ICounterStateService, CounterStateService>();

Counter.razor

<p>Current count: @CounterService.CurrentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Increment</button>

@code {
    [Parameter]
    public CounterService CounterService { get; set; }

    private void IncrementCount()
    {
        CounterService.CurrentCount++;
    }
}

PageA.razor

@page "/pagea"
@inject ICounterStateService CounterStateService

<h3>Page A</h3>
<Counter CounterService='CounterStateService["A"]' />
<button class="btn btn-primary" @onclick='CounterStateService["A"].ResetCount'>Reset Count</button>

PageB.razor

@page "/pageb"
@inject ICounterStateService CounterStateService

<h3>Page B</h3>
<Counter CounterService='CounterStateService["B"]' />
<button class="btn btn-primary" @onclick='CounterStateService["B"].ResetCount'>Reset Count</button>

Upvotes: 0

Views: 1112

Answers (1)

Henk Holterman
Henk Holterman

Reputation: 273494

Or, should this be done some other way?

Yes. Baking this usage pattern into the Type structure isn't going to adapt or scale well. What if you need a third page, or want to make it dynamic?

You could use a wrapper service and a scheme with a key to bind a State to a Component:

public class CounterService  { ... }    // no need to register

public class CounterStateService  // register for injection
{

    private Dictionary <string, CounterService> stateLookup = new();

    public CounterService  this[string key] 
    {
      if (! stateLookup.tryGetValue (key, out CounterService  service))
      {
         service = new CounterService();
         stateLookup.Add(key, service);
      }
      return service;
    }
}

and use it like

@page "/pagea"
@inject CounterStateService  CounterStateService

<h3>Page A</h3>
<Counter CounterService="CounterStateService["A"]" />

Upvotes: 1

Related Questions