Prototyping an alternative to Model View View Model (MVVM) using Blazor EditContext - SpiralAngle/SpiralAngle.Blazor.Samples.Forms GitHub Wiki

I'm starting to experiment with developing some patterns in Blazor. This post is documenting an in progress experiment/prototype to support loosely coupled Razor components that collaborate in editing a single data. It's not intended to be a final pattern, just the documentation of the current state of the journey.

The problem statement

I wish to be able to have a tool bar that controls putting a form into edit mode, observe changes to the form to know when to enable save, fire off the save action, and then also manage showing Confirm/Cancel Delete when a user clicks the delete button. The tool bar should be completely independent of the component that is presenting the fields to edit and vice versa.

In WPF and related tech, Microsoft has promoted the Model-View-ViewModel pattern as one that can be used to manage the communication between a data entity (the model) and the presentation (view). When you start using multiple view components (such as a calendar widget, a list box, and more), it can get complicated fast if you don't have good approach to how you manage that communication. MVVM helps manage that communication between those widgets while also giving a clear place to manage things like saving state. By abstracting all the management away from the view, it also is far more easy to test behavior.

One of the key concepts in MVVM is that it is event(or observer) driven. This allows you to edit a field in one view component and have another view see that change without the first component even knowing the second component exists or vice versa. This is elegant and SOLID. In a complex app, the extra effort to set up MVVM is generally well offset by the sanity you get using it.

Other patterns looking to solve the same sort of issues include Flux pattern used by Redux which in turn is often used in ReactJs.

Moving into Blazor, I wanted to explore whether there may be an easier, even out of the box, way to achieve many of the benefits of MVVM, even if I gave up some. The thing I tend to value most is the notification of change. The addition of commands is nice, but not the major value. The EditForm and EditContext give me much of what I actually care about. The use of EditForm was there from the beginning in this sample. As part of resolving Issue #19 Componentize GameSystem editor with this commit, I've extracted an EditModel which allows events to notify components that something has changed, adds some properties to also track change, and wraps the EditContext and the IService into a single model. Looking at the interface IEditModel, we can see the operations performed.

    public interface IEditModel<TEntity> where TEntity : class, IEntity
    {
        IService<TEntity> Service { get; }

        EditContext EditContext { get; }

        TEntity Model { get; }

        bool ReadOnly { get; }

        bool WasFromNew { get; }

        bool ConfirmDelete { get; }

        bool HasChanges { get; }

        bool HideConfirmDelete { get; }

        bool HideDelete { get; }

        event Action ModelDeleted;
        event Action ModelSaved;
        event Action EditCanceled;
        event Action DeleteCanceled;
        event Action EditStarted;

        Task InitializeEditorAsync(Guid id);

        Task CancelAsync();

        void CancelDelete();

        void Delete();

        Task DeleteConfirmedAsync();

        void StartEdit();

        Task SaveAsync();
    }

With this, the GameSystemEditor loses almost all logic. It also extracts a generic EditToolBar so we can add that toolbar, with other instances of the edit model, to other editors. On GameSystemEditor, I can add my custom toolbar like so:

 <EditToolbar TEntity="BlazorFormSample.Shared.GameSystem" EditModel ="editModel"></EditToolbar>

And then there is really no code in the EditToolBar itself, just the markup laying out the buttons. If you look at it, the @onclick for each button is calling the proper method on the EditModel and then the buttons are hidden or disabled through ReadOnly and other properties on the model. None of these are wired up in any way to GameSystemEditor and in fact could be placed n a wholly different component as long as they're using the same instance of EditModel as GameSystemEditor and they'd still talk nice.

@typeparam TEntity 
<div class="btn-toolbar" role="toolbar">
    <button class="btn btn-primary" @onclick="EditModel.StartEdit" aria-label="Start Editing" hidden="@(!EditModel.ReadOnly)" disabled="@(!EditModel.ReadOnly)" tabindex="0">
        <span class="oi oi-pencil"></span>
        <span>Edit</span>
    </button>
    <button class="btn btn-primary mr-1" @onclick="EditModel.SaveAsync" aria-label="Save" hidden="@(EditModel.ReadOnly || !EditModel.HasChanges)" disabled="@(!EditModel.HasChanges)" tabindex="0">
        <span class="oi oi-check"></span>
        <span>Save</span>
    </button>
    <button class="btn btn-secondary" @onclick="EditModel.CancelAsync" aria-label="Cancel" hidden="@(EditModel.ReadOnly)" disabled="@(EditModel.ReadOnly)" tabindex="0">
        <span class="oi oi-ban"></span>
        <span>Cancel</span>
    </button>
    <div class="btn-group ml-4" role="group">
        <button class="btn btn-outline-danger" @onclick="EditModel.Delete" aria-label="Delete" hidden="@EditModel.HideDelete" disabled="@EditModel.HideDelete" tabindex="0">
            <span class="oi oi-trash"></span>
            <span>Delete</span>
        </button>
    </div>
    <div class="btn-group ml-4" role="group">
        <button class="btn btn-danger" @onclick="EditModel.CancelDelete" aria-label="Cancel Delete" hidden="@EditModel.HideConfirmDelete" disabled="@EditModel.HideConfirmDelete" tabindex="0">
            <span class="oi oi-bell"></span>
            <span>Cancel Delete</span>
            <span class="oi oi-ban"></span>
        </button>
        <button class="btn btn-outline-danger" @onclick="EditModel.DeleteConfirmedAsync" aria-label="Confirm Delete" hidden="@EditModel.HideConfirmDelete" disabled="@EditModel.HideConfirmDelete" tabindex="0">
            <span class="oi oi-trash"></span>
            <span>Yes, Delete</span>
            <span class="oi oi-trash"></span>
        </button>
    </div>
</div>

@code {

}

The gotcha is that because this uses a generic typeparam that needs a constraint on it, a partial class is needed. The markup file is EditToolbar.razor so the partial class needs to be called EditToolbar.razor.cs

    // partial is needed to add generic constraints.
    // https://github.com/dotnet/aspnetcore/issues/8433
    public partial class EditToolbar<TEntity> where TEntity: class,IEntity
    {
        [Parameter]
        public IEditModel<TEntity> EditModel { get; set; }
    }
}

What isn't built in yet is the handling of collections within the Game System Model. That's still on the GameSystemEditor component, but I'm planning on experimenting with ObservableCollection and maybe a little reflection to wire that up. I am not going to be surprised if there is a better/easier way. For now, the GameSystemEditor is simply notifying the editmodel/context via this kind of code: editModel.EditContext.NotifyFieldChanged(new FieldIdentifier(editModel.Model, "Roles"));. That then raises up the change as if the change was coming through the EditForm itself. Again, I'm still working on it.

One thing I found is that when the EditModel is a property of the current component, the notification of ReadOnly wasn't working. I plan on continuing to work on this and make it more elegant. It's because the page doesn't realize there was a change, so a call to InvokeAsync(StateHasChanged); when ReadOnly changes solves this. Also, a factory or injection needs to be used. And tests need to be added. This is prototyping/exploring in the pattern, so I want to keep it simple!

-- Jim Leonardo, 2020 09 23

⚠️ **GitHub.com Fallback** ⚠️