A simple database management system with Blazor SPA

Today I will show you how to create a simple Employee Database Management System (DBMS) single-page application(SPA) in .NET Blazor WebAssembly(WASM).

Employee DBMS(left of image) is actually a client to a Employee Database REST API. Client sends HTTP requests to REST API service which sends response back. Response can be data or status or both.

For testing purposes, I’m using a REST API that I developed in my previous tutorial called Create a Simple Database REST API.

Download the complete Visual Studio project here.


Content


Motivation for this project

There are different ways of presenting the data. I chose to present the data in HTML table, even though it does not look good on mobile devices, because this was a question that a reader asked me from one of my posts in my old GitHub Page, called Consume RESTful Web Services Using HttpClient in ASP.NET Core and Razor Pages – the question was how to read the JSON data and put them in a table. In that previous tutorial, I chose to display the data in a raw JSON format shown inside a text box – it works but not it will get a D in user experience.

Moreover, I chose the HTML table layout because it is similar to a 6-year old .NET MVC application used in my company that keeps track of open bids. In fact, I used that MVC app as the inspiration for the user interface(UI) of this tutorial.


What you will learn

This article is a bit long, and rather too much in terms of information. So below is a list of the things that I think you will get out of this article.

  • How to create a Single-Page Application in Blazor WASM
  • How to call, access, and modify resource from a REST API service
  • How to display data from REST API in html table
  • How to access and read appsettings.json into your application
  • How to pass data between components using route parameters
  • How to share complex objects between components using state objects
  • How to check validation errors from a REST API
  • How to keep track of checkboxes between components

Project Setup

We are going to create a .NET Blazor WebAssembly project in Visual Studio. The setup for this project is similar to my previous tutorial called How to Create a REST Client Single-Page. But I will show it again here as it is simple and quick if you’re using Visual Studio.

Open Visual Studio and select Create a new project option when asked what you want to do. As shown below, in the Create a new project dialog box, search for Blazor in the search box.

Select Blazor WebAssembly App in the list. Click Next.

In the Configure your new project dialog box, enter a project name you want. In my example above, I entered EmployeeDBMS for Project name. I used the same name for the solution name. Click Next.

In the Additional information dialog box, choose .NET 6.0 as Framework (or later framework that supports Blazor). Because this is just a demo, we’re not doing Authentication or configuring for HTTPS. Click Create.

Shown below is the new Blazor WASM project in the Solution Explorer. By default, the project template created two three Razor components for us under the Pages folder – Index, Counter and FetchData.

Run the program by hitting F5 or Ctrl+F5, and you will get a Hello World! page.

On the left panel is a menu of the things that we can do in this application. Home points to Index component, and simply displays Hello World! Counter counts how many times you click the Click me button. Fetch data simply displays a Weather forecast page that, for demonstration purposes, gets is data from a JSON file.


Models

By default, the .NET Blazor WASM template does not create a Model directory in the project. But in this tutorial, we need an Employee model and a Checkbox model. And we need a Model folder which will contain our models.

Because the models are shared by components, we will put them under the Shared directory. Right-click the Shared directory, then click Add > New Folder. Name the folder Models.

Employee model

We need an Employee model, not for storage(as this is not a database application), but, specifically for these two reasons:

  • For binding values in form (i.e., EditForm), and
  • Deserializing data from Web API service to a list of Employee.

To create the model, right-click on the Models folder, and click Add > Class. For the name, use Employee.cs. Then, click Add button.

Use the following code in Employee.cs.

namespace EmployeeDBMS.Shared.Models
{
    public class Employee
    {
	public int Id { get; set; }

        public string FName { get; set; }

        public string LName { get; set; }

        public string Email { get; set; }

        public DateTime? DateOfHire { get; set; } = DateTime.Now;

        public DateTime? DateOfBirth { get; set; } = DateTime.Now;

        public string Position { get; set; }

        public string Department { get; set; }

        public string Address { get; set; }

        public string City { get; set; }

        public string State { get; set; }

        public string Zipcode { get; set; }
    }
}

This is actually the same as the Employee model of the database REST API from my previous tutorial, Create a Simple Employee Database REST API, which, I will use later for testing.


CheckboxModel

The second model is called CheckboxModel. This entity is used to keep track the checkboxes in the Form. Again, to create this model, right-click the Models folder, then click Add > Class. And then, type CheckboxModel.cs in the Name box.

Use the following code in CheckboxModel.cs.

namespace EmployeeDBMS.Shared.Models
{
    public class CheckboxModel
    {
        public int Value { get; set; }
        public bool Checked { get; set; }

        public CheckboxModel()
        {
        }
        public CheckboxModel(int v, bool c)
        {
            Value = v;
            Checked = c;
        }
    }
}

ValidationErrorDetails

The ValidationErrorDetails class will contain HttpResponseMessage content if and only if there is an error processing the request. The model is based on Json object coming from the API service, and it looks like this:

using System.Text.Json.Serialization;

namespace EmployeeDBMS.Shared.Models
{
    public class ValidationErrorDetails
    {
        [JsonPropertyName("errors")]
        public IDictionary<string, string[]>? Errors { get; set; }

        [JsonPropertyName("type")]
        public string? Type { get; set; }

        [JsonPropertyName("title")]
        public string? Title { get; set; }

        [JsonPropertyName("status")]
        public int? Status { get; set; }

        [JsonPropertyName("traceId")]
        public string? TraceId { get; set; }
    }
}

The most important property of this model is Errors. This property will contain one or more errors that the API returns to us when something is wrong – for example, if a required field is missing when user submits the form.

Now that we’re done with the models, we’re going to go ahead and talk about configurations settings.

Open the file called _Imports.razor, and add the following code at the end of the file.

@using EmployeeDBMS.Shared.Models;

By doing so, it applies to all the components in the application, and we don’t need to add a @using directive in each of the components. You’ll understand this as we go along a little bit later in the tutorial.


App Settings

For this tutorial, we are going to use the configuration file to store the name of the REST API service that we’re going to call. If you don’t have appsettings.json under wwwroot folder, create it by right-clicking the wwwroot folder, then clicking Add > New item… In the search box in top-right, type json, and you will be given a list of json template files – choose App Setting File, and click the Add button, as illustrated below.

You now should have appsettings.json file under wwwroot folder. Open this file, and you should see one configuration setting called “ConnectionStrings”, as shown below.

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=_CHANGE_ME;Trusted_Connection=True;MultipleActiveResultSets=true"
  },

  "RestSvc": {
    "ApiUrl": "http://localhost:5000/api/v2/Employees/" 
  }
}

As you notice from above, when Visual Studio created appsettings.json, it also automatically created the ConnectionStrings settings, which I’m not going to use because I’m using a REST API service for my employee data.

Speaking of REST API service, I’m adding my own configuration setting, and it is called RestSvc, as shown above, and it contains a URL string – this is the URL of the REST API service that we will use later to test our application.

Now that we’ve created our project, created our models, and created our app setting, we are now ready to create our Blazor components. Let’s create the four files that represent these four components: DisplayEmployees, CreateEmployee, UpdateEmployee, and DeleteEmployee.

Right-click on the Pages folder and click Add > Razor Components. In the Name box, enter DisplayEmployees.razor, and finish by hitting Add button. Repeat this process for CreateEmployee, UpdateEmployee, and DeleteEmployee.

Sample Data

Initially, I will test this application with a sample data, especially for displaying employees. So, create a file called employees.json under wwwroot/sample-data/ folder, and copy the code below. Notice that there is already another file under that folder, and is called weather.json which the FetchData component uses.

[
  {
    "Id": 1,
    "FName": "Justin",
    "LName": "Bieber",
    "Email": "bieber@gmailcom",
    "DateOfHire": "2014-12-05",
    "DateOfBirth": "1975-01-01",
    "Position": "Manager",
    "Department": "IT",
    "Address": "324 Mainsail Ln",
    "City": "Fort Worth",
    "State": "TX",
    "Zipcode": "76135"
  },
  {
    "Id": 2,
    "FName": "Madonna",
    "LName": "Ciccone",
    "Email": "mciccone@gmail.com",
    "DateOfHire": "2011-11-15",
    "DateOfBirth": "1945-02-02",
    "Position": "Web Developer",
    "Department": "Web Communications",
    "Address": "123 Main Street",
    "City": "Hollywood",
    "State": "CA",
    "Zipcode": "33019"
  },
  {
    "Id": 3,
    "FName": "Cyndi",
    "LName": "Lauper",
    "Email": "lauper@gmailcom",
    "DateOfHire": "2014-12-05",
    "DateOfBirth": "1975-01-01",
    "Position": "Analyst",
    "Department": "Information Technology",
    "Address": "321 Lakewood Ave",
    "City": "Arlington",
    "State": "TX",
    "Zipcode": "76113"
  },
  {
    "Id": 4,
    "FName": "David",
    "LName": "Bowie",
    "Email": "bowie@aol.com",
    "DateOfHire": "2011-11-15",
    "DateOfBirth": "1945-02-02",
    "Position": "Senior Web Developer",
    "Department": "QA",
    "Address": "123 Market Street",
    "City": "Dallas",
    "State": "TX",
    "Zipcode": "76111"
  }
]

Later on during the implementation of the UpdateEmployee and DeleteEmployee, I will test with a separate Web API service that I implemented from one of my previous tutorials.

This is how our project should look like after creating all those files above.


DisplayEmployees

DisplayEmployees component is our home page. This is the default component that gets displayed first – when page is loaded, all employees are displayed inside a table.

For testing purposes, I will use a ASP.NET Core Web API application that I implemented from my previous tutorial called How to create a simple Employee REST API. I highly recommend that you download the REST API project from that tutorial, so that you can test against a working Web API service and not just sample data. Just open that project in a second Visual Studio instance, and run it. If you’re not already using port 500, the URL of that application should be http://localhost:5000/api/v2/Employees/.

Remember the App Settings (appsettings.json) that I discussed earlier with the API URL setting?

  "RestSvc": {
    "ApiUrl": "http://localhost:5000/api/v2/Employees/"
  }

The “http://localhost:5000/api/v2/Employees/” is, of course, the URL of the Web API that we will send our HTTP requests to. In the next section, I will explain how to access this resource to get all the employees from the database.

Like I showed you earlier, this is how DisplayEmployees interface looks like:

Assuming that there are 4 employees in the company, the home page displays a table of all employees in the database. This is the very first thing you see when you visit the site. The home page has all the functionalities that you need besides displaying all employees including creating, updating, and deleting employee.

There are checkboxes for selecting records that you want to delete from the database. There is a “Delete Selected” button that sends a DELETE request to API. There is also an “Update” button for each record that you want to modify. Finally, at the top of the homepage is “Create Employee” link, and this is what I will show next.

To create our DisplayEmployee component, right-click on Pages directory > Add > Razor component. Name the file DisplayEmployee.razor. Initially, newly created razor files will do nothing but display a title – they contain nothing but an <h3> title, and an empty @code block, as shown below:

<h3>Page Title</h3>

@code {

}

The full implementation is shown below:

@page "/displayemployees"

@using Newtonsoft.Json; 
@using Newtonsoft.Json.Linq;
@inject NavigationManager navigationManager
@inject HttpClient httpClient
@inject IConfiguration configuration
@inject MyStateObject myStateObject
@implements IDisposable

<h3>Employee Management System</h3>

<div id="create-link"><a href="/createemployee">Create Employee</a></div>

<div>
    <EditForm Model="@emp">
        <table>
            <thead>
                <tr>
                    <th></th>
                    <th>ID</th>
                    <th>First name</th>
                    <th>Last name</th>
                    <th>Email</th>
                    <th>Date of Hire</th>
                    <th>Date of Birth</th>
                    <th>Position</th>
                    <th>Department</th>
                    <th>Address</th>
                    <th>City</th>
                    <th>Zipcode</th>
                    <th></th>
                </tr>
            </thead>
            <tbody>
                @foreach (Employee content in employees)
                {
                <tr>
                    <td>
                        @{ int id = (int)content.Id; }
                        <input name="deleteCheckbox" type="checkbox" @key="content.Id" @onchange="eventArgs => { ProcessCheckbox(id, eventArgs.Value); }"  />
                    </td>
                    <td>@content.Id</td>
                    <td>@content.FName</td>
                    <td>@content.LName</td>
                    <td>@content.Email</td>
                    <td>@content.DateOfHire.Value.ToShortDateString()</td>
                    <td>@content.DateOfBirth.Value.ToShortDateString()</td>
                    <td>@content.Position</td>
                    <td>@content.Department</td>
                    <td>@content.Address</td>
                    <td>@content.City</td>
                    <td>@content.Zipcode</td>
                    <td>
                        <button @onclick="() => HandleUpdateSubmit(content.Id)">Update</button>
                    </td>
                </tr>
                }
            </tbody>
        </table>

        <div class="input-group mt-3">
        <button type="submit" @onclick="@(()=>HandleDeleteSubmit())">Delete Selected</button>
        </div>

    </EditForm>

</div>
@code {
    Employee? emp = new();
    List<Employee> employees = new List<Employee>();
    List<CheckboxModel> checkboxModels = new List<CheckboxModel>();

    protected override async Task OnInitializedAsync()
    {
        myStateObject.OnStateChange += StateHasChanged;
        try
        {
            await FetchDatabase();
        }
        catch(Exception ex)
        {
            System.Console.WriteLine("Exception: " + ex.Message);
        }
    }

    private async Task FetchDatabase()
    {
        //var emp1  = await httpClient.GetFromJsonAsync<List<Employee>>("sample-data/employees.json");
        //employees.AddRange(emp1);

        string restSvcApiUrl = configuration["RestSvc:ApiUrl"];
        HttpResponseMessage responseMessage =  new();

        responseMessage = await httpClient.GetAsync(restSvcApiUrl);


        // Extract content(body) of response
        string resp = responseMessage.Content.ReadAsStringAsync().Result;

        JToken jToken = JToken.Parse(resp);
        if ( jToken is JArray )
        {
            // Deserialize json string to Json 
            employees = JsonConvert.DeserializeObject<List<Employee>>(resp);
        }
        else
        {
            employees.Add(JsonConvert.DeserializeObject <Employee>(resp));

        }
    }

    private void HandleUpdateSubmit(int id)
    {       
        navigationManager.NavigateTo($"/updateemployee/{id}");
    }

    private void HandleDeleteSubmit()
    {
        myStateObject.SetValue(checkboxModels);
        navigationManager.NavigateTo($"/deleteemployee");   
    }
    
    private void ProcessCheckbox(int id, object checkedValue)
    {
        if((bool)checkedValue)
        {
            checkboxModels.Add(new CheckboxModel(id, (bool)checkedValue));
            System.Console.WriteLine(id + ":" + (bool)checkedValue);
        }
        else
        {
            checkboxModels.RemoveAll(x=> x.Value == id);
        }
    }

    public void Dispose()
    {
        myStateObject.OnStateChange -= StateHasChanged;
    }
}

Take note that we have about 5 major methods – OnInitializedAsync(), FetchDatabase(), HandleUpdateSubmit(int id), HandleDeleteSubmit(), ProcessCheckbox(), and Dispose().

OnInitializedAsync()

OnInitializedAsync() on line 74 is the first method that is executed when DisplayEmployees component is loaded. And inside this method I call the FetchDatabase() function on line 79 because I want all the employees displayed as soon as this component is rendered. FetchDatabase() sends a Get request to the Web API, and gets a list of all employees. We, then, store this list in employees variable which is declared as List<Employee> on line 71.

After we get out of OnInitializedAsync(), the html(or markup) is then executed with employees being displayed. On line 36, in the foreach loop, the employees variable is checked whether or not it’s empty, and if it’s not, then we iterate through the list and display each employee record.

Also notice the StateObject event subscription on line 76, which I will explain a little bit later in its own section.

FetchDatabase()

As I mentioned, the FetchDatabase() function on line 87 sends an HTTPGET request to the Web API to get all the employee records.

On line 92, I call the configuration["RestSvc:ApiUrl"] which should return the URL “http://localhost:5000/api/v2/Employees/”. Then, on line 95, I use the HTTPClient.GetAsync() method to send a GET request to that URL. I get a response which I store into HttpResponseMessage object. Make sure to add @using IConfiguration at the top to be able to read configurations.

Our objective at this point in time is to deserialize the data into our own model so the data is easy to manipulate in C#. On line 99, we first extract the response content by calling ReadAsStringAsync() – the string that we get is either a list of employees or one employee. On line 101 I parse the content into a JToken object, and on line 102, I check if this is an array – that is, if it’s multiple employees.

On line 105, I deserialize the content into List<Employee> for when the content is multiple employees. On line 109, I deserialize the content into Employee object for when the content is one employee. Make sure you add @using Newtonsoft.Json at the top as shown on lines 3-4.

Lastly, on line 9, I added @implements IDisposable which allows us to implement Dispose() method and add our own cleanup.

After OnInitializedAsync() completes, we should have the employee(s) displayed in an html table.

ProcessCheckbox

ProcessCheckbox method on line 125 is an event handler that keeps track of what checkboxes are selected by the user.

Everytime a user checks/unchecks a box(line 41 of markup), the ProcessCheckbox method is called. Two things get passed to the method – the id of the employee and a “Value”. The Value is of boolean type – it is true if box is checked, and false if it is unchecked. We use the parameter checkedValue to store this value.

We use checkboxModels, declared as List<CheckboxModel> to store id’s of the employees that are selected. Remember from the definition of CheckboxModel class that it has two properties: int Value, and bool Checked.

On line 127, we check if checkedValue is true, and if it is, we add the employee id to the checkboxModels list. If false, we remove it from the list.

HandleUpdateSubmit(int id)

Each employee in the employee table corresponds to one “Update” button, and line 55 of our html shows this <button> element. When you click that button on the page, a button handler is called – HandleUpdateSubmit(int id), shown on lines 14-17. What it does is simply navigates you to the actual UpdateEmployee component that does the actual work.

Incidentally, navigating to another component and passing a value in the URL is called route parameters and you can read more about it in my previous article called Five Ways to Pass Data Between Components in Blazor.

HandleDeleteSubmit()

Similarly, the HandleDeleteSubmit method shown on lines 19-23 simply navigates us to the DeleteEmployee component which does the actual work. Obviously, this method is called after we have checked the boxes and clicked the Delete Selected button. That <button> element is shown on line 63 of our markup.

One important thing to realize is that we somehow need to pass the checkboxModels object to the DeleteEmployee component, and this is where the state object comes in on Line 121, where we use MyStateObject class to store checkboxModels object. Only then can I go ahead and navigate to DeleteEmployee component.

MyStateObject

I thought I’d discuss state objects (or state containers) in its own section because, even though it is easy to use, it isn’t very straightforward as it requires the object to be added as a service, and used via dependency injection.

Basically, if you are trying to pass the checkboxModels object from DisplayEmployees to DeleteEmployee, you can’t really do the following:

navigationManager.NavigateTo($"/deleteemployee/{checkboxModels}"

This is because a route value should only be either an integer or a string, and not a complext object like a List or a user-defined class.

Secondly, you can’t add a markup in DisplayEmployees like so:

<DeleteEmployee MyList="@checkboxModels"></DeleteEmployee>

This is because you don’t want DeleteEmployee as a child component of DisplayEmployees. There is no parent-child relationship in my application.

When I was doing research on how to create a “global” variable so that all components have access to it without passing it as a parameter, I came across the concept of State Management that just does this – it “manages an object in the global scope of the application as a singleton object in the dependency container“.

What I needed to is define a State object that will hold my checkboxModels object, register this State object as a singleton, and finally, inject it to the components that want to use checkboxModels object.

In the Solution Explorer, right-click on the Shared folder, and create a class, and call it MyStateObject. This class is shown below:

using EmployeeDBMS.Shared.Models;

namespace EmployeeDBMS.Shared
{
    public class MyStateObject
    {
        public List<CheckboxModel> Value { get; set; }

        public event Action OnStateChange;

        public void SetValue(List<CheckboxModel> value)
        {
            Value = value;
            NotifyStateChanged();
        }

        private void NotifyStateChanged() => OnStateChange?.Invoke();
    }
}

As shown above, I declare a property called Value declared as List<CheckboxModel> – same as checkboxModels variable.

OnStateChange is an event that is raised when the checkbox is checked on unchecked.

NotifyStateChanged() is a method that we define to raise the OnStateChange event.

Finally, SetValue() is a method that sets the value of the Value property – in this case, our value is the list of CheckboxModel.

After creating MyStateObject, we need to register it inside Program.cs, as shown below:

builder.Services.AddSingleton<MyStateObject>();

Don’t forget to add using EmployeeDBMS.Shared statement at the top of the file. By adding a Singleton service, we create a global in-memory state container that is accessible among components.

It is now easier to understand the various calls to MyStateObject object in our C# code. For example, in DisplayEmployees, inside OnInitializedAsync(), I have this line:

myStateObject.OnStateChange += StateHasChanged;

With this, our component subscribes to the OnStateChange event.

Also, inside our HandleDeleteSubmit() method, I have this line:

myStateObject.SetValue(checkboxModels);

With this, we update set the Value property, and also update the state of MyStateObject.

As I mentioned earlier we included this directive at the top of our DisplayEmployees component – @implements IDisposable. With this, our component technically implements the IDisposable interface, and hence, allows us to implement our own Dispose() method(line 70 of DisplayEmployees) in which we do some explicit cleanup, such as unsubscribe to the OnStateChange event.

And that concludes the implementation of DisplayEmployees component. Next up is CreateEmployees component and how to check validation errors.

For more examples of State Containers, check out my previous article called Five Ways to Pass Data Between Components in Blazor.


CreateEmployee

This is how the CreateEmployee page looks like:

You enter all the employee data in the form and click Submit when done. If successful, page is dynamically updated without reloading page(illustrated below).

And this is what is awesome about Blazor single-page application, only the Display Employee component is reloaded, not the whole page.

What if your input is not valid? When a required field is missing or, say, the email address you entered is not valid, you will get a simple error message(s) displayed on top of the page, which looks like this:

Basically, after sending a POST request to the Web API, we need to check for the response message, and look for errors. As in the example above, the user did not enter a last name, and also entered an email with the wrong format.

Let’s look at the complete C# code for our component below and see how we are using these two classes.

@page "/createemployee"

@using Newtonsoft.Json
@inject NavigationManager navigationManager
@inject IConfiguration configuration
@inject HttpClient httpClient

<h3>CreateEmployee</h3>

<EditForm OnValidSubmit="@HandleValidSubmit" EditContext="_editContext">

    @if(errorDictionary !=null)
    {
        <div class="errormessages-wrapper">
            <h5>Please correct errors</h5>
            <div class="input-group mb-3" >  
                <ul>
                    @foreach(var objectDict in errorDictionary)
                    {
                        string a = objectDict.Key;
                        var errors = objectDict.Value;
                        @foreach(string err in errors)
                        {
                            <li>@err</li>
                        }
                    }
                </ul>
            </div>   
        </div>

     }

    <div class="input-group mb-3">
    <label for="inputFName" class="col-sm-2 col-form-label">First Name</label>
    <InputText id="inputFName" @bind-Value="empModel.FName" class="form-control" aria-label="" />
    </div>   
    <div class="input-group mb-3">
        <label for="inputLName" class="col-sm-2 col-form-label">Last Name</label>
        <InputText id="inputLName" @bind-Value="empModel.LName" class="form-control" aria-label="" />
    </div>   
    <div class="input-group mb-3">
        <label for="inputEmail" class="col-sm-2 col-form-label">Email</label>
        <InputText id="inputEmail" @bind-Value="empModel.Email" class="form-control" aria-label="" />
    </div>   
    <div class="input-group mb-3">
        <label for="inputDateOfHire" class="col-sm-2 col-form-label">Date of Hire</label>
        <InputDate id="inputDateOfHire" @bind-Value="empModel.DateOfHire" class="form-control" aria-label="" />
    </div>  
    <div class="input-group mb-3">
        <label for="inputDateOfBirth" class="col-sm-2 col-form-label">Date of Birth</label>
        <InputDate id="inputDateOfBirth" @bind-Value="empModel.DateOfBirth" class="form-control" aria-label="" />
    </div>  
    <div class="input-group mb-3">
        <label for="inputPosition" class="col-sm-2 col-form-label">Position</label>
        <InputText id="inputPosition" @bind-Value="empModel.Position" class="form-control" aria-label="" />
    </div>  
    <div class="input-group mb-3">
        <label for="inputDepartment" class="col-sm-2 col-form-label">Department</label>
        <InputText id="inputDepartment" @bind-Value="empModel.Department" class="form-control" aria-label="" />
    </div>  
    <div class="input-group mb-3">
        <label for="inputAddress" class="col-sm-2 col-form-label">Address</label>
        <InputText id="inputAddress" @bind-Value="empModel.Address" class="form-control" aria-label="" />
    </div>   
    <div class="input-group mb-3">
        <label for="inputCity" class="col-sm-2 col-form-label">City</label>
        <InputText id="inputCity" @bind-Value="empModel.City" class="form-control" aria-label="" />
    </div>   
    <div class="input-group mb-3">
        <label for="inputState" class="col-sm-2 col-form-label">State</label>
        <InputText id="inputState" @bind-Value="empModel.State" class="form-control" aria-label="" />
    </div> 
    <div class="input-group mb-3">
        <label for="inputZipcode" class="col-sm-2 col-form-label">Zipcode</label>
        <InputText id="inputZipcode" @bind-Value="empModel.Zipcode" class="form-control" aria-label="" />
    </div> 

    <div class="input-group mb-3">
    <button type="submit">Submit</button> <a href="/displayemployees" id="cancel" name="cancel" class="btn btn-default">Cancel</a>
    </div>

</EditForm>
@code {
    Employee empModel = new();
    EditContext _editContext;
    //ValidationMessageStore msgStore;
    public IDictionary<string, string[]>? errorDictionary;

    protected override void OnInitialized()
    {
        _editContext = new EditContext(empModel);
        //msgStore = new ValidationMessageStore(_editContext);
    }

    private async Task HandleValidSubmit()
    {
        //msgStore.Clear();

        HttpResponseMessage responseMessage =  new();

        try
        {           
            string restSvcApiUrl = configuration["RestSvc:ApiUrl"];
            string body = JsonConvert.SerializeObject(empModel);

            //
            // Call POST method given a URL and byte array(changed from StringContent)
            //
            byte[] messageBytes = System.Text.Encoding.UTF8.GetBytes(body);
            var byteContent = new ByteArrayContent(messageBytes);
            byteContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");          
            responseMessage = await httpClient.PostAsync(restSvcApiUrl, byteContent);

            if (responseMessage.StatusCode != System.Net.HttpStatusCode.Created)
            {
                // Extract content(body) of response
                var content = responseMessage.Content.ReadAsStringAsync().Result;

                // Deserialize json string to my error model 
                ValidationErrorDetails? validationErrorDetails = JsonConvert.DeserializeObject<ValidationErrorDetails>(content);

                errorDictionary = validationErrorDetails.Errors;

                _editContext = new EditContext(empModel);
                //msgStore = new ValidationMessageStore(_editContext);
            }
            else
            {
                //StateHasChanged();
                navigationManager.NavigateTo("/displayemployees");
            }

        }
        catch(Exception ex)
        {
            System.Console.WriteLine("Exception: " + ex.Message);
        }
    }
}

Initialization

On line 89, I am overriding the OnInitialized() method, and this is because I want to initialize EditContext as soon as the component is loaded.

EditContext holds metadata related to a data editing process, such as flags to indicate which fields have been modified and the current set of validation messages[1].

When we instantiate an EditContext object, we pass our EmployeeModel object to it, shown on line 91.

It is important to note that we need to make use of EditContext as an attribute inside <EditForm>, as shown below, and as shown above on line 10:

<EditForm OnValidSubmit="@HandleValidSubmit" EditContext="_editContext">

It is also important to point out that if we didn’t use EditContext, our <EditForm> element will look like this:

<EditForm OnValidSubmit="@HandleValidSubmit" Model="_empModel">

That is, we use the attribute Model instead of EditContext.

But we need to use EditContext for CreateEmployee component in order to store information about form fields being changed by the user, or validation messages generated from the Web API.

HandleValidSubmit()

When the user clicks the “Submit” button, it goes to the HandleValidSubmit() method, shown on lines 95-139.

As you have noticed, this method performs a lot of things. First, on line 103, we call configuration["RestSvc:ApiUrl"] to get the URL of the REST API that we are sending a request to.

Next, On line 04, we serialize the employee object to string. Remember that , by this time, the user has already entered their data before clicking the “Submit” button.

Lines 109 to 111 show you how to convert your data to ByteArrayContent. We are doing this because the second parameter of PostAsync is HttpContent type. So, the most common derived type that you can use is StringContent or ByteArrayContent. In my previous tutorial where my data was straight up JSON string, I used StringContent. For this tutorial though, I’m using an Employee model class that is serialized to string, so I needed to convert it to byte array, and set the content type to “application/json”.

Line 112 shows us how to send our Post request using PostAsync method, in which the first parameter is the URL of the Web API, and the second parameter is our data in byte array.

Checking for validation errors

Line 114 checks if the call to PostAsync is successful or not. PostAsync returns HttpStatusCode.Created when successful.

But if not, we need to read the content from the response, and deserialize it. Line 117 shows you how to read the HttpContent from the HttpResponseMessage object. The content body is actually a JSON that contains 5 sections – a list of errors, type, title, status, and traceId. We are only concerned with the list of errors, at least, for my purpose. Line 120 shows you how to deserialize the content to my error model.

On line 122, I then store list of errors into IDictionary<string, string>. Note that the errors are returned as key-value pairs. Later on, we want to display these errors above the <EditForm> in red color, as shown in the demo.

On line 124, we need to create a new instance of EditContext. Why? After processing the errors, you want to reset the metadata that it holds. If you don’t create a new context, the user won’t be able to re-submit the form – the Submit button will not respond because the form is still in the error state.

To summarize the error handling, if our errorDictionary variable had been set with data, that means that we received validation errors from the API. In this case, the code @if(errorDictionary !=null) on line 12 will validate to true, and will then display the error(s).

If there was no error in the first place, we would have gone to line 130 to navigate back to DisplayEmployees, and you’ll see the newly added employee record.

And that concludes the implementation of CreateEmployee. Now we move on to UpdateEmployee!


UpdateEmployee

Clicking one of the Update button on the home page will take you to the UpdateEmployee page where you can change the data for that particular employee, as shown below.

In this example, I want to change the Email from “lauper@gmail.com” to “cyndilauper@gmail.com” and the Department name from “IT” to “Information Technology”. After you hit “Submit”, the page is updated as shown below.

Below is the complete source code:

@page "/updateemployee"
@page "/updateemployee/{employeeid:int}"

@using Newtonsoft.Json;
@using System.Text;
@inject NavigationManager navigationManager
@inject IConfiguration configuration
@inject HttpClient httpClient

<h3>Update Employee</h3>
<EditForm EditContext="_editContext" OnValidSubmit="@HandleValidSubmit">

    @if(errorDictionary !=null)
    {
        <div class="errormessages-wrapper">
            <h5>Please correct errors</h5>
            <div class="input-group mb-3" >  
                <ul>
                    @foreach(var objectDict in errorDictionary)
                    {
                        string a = objectDict.Key;
                        var errors = objectDict.Value;
                        @foreach(string err in errors)
                        {
                            <li>@err</li>
                        }
                    }
                </ul>
            </div>   
        </div>

     }

<div class="input-group mb-3">
    <label for="inputFName" class="col-sm-2 col-form-label">First Name</label>
    <InputText id="inputFName" @bind-Value="empModel.FName"  class="form-control" aria-label="" />
</div>   
<div class="input-group mb-3">
    <label for="inputLName" class="col-sm-2 col-form-label">Last Name</label>
    <InputText id="inputLName" @bind-Value="empModel.LName" class="form-control" aria-label="" />
</div>   
<div class="input-group mb-3">
    <label for="inputEmail" class="col-sm-2 col-form-label">Email</label>
    <InputText @bind-Value="empModel.Email" class="form-control" aria-label="" />
</div>   
<div class="input-group mb-3">
    <label for="inputDateOfHire" class="col-sm-2 col-form-label">Date of Hire</label>
    <InputDate id="inputEmail" @bind-Value="empModel.DateOfHire" class="form-control" aria-label="" />
</div>   
<div class="input-group mb-3">
    <label for="inputDateOfBirth" class="col-sm-2 col-form-label">Date of Birth</label>
    <InputDate id="inputDateOfBirth" @bind-Value="empModel.DateOfBirth" class="form-control" aria-label="" />
</div>  
<div class="input-group mb-3">
    <label for="inputPosition" class="col-sm-2 col-form-label">Position</label>
    <InputText id="inputPosition" @bind-Value="empModel.Position" class="form-control" aria-label="" />
</div> 
<div class="input-group mb-3">
    <label for="inputDepartment" class="col-sm-2 col-form-label">Department</label>
    <InputText id="inputDepartment" @bind-Value="empModel.Department" class="form-control" aria-label="" />
</div> 
<div class="input-group mb-3">
    <label for="inputAddress" class="col-sm-2 col-form-label">Address</label>
    <InputText @bind-Value="empModel.Address" class="form-control" aria-label="" />
</div>   
<div class="input-group mb-3">
    <label for="inputCity" class="col-sm-2 col-form-label">City</label>
    <InputText id="inputCity" @bind-Value="empModel.City" class="form-control" aria-label="" />
</div>   
<div class="input-group mb-3">
    <label for="inputState" class="col-sm-2 col-form-label">State</label>
    <InputText id="inputState" @bind-Value="empModel.State" class="form-control" aria-label="" />
</div> 
<div class="input-group mb-3">
    <label for="inputZipcode" class="col-sm-2 col-form-label">Zipcode</label>
    <InputText id="inputZipcode" @bind-Value="empModel.Zipcode" class="form-control" aria-label="" />
</div> 

<div class="input-group mb-3">
<button type="submit">Submit</button> <a href="/displayemployees" id="cancel" name="cancel" class="btn btn-default">Cancel</a>
</div>

</EditForm>

@code {
    [Parameter]
    public int employeeid { get; set; }

    Employee? empModel = new();

    public IDictionary<string, string[]>? errorDictionary;
    EditContext _editContext;
    //ValidationMessageStore msgStore;

    protected override async Task OnInitializedAsync()
    {
        _editContext = new EditContext(empModel);
        //msgStore = new ValidationMessageStore(_editContext);

        HttpResponseMessage responseMessage = new();
        try
        {
            string restSvcApiUrl = configuration["RestSvc:ApiUrl"];
            string urlID = restSvcApiUrl + employeeid;

            // Get the response from the given URI
            responseMessage = await httpClient.GetAsync(urlID);

            // Extract content(body) of response
            var content = responseMessage.Content.ReadAsStringAsync().Result;

            empModel = JsonConvert.DeserializeObject<Employee>(content);

        }
        catch(Exception ex)
        {
            System.Console.WriteLine("Exception: " + ex.Message);
        }
    }

    private async Task HandleValidSubmit()
    {
        //msgStore.Clear();

        HttpResponseMessage responseMessage =  new();

        try
        {
            string restSvcApiUrl = configuration["RestSvc:ApiUrl"];
            string body = JsonConvert.SerializeObject(empModel);

            // Create a formatted text (as json) to be used for PUT or POST methods.
            var stringContent = new StringContent(body, Encoding.UTF8, "application/json");

            string urlID = restSvcApiUrl + employeeid;

            // Call POST method given a URL and StringContent
            responseMessage = await httpClient.PutAsync(urlID, stringContent);

            if (responseMessage.StatusCode != System.Net.HttpStatusCode.OK)
            {

                // Extract content(body) of response
                var content = responseMessage.Content.ReadAsStringAsync().Result;

                // Deserialize json string to my error model 
                ValidationErrorDetails? validationErrorDetails = JsonConvert.DeserializeObject<ValidationErrorDetails>(content);

                errorDictionary = validationErrorDetails.Errors;
                
                _editContext = new EditContext(empModel);
                //msgStore = new ValidationMessageStore(_editContext);
            }
            else
            {
                //StateHasChanged();
                navigationManager.NavigateTo("/displayemployees");
            }
        }
        catch(Exception ex)
        {
            System.Console.WriteLine("Exception: " + ex.Message);
        }
    }
}

This component overrides OnInitializedAsync() so that we can display the employee as soon as the page loads.

We first need to send a GET request to the Web API to get the employee data. So, on line 107 we send a GET request to the Web API using GetAsync(urlID), wth urlID being the API URL plus the employee Id appended to it. We, then, deserialize the response into our Employee object on line 112.

After getting the employee data, and after we exit out of OnInitializedAsync(), we then populate all the <InputText> textboxes in our markup with data.

The HandleValidSubmit() method is almost identical to that of CreateEmployee component, except we are now sending a PUT instead of a POST request. Similarly, we check for validation errors the same way we did in CreateEmployee.

And that concludes the implementation of UpdateEmployee.


DeleteEmployee

The last component that we need to implement is DeleteEmployee, which, as the name implies, deletes employees that are selected by the user. For this example, I want to delete the employee named “David Bowie”.

As shown above, I click the checkbox next to that employee, and hit Delete Selected button. If successful, the DisplayEmployees component will be reloaded to show the updated list of employees.

This is probably the easiest component to implement because it does not have a UI at all. Check out the complete source code is shown below:

@page "/deleteemployee"

@inject NavigationManager navigationManager
@inject IJSRuntime jsRuntime
@inject MyStateObject myStateObject
@inject IConfiguration configuration
@inject HttpClient httpClient

<h3>DeleteEmployee</h3>

@code {
    private List<CheckboxModel> checkboxModels;

    protected override async Task OnInitializedAsync()
    {

        checkboxModels = myStateObject.Value;

        try
        {
            string restSvcApiUrl = configuration["RestSvc:ApiUrl"];
            HttpResponseMessage responseMessage = new();
            if (checkboxModels.Count != 0)
            {
                bool confirmDeletion = await jsRuntime.InvokeAsync<bool>("confirm", "Are you sure you want to delete?");

                if (confirmDeletion)
                {
                    for (int i = 0; i < checkboxModels.Count; i++)
                    {
                        int j = checkboxModels[i].Value;

                        string urlID = restSvcApiUrl + j;

                        responseMessage = await httpClient.DeleteAsync(urlID);

                    }
                }
                //StateHasChanged();
                navigationManager.NavigateTo("/displayemployees");
            }
            else
            {
                await jsRuntime.InvokeVoidAsync("alert", "Please select employee to delete.");
                navigationManager.NavigateTo("/displayemployees");
            }
        }
        catch(Exception ex)
        {
            System.Console.WriteLine("Exception: " + ex.Message);
        }
    }
}

The condition if (checkboxModels.Count != 0) on Line 23 checks if the list is empty or not. If the list is not empty, that means there are employees that need to be deleted. The checkboxModels variable, of course, contains the value of our state object which contains the Id’s of employees to be deleted.

Before we delete a record in a database application, it is imperative that we ask the user for confirmation. Line 25 displays pop-up box asking the user, “Are you sure want to delete?”

If user confirmed desire to delete, then, on line 29 we iterate through the list using a for loop, sending a DELETE request per Id in the list. We, then, navigate back to DisplayEmployee page after we have deleted all the employees in the list.

On line 44, also notice another pop-up asking the user to select employees to delete. This is when the user hit the “Delete Selected” button without first selecting employees.

That’s it, folks! That concludes the implementation of my simple employee database management system.

Conclusion

In this tutorial, I demonstrated how to use Blazor WASM to create a Single-Page Application(SPA) that serves as a client application to a remote database Web API service, how to use EditContext class to handle validation errors from the Web API service, and how to use State Container Object to share global data among components . We implemented 4 Blazor components which implements CRUD (create, read, update, and delete). Specifically, this is an Employee Database Management System(DBMS) which allows users to create, display, update and delete employees in a database. The database itself is part of a separate Employee Database Web API application that I implemented in a previous tutorial.

Alejandrio Vasay
Alejandrio Vasay

Welcome to Coder Schmoder! I'm a .NET developer with a 15+ years of web and software development experience. I created this blog to impart my knowledge of programming to those who are interested in learning are just beginning in their programming journey.

I live in DFW, Texas and currently working as a .NET /Web Developer. I earned my Master of Computer Science degree from Texas A&M University, College Station, Texas. I hope, someday, to make enough money to travel to my birth country, the Philippines, and teach software development to people who don't have the means to learn it.

Articles: 21

Leave a Reply

Your email address will not be published. Required fields are marked *