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