Friday, May 31, 2019

Blazor client-side app with CRUD operations against a Web API endpoint

A companion video for this article is: https://www.youtube.com/watch?v=wkSR3eo4Tek

Source code for this application can be found at:
https://github.com/medhatelmasry/BlazingSchool

Overview

What is Blazor?
Blazor is a framework for developing interactive client-side web applications using C# instead of JavaScript.

What are we going to do in this Tutorial?In this tutorial I will show you how to build a Blazor application with the following structure:
1) The class library will have model definitions that are shared between the Web API and Blazor Visual Studio projects. The model that we will create in this tutorial is a simple C# Student model that looks like this:

public class Student {
  public string StudentId { get; set; }
  [Required]
  public string FirstName { get; set; }
  [Required]
  public string LastName { get; set; }
  [Required]
  public string School { get; set; }
}

2) The ASP.NET Core Web API app will provide the REST endpoints for the students service that the Blazor client-side application will consume. It will use the GET, POST, PUT and DELETE methods to carry out CRUD operations with the API service. Entity Framework will be used to save data in SQL Server using the "Code First" approach.

3) The Client-side Blazor app will provide the user interface for processing student data.

Perquisites

This tutorial was written while Blazor was in preview mode. I used the following site for setting up my environment:

This is how I setup my development environment:

- .NET Core 3.0 Preview SDK installed from https://dotnet.microsoft.com/download/dotnet-core/3.0

- Install Blazor templates by running the following command in a terminal window:

dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.0.0-preview5-19227-01

- Visual Studio 2019 preview version installed from: https://visualstudio.com/preview

- Latest Blazor extension for Visual Studio 2019 installed from https://go.microsoft.com/fwlink/?linkid=870389.

Creating the class library

Create a new project in Visual Studio 2019.

Choose "Class Library (.NET Standard)" then click Next:

Set these values on the next dialog:

Project name: SchoolLibrary
Solution name: School

Click on Create.

We need to install a package to the class library project. From within a terminal window at the main SchoolLibrary folder, run the following command:

dotnet add package System.ComponentModel.Annotations

Delete Class1.cs, then create a new Student.cs class file and add to it the following code:
public class Student {
  public string StudentId { get; set; }
  [Required]
  public string FirstName { get; set; }
  [Required]
  public string LastName { get; set; }
  [Required]
  public string School { get; set; }
}

Resolve "Required" by adding the following using statement at the top of Student.cs:
using System.ComponentModel.DataAnnotations;

Creating the ASP.NET Core Web API student service application

Right-click on the main solution node in Solution Explorer and select: Add >> New Project. Choose the "ASP.NET Core Web Application" template:
Name the Project name: SchoolAPI

Click on Create.

On the next dialog, select ASP.NET Core 2.2 and choose the API template then click on Create:

Our Web API project needs to use the Student class in the SchoolLibrary project. Therefore, we will make a reference from the SchoolAPI project to the SchoolLibrary project. Right-click on the SchoolAPI project node then: Add >> Reference... >> Projects >> Solution >> check SchoolLibrary. Then click on OK.


We need to let the dotnet utility know that we are using .NET Core 2.2 in the SchoolAPI project, even though the default SDK on our computer is a preview version of .NET Core 3.0. This is done by adding a global.json file in the root SchoolAPI folder with the following content:
{
  "sdk": {
    "version": "2.2.103"
  }
}

A quick way of doing this is to run the following CLI command inside the SchoolAPI folder:
dotnet new globaljson --sdk-version 2.2.103

Since we will be using SQL Server, we will need to add the appropriate Entity Framework packages and tooling. From within a terminal window at the root of your StudentAPI project,  run the following commands that will import the appropriate database related packages:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.SqlServer.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools

We need to add a connection string to the database. Add the following to the top of the appsettings.json file in project SchoolAPI:
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=SchoolDB;Trusted_Connection=True;MultipleActiveResultSets=true"
  },

We will be using the Entity Framework Code First approach. The starting point is to create a database context class. Add a C#  class file named SchoolDbContext.cs with the following class code:
public class SchoolDbContext : DbContext {
  public DbSet<Student> Students { get; set; }

  public SchoolDbContext(DbContextOptions<SchoolDbContext> options) : base(options) { }

  protected override void OnModelCreating(ModelBuilder builder) {
    base.OnModelCreating(builder);
    
    builder.Entity<Student>().HasData(
      new { StudentId = Guid.NewGuid().ToString(), 
            FirstName = "Jane", LastName = "Smith", School = "Medicine" },
      new { StudentId = Guid.NewGuid().ToString(), 
            FirstName = "John", LastName = "Fisher", School = "Engineering" },
      new { StudentId = Guid.NewGuid().ToString(), 
            FirstName = "Pamela", LastName = "Baker", School = "Food Science" },
      new { StudentId = Guid.NewGuid().ToString(), 
            FirstName = "Peter", LastName = "Taylor", School = "Mining" }
    );
  }
}

You will need to resolve some of the unrecognized namespaces.

Notice the above code is adding four records of seed data into the database.

In the Startup.cs file in the SchoolAPI project, add the following code to the ConfigureServices() method so that our application can use SQL Server:

services.AddDbContext<SchoolDbContext>(
  option => option.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
We are now ready to apply Entity Framework migrations, create the database and seed data. Remember to build your entire solution before proceeding. Then, from within a terminal window in the SchoolAPI directory, run the following command to create migrations:
dotnet ef migrations add initial_migration

This results in the creation of a migration file ending with the name ....initial_migration.cs in the Migrations folder. In my case, this file looked like this:
using Microsoft.EntityFrameworkCore.Migrations;
namespace SchoolAPI.Migrations {
  public partial class initial_migration : Migration {
    protected override void Up(MigrationBuilder migrationBuilder) {
      migrationBuilder.CreateTable(
        name: "Students",
        columns: table => new {
          StudentId = table.Column<string>(nullable: false),
          FirstName = table.Column<string>(nullable: false),
          LastName = table.Column<string>(nullable: false),
          School = table.Column<string>(nullable: false)
        },
        constraints: table => {
          table.PrimaryKey("PK_Students", x => x.StudentId);
        });

      migrationBuilder.InsertData(
        table: "Students",
        columns: new[] { "StudentId", "FirstName", "LastName", "School" },
        values: new object[,] {
          { "8734ce0b-4337-487a-bd86-102130d47f8d", "Jane", 
            "Smith", "Medicine" },
          { "db356c3a-d745-4f65-9aac-234f706859c2", "John", 
            "Fisher", "Engineering" },
          { "7721c620-a82b-4204-80af-6c7636efc81e", "Pamela", 
            "Baker", "Food Science" },
          { "9a85f51f-160e-4003-9de9-b01354e180a3", "Peter", 
            "Taylor", "Mining" }
        });
    }

    protected override void Down(MigrationBuilder migrationBuilder) {
      migrationBuilder.DropTable(
        name: "Students");
    }
  }
}

Note that the above code also includes commands for inserting sample data.

The next step is to create the SchoolDB database in SQL Server. This is done by running the following command from inside a terminal window at the SchoolAPI folder.

                                dotnet ef database update


If no errors are encountered, we can assume that the database was created and properly seeded with data. Let us create an API controller so that we can see the data that is in our database.  

Right-click on the Controllers node in the SchoolAPI project then: Add >> Controller...

Under Installed >> Common, choose the API tab, highlight "API Controller with actions, using Entity Framework" then click Add.



On the next dialog, make the following choices:

Model class: Student (SchoolLibrary)
Data context class: SchoolDbContext (SchoolAPI)
Controller name: StudentsController


Once you click on Add, the StudentsController.cs file is created in the Controllers folder. To load that particular controller every time you run the SchoolAPI application, edit the launchSettings.json file under the Properties node. Change every instance of "api/values" to "api/students". Now you can run the application by hitting CTRL F5 on your keyboard. This is what you should see in your browser:

[{"studentId":"7721c620-a82b-4204-80af-6c7636efc81e",
  "firstName":"Pamela","lastName":"Baker","school":"Food Science"},
{"studentId":"8734ce0b-4337-487a-bd86-102130d47f8d",
  "firstName":"Jane","lastName":"Smith","school":"Medicine"},
{"studentId":"9a85f51f-160e-4003-9de9-b01354e180a3",
  "firstName":"Peter","lastName":"Taylor","school":"Mining"},
{"studentId":"db356c3a-d745-4f65-9aac-234f706859c2",
  "firstName":"John","lastName":"Fisher","school":"Engineering"}]

Even though our API app seems to be working fine, there is one more thing we need to do. We need to enable CORS (Cross Origin Resource Sharing) so that the service can be accessed from other domains. Add the following code to the ConfigureServices() method in the Startup.cs file found in the SchoolAPI project:

// Add Cors
services.AddCors(o => o.AddPolicy("Policy", builder => {
  builder.AllowAnyOrigin()
    .AllowAnyMethod()
    .AllowAnyHeader();
}));

Add this statement in the Configure() method also in Startup.cs just before app.UseMvc():

            app.UseCors("Policy");

We now have an API and data that we can work with. Let us now see how we can use this data in a Blazor app.

Creating our Client-Side Blazor app

Let us add a client-side Blazor project to our solution. In Solution Explorer, right-click on the Solution node then: Add >> New Project...

Choose the "ASP.NET Core Web Application" template then click on Next.


Name the project SchoolClient, then click on Create.

Choose "ASP.NET Core 3.0" and the "Blazor (client-side)" template then click on Create.

Let us run our Blazor application to see what we have out of the box. Run the SchoolClient application by hitting CTRL F5 on your keyboard. You will see a UI that looks like this:

We will add a Students razor page and menu item to this client-side Blazor application.

Our Blazor project needs to use the Student class in the class library. Therefore, we will make a reference from the Blazor SchoolClient project to the class library project SchoolLibrary. Right-click on the SchoolClient project node then: Add >> Reference... >> Projects >> Solution >> check SchoolLibrary. Then click OK.


Open the _Imports.razor file in the editor and add the following using statement to the bottom of the content so that the Student class is available to a  views:

@using SchoolLibrary

Make a duplicate copy of the FetchData.razor file in the Pages node and call the new file Students.razor. Replace its contents with the following code:

@page "/students"
@inject HttpClient httpClient

<h1>Students</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (students == null) {
<p><em>Loading...</em></p>
} else {
<table class='table table-hover'>
  <thead>
    <tr>
      <th>ID</th>
      <th>First Name</th>
      <th>Last Name</th>
      <th>School</th>
    </tr>
  </thead>
  <tbody>
    @foreach (var item in students) {
    <tr>
      <td>@item.StudentId</td>
      <td>@item.FirstName</td>
      <td>@item.LastName</td>
      <td>@item.School</td>
    </tr>
    }
  </tbody>
</table>
}

@functions {
  Student[] students;
  string baseUrl = "https://localhost:44318/";

  protected override async Task OnInitAsync() {
    await load();
  }

  protected async Task load() {
    students = await httpClient.GetJsonAsync<Student[]>($"{baseUrl}api/students");
  }
}

NOTE: Make sure you adjust the value of baseUrl to match the URL of your SchoolAPI service.

Let us focus on the @functions block. The OnInitAsyns() method is called when the page gets loaded. It calls a local load() method. The load() method loads a students array with data from our SchoolAPI service. The remaining HTML/Razor code simply displays the data in a table.

Let us add a menu item to the left-side navigation of our client application. Open Shared/NavMenu.razor in the editor and add the following <li> to the <ul> block (around line 24):

<li class="nav-item px-3">
  <NavLink class="nav-link" href="students">
    <span class="oi oi-list-rich" aria-hidden="true"></span> Students
  </NavLink>
</li>

You must be looking forward to testing our client-side application. To run your app, highlight the SchoolAPI node then hit CTRL F5 to run the server-side application without debugging. This starts the API service.

Next, we will run the client-side Blazor application. Highlight the SchoolClient node then hit CTRL F5 to run the blazor app. This is what it should look like when you click on Students:


Adding data using the POST method
The client side application will not be complete without adding to it add, edit and delete funtionality. We shall start with adding data. Place the following instance variables just above the OnInitAsyc() method:

string studentId;
string firstName;
string lastName;
string school;

We need an HTML form to add data. Add the following RAZOR code just before @functions:

@if (students != null) // Insert form
{
  <input placeholder="First Name" bind="@firstName" /><br />
  <input placeholder="Last Name" bind="@lastName" /><br />
  <input placeholder="School" bind="@school" /><br />
  <button onclick="@Insert" class="btn btn-warning">Insert</button>
}

When the Insert button is clicked, an Insert() method is called. Add the following Insert() method inside the @functions() block that makes a POST request to the API service:
protected async Task Insert() {
  string endpoint = $"{baseUrl}api/students";

  Student student = new Student() {
    StudentId = Guid.NewGuid().ToString(),
    FirstName = firstName,
    LastName = lastName,
    School = school
  };

  await httpClient.SendJsonAsync(HttpMethod.Post, endpoint, student);
  ClearFields();
  await load();
}

After data is added, the above code clears the fields then loads the data again in the HTML table. Add the following ClearFields() method:

protected void ClearFields() {
  studentId = string.Empty;
  firstName = string.Empty;
  lastName = string.Empty;
  school = string.Empty;
}

Run the Blazor client-side application and select Students from the navigation menu. This is what it should look like:



I entered David, Strong and Entertainment for data and when I clicked on Insert I got the following data added to the database:
Updating & Deleting data using the PUT & DELETE methods
To distinguish between INSERT and EDIT/DELETE mode, we shall add an enum to our code. Add the following to the list of instance variables:

private enum MODE { None, Add, EditDelete };
MODE mode = MODE.None;
Student student;

We will add a button at the top of our table dedicated to adding data. Add the following markup just above the opening <table> tag:

<button onclick="@Add"  class="btn btn-success">Add</button>

Let us add the following Add() method:

protected void Add() {
  ClearFields();
  mode = MODE.Add;
}

Around line 34, change the @if (students != null) statement to:

@if (students != null && mode==MODE.Add)

If you run the application, the form for inserting data only displays when the Add button is clicked:
After we successfully insert data, we want the insert form to disappear. Add the following code to the bottom of the Insert() method:
mode = MODE.None;
Let us now add a form that only appears when we wish to update or delete data. Add the following just before @functions:

@if (students != null && mode==MODE.EditDelete) // Update & Delete form
{
  <input type="hidden" bind="@studentId" /><br />
  <input placeholder="First Name" bind="@firstName" /><br />
  <input placeholder="Last Name" bind="@lastName" /><br />
  <input placeholder="School" bind="@school" /><br />
  <button onclick="@Update" class="btn btn-primary">Update</button>
  <span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
  <button onclick="@Delete" class="btn btn-danger">Delete</button>
}

Add these Update() and Delete() methods as shown below:

protected async Task Update() {
  string endpoint = $"{baseUrl}api/students/{studentId}";

  Student student = new Student() {
    StudentId = studentId,
    FirstName = firstName,
    LastName = lastName,
    School = school
  };

  await httpClient.SendJsonAsync(HttpMethod.Put, endpoint, student);
  ClearFields();
  await load();
  mode = MODE.None;
}

protected async Task Delete() {
  string endpoint = $"{baseUrl}api/students/{studentId}";
  await httpClient.SendJsonAsync(HttpMethod.Delete, endpoint, null);
  ClearFields();
  await load();
  mode = MODE.None;
}

We want to be able to select a row of data and update it. We can add an onclick handler to a row. In the HTML table, replace the <tr> opening tag under the @foreach statement with this:

<tr onclick="@(() => Show(item.StudentId))">

The above would pass the appropriate studentId to a method named Show() whenever a row is clicked. Add the following Show() method that retrieves a student from the API service and displays it in the Update/Delete form:

protected async Task Show(string id) {
  student = await httpClient.GetJsonAsync<Student>($"{baseUrl}api/students/{id}");
  studentId = student.StudentId;
  firstName = student.FirstName;
  lastName = student.LastName;
  school = student.School;
  mode = MODE.EditDelete;
}

Let us run the application and test it out for adding, updating and deleting data. Run the application:
Click on the Add button to insert data. 
Enter data then click on the Insert button. The data should get added to the database:

To update the data, click on a row. The selected data should display in the update/delete form.

I selected Bill Black and changed Black to White then clicked on Update. Last Name was successfully changed to White.

Lastly, let us delete a record. I clicked on the Bill White row. Data was displayed in the Update/Delete form.
When I clicked on Delete, Bill White get deleted.
I hope you found this tutorial useful. I expect Blazor to change by the time it is generally available in release mode. I will do my best to update this article with any breaking changes.

No comments:

Post a Comment