简体   繁体   中英

How to implement a State Pattern for Blazor pages using multiple components to build a page?

I have a Blazor page that utilizes multiple components within it - how can I implement a State pattern (ideally per-page) that would be able to handle the current state of a page?

Currently I have all of the state and state-manipulation being done on the page (and via injected Services), but I imagine it would be cleaner to implement a state pattern where each page has some kind of State object which then allows you to manipulate the page and its components in a strict manner.

Ideally the State object would implement INotifyPropertyChanged and be able to dynamically have its State updated, but I also don't hate the idea of having the State object relegate State-manipulation to methods on the object to make sure state isn't just 1-off updated on the Blazor page.

I've already tried to implement some kind of MVVM pattern, but that turned into more questions than answers.

I started to create a State object for the current page being worked on, but I'm not sure if I should basically just be putting most of the logic that was on the Blazor page in the State object, or if I should still have some data, but delegating the heavy lifting to the State.

eg: I have some code that used to be in the "OnAfterRenderAsync" function on the Blazor page, but I'm in the process of moving basically everything in there to a "LoadMatterDetails()" function in the State object that is handling that. Does this make sense, or should I only really have object State in the state object, and writing to & reading from the State object when particular pieces of information are available?

public class MatterDetailsState : IMatterDetailsState
{
    private readonly IMatterDetailsService matterDetailsService;
    private readonly NavigationManager navigationManager;

    public bool EditMode { get; private set; } = false;
    public int EditMatterId { get; private set; } = 0;
    public Matter Matter { get; set; } = new();
    public MatterPaymentOptionDetails PaymentDetails { get; set; } = new();

    public List<MatterStatus> MatterStatuses { get; private set; } = new();


    public MatterDetailsState(
        IAppState appState,
        IMatterDetailsService matterDetailsService,
        NavigationManager navigationManager)
    {
        this.matterDetailsService = matterDetailsService;
        this.navigationManager = navigationManager;
    }

    public async Task LoadMatterDetails()
    {
        // Query Params handling
        var uri = navigationManager.ToAbsoluteUri(navigationManager.Uri);
        var decryptedUri = HelperFunctions.Decrypt(uri.Query);
        var queryParamFound = QueryHelpers.ParseQuery(decryptedUri).TryGetValue("MatterID", out StringValues uriMatterID);

        if (queryParamFound)
        {
            EditMatterId = Convert.ToInt32(uriMatterID);
            EditMode = !String.IsNullOrEmpty(uriMatterID) && EditMatterId > 0;
        }

        await LoadMatterStatuses();

        if (EditMode)
        {
            Matter = await matterDetailsService.GetMatterByIdAsync(EditMatterId);
            PaymentDetails = await matterDetailsService.GetMatterPaymentInfoByMatterId(EditMatterId);
        }
    }

    private async Task LoadMatterStatuses()
    {
        MatterStatuses = await matterDetailsService.GetAvailableMatterStatusesAsync();
    }
}

Basically, should I instead of having more or less the entire function in the State object, or only make the calls like setting Matter & PaymentDetails go through functions in the State object? Not sure what the standard for this is.

I've used Fluxor, which is a Flux/Redux library for Blazor, and have liked it. It holds all your state in an object which you can inject into your component for read access. You then manage state by dispatching actions from your components which are processed by effects or reducers which are essentially methods that process the action and make changes to state. It keeps everything neat, separated and very testable in my experience.

https://github.com/mrpmorris/Fluxor

There isn't a "standard", but applying good coding practices such as the "Single Responsivity Principle" and Clean Design principles drives you in a certain direction.

I divide the presentation and UI code into three:

  1. UI - components and UI logic
  2. State - data that you want to track state on.
  3. Data Management - getting, saving,....

Each represented by one or more objects (Data Management is the ViewModel in MVVM).

You can see an example of this in this answer - https://stackoverflow.com/a/75157903/13065781

The problem is then how do you create a ViewModel instance that is scoped the same as the Form component. You either:

  1. Scope the VM as transient - you can cascade it in the form if sub components need direct access to it. This is the approach in the referenced example.

  2. Create an instance from the IServiceProvider using ActivatorUtilities and deal with the disposal in the form component.

If the VM implements IDisposable/IAsycDisposable the you have to do the second.

The following extension class adds two methods to the IServiceProvider that wrap up this functionality.

public static class ServiceUtilities
{
    public static bool TryGetComponentService<TService>(this IServiceProvider serviceProvider,[NotNullWhen(true)] out TService? service) where TService : class
    {
        service = serviceProvider.GetComponentService<TService>();
        return service != null;
    }

    public static TService? GetComponentService<TService>(this IServiceProvider serviceProvider) where TService : class
    {
        var serviceType = serviceProvider.GetService<TService>()?.GetType();

        if (serviceType is null)
            return ActivatorUtilities.CreateInstance<TService>(serviceProvider);

        return ActivatorUtilities.CreateInstance(serviceProvider, serviceType) as TService;
    }
}

Your form then can look something like this:

public partial class UIForm: UIWrapperBase, IAsyncDisposable
{
    [Inject] protected IServiceProvider ServiceProvider { get; set; } = default!;

    public MyEditorPresenter Presenter { get; set; } = default!;

    private IDisposable? _disposable;

    public override Task SetParametersAsync(ParameterView parameters)
    {
        // overries the base as we need to make sure we set up the Presenter Service before any rendering takes place
        parameters.SetParameterProperties(this);

        if (!initialized)
        {
            // Gets an instance of the Presenter from the Service Provider
            this.Presenter = ServiceProvider.GetComponentService<MyEditorPresenter>() ?? default!;

            if (this.Presenter is null)
                throw new NullReferenceException($"No Presenter could be created.");

            _disposable = this.Presenter as IDisposable;
        }

        return base.SetParametersAsync(ParameterView.Empty);
    }

//....
    public async ValueTask DisposeAsync()
    {
        _disposable?.Dispose();

        if (this.Presenter is IAsyncDisposable asyncDisposable)
            await asyncDisposable.DisposeAsync();
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM