This is the fifth in a short series of blog posts where I will go beyond the introductory level and dig a bit deeper into using the Fluxor library in a Blazor Wasm project.
EditForm Databinding
So far we've spent a lot of time taking advantage of the unidirectional data flow inherent in the Flux pattern.
But one of the major benefits of using Blazor is being able to take advantage of your C# classes and .NET functionality in the browser as well as on the server.
A specific example is using an AspNetCore EditForm, binding it a Model class, and validating it automatically, both in the browser and on the server.
EditForm
, however, is based on two-way databinding to its Model
. Binding the form fields directly to a read-only State
object would not work.
To illustrate, let's create a UserFeedback feature in our application. Consider the following requirement:
A user can submit a feedback form, with three fields: Email Address, Rating (1 through 10), and a Comment. Upon successful submit, hide the form and display a success message.
It would be tempting to begin by creating a UserFeedbackState
record with those properties:
public record UserFeedbackState
{
public string EmailAddress { get; init; }
public int Rating { get; init; }
public string Comment { get; init; }
}
But it wouldn't take long to realize that the read-only nature of the record
with init
methods in place of set
makes two-way binding to the EditForm
impossible.
Instead, we need a traditional model class, where we can take advantage of DataAnnotations
. We'll create this model and place it in the Shared
project so that it can be referenced by the Blazor front-end and also by the server ApiController:
using System.ComponentModel.DataAnnotations;
namespace BlazorWithFluxor.Shared
{
public class UserFeedbackModel
{
[EmailAddress]
[Required]
[Display(Name = "Email Address")]
public string EmailAddress { get; set; }
[Required]
public int Rating { get; set; }
[MaxLength(100)]
public string Comment { get; set; }
public UserFeedbackModel()
{
EmailAddress = string.Empty;
Rating = 1;
Comment = string.Empty;
}
}
}
Back in the Client
project, let's put in place the folder structure \Features\UserFeedback
with subfolders for \Pages
and \Store
.
Create a UserFeedbackStore.cs
file in the \Store
folder, and begin with a UserFeedbackState and its Feature:
public record UserFeedbackState
{
public bool Submitting { get; init; }
public bool Submitted { get; init; }
public string ErrorMessage { get; init; }
public UserFeedbackModel Model { get; init; }
}
public class UserFeedbackFeature : Feature<UserFeedbackState>
{
public override string GetName() => "UserFeedback";
protected override UserFeedbackState GetInitialState()
{
return new UserFeedbackState
{
Submitting = false,
Submitted = false,
ErrorMessage = string.Empty,
Model = new UserFeedbackModel()
};
}
}
I've added a few properties to the UserFeedbackState to represent the state of the component: Submitting, Submitted, ErrorMessage. These represent the state of the form, but not the values in the form.
The values in the form will be databound to the Model
property, where I'm cheating compromising a bit by using an init-only object but with read/write properties.
Note: I'm certain that this technique isn't strictly adherent to the "immutable state" approach of Flux, since technically some state is being mutated without going through a reducer. But I think this specific and limited situation is an acceptable exception to the rule, given the benefits.
For Actions, we only need a couple:
public class UserFeedbackSubmitSuccessAction { }
public class UserFeedbackSubmitFailureAction
{
public string ErrorMessage { get; }
public UserFeedbackSubmitFailureAction(string errorMessage)
{
ErrorMessage = errorMessage;
}
}
public class UserFeedbackSubmitAction
{
public UserFeedbackModel UserFeedbackModel { get; }
public UserFeedbackSubmitAction(UserFeedbackModel userFeedbackModel)
{
UserFeedbackModel = userFeedbackModel;
}
}
We'll need one Effect for the form submit:
public class UserFeedbackEffects
{
private readonly HttpClient _httpClient;
public UserFeedbackEffects(HttpClient httpClient)
{
_httpClient = httpClient;
}
[EffectMethod]
public async Task SubmitUserFeedback(UserFeedbackSubmitAction action, IDispatcher dispatcher)
{
var response = await _httpClient.PostAsJsonAsync("Feedback", action.UserFeedbackModel);
if (response.IsSuccessStatusCode)
{
dispatcher.Dispatch(new UserFeedbackSubmitSuccessAction());
}
else
{
dispatcher.Dispatch(new UserFeedbackSubmitFailureAction(response.ReasonPhrase));
}
}
}
The EffectMethod will accept the Model
as part of the action
, and use the injected HttpClient
to post it to the ApiController we'll soon create. It will dispatch either a Success or Failure action when it's done.
And finally for the store, the ReducerMethods:
public static class UserFeedbackReducers
{
[ReducerMethod(typeof(UserFeedbackSubmitAction))]
public static UserFeedbackState OnSubmit(UserFeedbackState state)
{
return state with
{
Submitting = true
};
}
[ReducerMethod(typeof(UserFeedbackSubmitSuccessAction))]
public static UserFeedbackState OnSubmitSuccess(UserFeedbackState state)
{
return state with
{
Submitting = false,
Submitted = true
};
}
[ReducerMethod]
public static UserFeedbackState OnSubmitFailure(UserFeedbackState state, UserFeedbackSubmitFailureAction action)
{
return state with
{
Submitting = false,
ErrorMessage = action.ErrorMessage
};
}
}
The ReducerMethods are straight-forward, just keeping track of the few properties that we'll use to decide what to display on the screen.
In the UserFeedback\Pages
folder we'll add a Feedback.razor file as the page to hold the form. The EditForm will look like:
<EditForm Model="@model" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<div class="form-group">
<label for="emailAddress">Email Address</label>
<InputText class="form-control" id="emailAddress" @bind-Value="model.EmailAddress" />
</div>
<div class="form-group">
<label for="rating">Rating (1-10)</label>
<InputSelect class="form-control" id="rating" @bind-Value="model.Rating">
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>6</option>
<option>7</option>
<option>8</option>
<option>9</option>
<option>10</option>
</InputSelect>
</div>
<div class="form-group">
<label for="comment">Comment</label>
<InputTextArea class="form-control" id="comment" @bind-Value="model.Comment" rows="3"></InputTextArea>
</div>
<button class="btn btn-primary" type="submit">Submit</button>
<ValidationSummary />
</EditForm>
The form has all the AspNetCore goodness: two-way databinding, DataAnnotationsValidator, ValidationSummary, etc. The HandleValidSubmit
method will only be invoked once all of the form fields pass all validation.
The entire razor page, including the @code
block and all of the code deciding which portions of the screen to display is below:
@page "/feedback"
@inherits FluxorComponent
@using BlazorWithFluxor.Client.Features.UserFeedback.Store
@inject IState<UserFeedbackState> UserFeedbackState
@inject IDispatcher Dispatcher
<h3>User Feedback</h3>
@if (UserFeedbackState.Value.Submitting)
{
<div>
Submitting... Please wait.
</div>
}
else if (UserFeedbackState.Value.Submitted && string.IsNullOrWhiteSpace(UserFeedbackState.Value.ErrorMessage))
{
<div class="alert alert-success">
Thank you for sharing!
</div>
}
else
{
<EditForm Model="@model" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<div class="form-group">
<label for="emailAddress">Email Address</label>
<InputText class="form-control" id="emailAddress" @bind-Value="model.EmailAddress" type="email" />
</div>
<div class="form-group">
<label for="rating">Rating (1-10)</label>
<InputSelect class="form-control" id="rating" @bind-Value="model.Rating">
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>6</option>
<option>7</option>
<option>8</option>
<option>9</option>
<option>10</option>
</InputSelect>
</div>
<div class="form-group">
<label for="comment">Comment</label>
<InputTextArea class="form-control" id="comment" @bind-Value="model.Comment" rows="3"></InputTextArea>
</div>
<button class="btn btn-primary" type="submit">Submit</button>
<ValidationSummary />
</EditForm>
}
@if (!string.IsNullOrWhiteSpace(UserFeedbackState.Value.ErrorMessage))
{
<div class="alert alert-danger">
Error: @UserFeedbackState.Value.ErrorMessage
</div>
}
@code {
private UserFeedbackModel model => UserFeedbackState.Value.Model;
private void HandleValidSubmit()
{
Dispatcher.Dispatch(new UserFeedbackSubmitAction(UserFeedbackState.Value.Model));
}
}
To get to the page we add an item to the NavMenu
:
<li class="nav-item px-3">
<NavLink class="nav-link" href="feedback">
<span class="oi oi-list-rich" aria-hidden="true"></span> Feedback
</NavLink>
</li>
And finally, we need a place to POST the form to. Let's create a FeedbackController in the Server project:
using BlazorWithFluxor.Shared;
using Microsoft.AspNetCore.Mvc;
using System;
namespace BlazorWithFluxor.Server.Controllers
{
[ApiController]
[Route("[controller]")]
public class FeedbackController : ControllerBase
{
[HttpPost]
public void Post(UserFeedbackModel model)
{
var email = model.EmailAddress;
var rating = model.Rating;
var comment = model.Comment;
Console.WriteLine($"Received rating {rating} from {email} with comment '{comment}'");
}
}
}
The Post
action receives the same UserFeedbackModel
class that the EditForm
was bound to. We can (and should) re-validate the model here before further processing, but for this example I'm just going to log the contents of the model to the console.
As you can see, all of the built-in features of the EditForm and DataAnnotationsValidator are available, plus the state of the form was maintained by Fluxor when I navigated away from and then back to the form. The best of both worlds.
Happy Coding!
Top comments (9)
My approach is to allow the user to edit the DTO that is sent to the API.
When the form is submitted, I Dispatch an action containing that DTO.
A Reducer sets IsSaving = true
An effect calls the server
If the server indicates success then I dispatch a different action with the DTO in it
Any state that has an interest in that piece of data (e.g. Client) has a reducer that reduces the values of the DTO properties into their own immutable states.
You should avoid having mutable state.
Two thank you's!
First @mr_eking thanks for this excellent series. I've passed it along to several colleagues and it was instrumental in my decision to move forward with Fluxor for our current project.
Second @mrpmorris thank you for Fluxor and also taking the time to make this comment about using DTO's and forms. It was very helpful to me.
I see @mr_eking 's point here but I'm interested in the thoughts of @mrpmorris ,
Just getting into this pattern and wanted to get your idea straight for the immutable state of things:
Are you transforming the DTO into a record or will it be cloned in the reducer? or just passing that DTO instance in?
And when the server indicates a success, will that DTO be the same instance? or cloned or reconstructed or ...?
For the both of you: library + excellent blog + comments !
Yes, I'm doing exactly the same thing, with the one exception that I'm also storing the form's DTO (which I'm calling "model" above) into the form component's state.
I feel that's ok, since there's no chance for the state of that form's DTO to have any effect on anything else in the application's state. Not other features' states, not even its own feature's state. It merely allows the form's field values a place to live.
If there were something other than the data-bound form itself that has interest in the DTO properties then they would have to get that value via a reducer like everything else.
Thx for you response.
Yes sessionStorage should be a good solution.
As you said depends of how many states need to be saved.
But in your opinion is it in the Effects classes that we should persiste the states in session before to disptach.
Yes, pretty much. I have added another post to the series, with examples of that approach. Let me know what you think!
Thanks for your work. I am working on a Blazor Server Project (not WASM) but your posts helped me a lot so far. Now i would like to use fluxor for an image upload from the client via blazor-server to a Rest-API.
Currently, the upload is working (without fluxor), following the Microsoft-Docs, using: InputFile-Component, MultipartFormDataContent with StreamContent and HttpClient.
Is there is a good practice fluxorifying that?
I am struggling to find a appropriate way to hand over the Image/HttpContent via Action to the handling Effect since i believe actions should be serializable and it would be bad practice to read the whole image into memory and store it into the action. Not sure if i am wrong with that.
Thx for you post, very Useful.
I Have a question. How you handle if users refresh page.
Counterstate will be reset. If you want to keep counterState.
How would you implement this?
Well, you would have to take the state and store it off somewhere so that it could be retrieved later.
One place to put the values of the store might be in the browser's localStorage, which would allow you to retrieve the state later for that user on that browser.
You could also store the state in a database somewhere, so it can be retrieved by the user regardless of which browser they are using, as long as they log in so you know who they are.
But how much of the state do you store? How often? And where? How secure do you want that persisted state to be? The answers to those questions really depend on the application.