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 (11)
Thanks for your tutorial, it is easy to follow and it's nice if you are new to Fluxor.
I think I know a way though, in which you can use a fully one-directional flow, with immutability for all the properties, and you don't need to mutate an object in your state. It only requires a bit of knowledge about the EditContext.
1) When you initialize the EditContext, it wants a model, as you said, not a changing reference. If you would supply your EditContext with IState, the wrapper object, you know it has the same reference for the lifetime of your component.
2) @bind-Value forces you to use two-directional binding. But, a little bit more verbose, you can write it out, and gain full control of the direction of the data.
Because @bind-Value="State.Value.TextProperty" is really the same as:
Value=@State.Value.TextProperty
ValueChanged=TextPropertyChanged
ValueExpression=() => State.Value.TextProperty />
In TextPropertyChanged you don't set the value, but you are free to call your Dispatcher and dispatch an action like TextPropertyChangedAction.
I learned the EditContext works with FieldIdentifiers. This is a struct that needs the ValueExpression from above to identify a field in the form and display an errormessage at the correct field. Of course then the validator uses the same FieldIdentifiers to validate and add the errorstate.
Every time you change the state, the ValueExpression will change though:
State.Value.TextProperty, after each change there will be a new "value" and a new FieldIdentifier is needed.
So, just make sure you validate only AFTER changing your state. Then you know that your ValueExpression from validation, will match with the ValueExpression of the form field values.
Happy Fluxing!
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.
Hi @mr_eking
Firstly, thank you for sharing your blog posts on Fluxor and Blazor. They are very well written and helpful.
I understand your point about whether it is acceptable to "bend the rules" with regard to the immutability of the Fluxor state for this specific use case (UserFeedback). However, I believe a more generically applicable approach would be to change the code in
Feedback.razor
as follows:Treating the UserFeedbackModel as a DTO allows the use of standard Razor form features without violating the immutability principle.
Actually, if your underlying model for the edit form needs to respond to Fluxor state changes, I suggest implementing the action subscription pattern instead. In this case, I don't believe that is required at all.
As mentioned by @mrpmorris there is an important difference between the immutable application state being served up by the Fluxor framework and the mutable and not yet dispatched state of the edit form.
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.