Advanced Blazor State Management Using Fluxor, part 5 - ASP.NET EditForm Binding
Tags: csharp, blazor
Published: 2021-03-10 13:39:17

>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.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/249ohdevht6goxio5d39.png)

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](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform), binding it a [Model](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.forms.editform.model) class, and [validating it](https://docs.microsoft.com/en-us/aspnet/core/blazor/forms-validation) 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:

```csharp
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:

```csharp
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:

```csharp
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:

```csharp
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:

```csharp
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:

```csharp
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:

```html
<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:

```html
@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`:

```html
<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:

```csharp
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.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6isi449psszmojdmb3a9.gif)

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!