In this tutorial we'll go through an example
of how to build a simple user registration, login and user management (CRUD)
application with Blazor WebAssembly.
The blazor app runs with a fake
backend by default to enable it to run completely in the browser without a
real backend api (backend-less), to switch to a real api you just have to
change the "fakeBackend" setting to "false" in the app settings file (/wwwroot/appsettings.json). You can build your own api or hook it up
with the ASP.NET Core api or Node.js api available (instructions below).
The project is available on GitHub
at https://github.com/cornflourblue/blazor-webassembly-registration-login-example.
Styling of the example app is all done with
Bootstrap 4.5 CSS, for more info about Bootstrap see https://getbootstrap.com/docs/4.5/getting-started/introduction/.
Here it is in action:
(Hosted on GitHub Pages at https://cornflourblue.github.io/blazor-webassembly-registration-login-example/)
Tools required to run the Blazor Login Example Locally
To develop and run ASP.NET Core Blazor
applications locally, download and install the following:
- .NET Core SDK - includes the .NET
Core runtime and command line tools
- Visual Studio Code - code editor
that runs on Windows, Mac and Linux
- C# extension for Visual Studio Code
- adds support to VS Code for developing .NET Core applications
For more detailed instructions on setting up
your local dev environment see ASP.NET Core - Setup Development Environment.
Running the Blazor Login Tutorial Example Locally
- Download or clone the tutorial
project code from https://github.com/cornflourblue/blazor-webassembly-registration-login-example
- Start the app by running dotnet
run from the command line in
the project root folder (where the BlazorApp.csproj file is located)
- Open a new browser tab and
navigate to the URL http://localhost:5000
NOTE: To enable hot reloading during
development so the app automatically restarts when a file is changed, start the
app with the command dotnet watch run.
Running the Blazor WebAssembly App with an ASP.NET Core 3.1 API
For full details about the example ASP.NET
Core API see the post ASP.NET Core 3.1 - Simple API for Authentication,
Registration and User Management. But to get up and running quickly
just follow the below steps.
- Download or clone the project
source code from https://github.com/cornflourblue/aspnet-core-3-registration-login-api
- Start the api by running dotnet
run from the command line in
the project root folder (where the WebApi.csproj file is located), you
should see the message Now listening on: http://localhost:4000.
- Back in the Blazor WebAssembly
app, change the "fakeBackend" setting
to "false" in
the app settings file (/wwwroot/appsettings.json), then start the Blazor app and it should now be
hooked up with the ASP.NET Core API.
Running the Blazor App with a Node.js + MySQL API
For full details about the example Node.js +
MySQL API see the post NodeJS + MySQL - Simple API for Authentication,
Registration and User Management. But to get up and running quickly
just follow the below steps.
- Install MySQL Community Server
from https://dev.mysql.com/downloads/mysql/ and
ensure it is started. Installation instructions are available at https://dev.mysql.com/doc/refman/8.0/en/installing.html.
- Install NodeJS and NPM
from https://nodejs.org.
- Download or clone the project
source code from https://github.com/cornflourblue/node-mysql-registration-login-api
- Install all required npm
packages by running npm install or npm i from
the command line in the project root folder (where the package.json is
located).
- Start the api by running npm
start from the command line in
the project root folder, you should see the message Server
listening on port 4000.
- Back in the Blazor WebAssembly
app, change the "fakeBackend" setting
to "false" in
the app settings file (/wwwroot/appsettings.json), then start the Blazor app and it should now be
hooked up with the Node + MySQL API.
Running the Blazor App with a Node.js + MongoDB API
For full details about the example Node.js + MongoDB
API see the post NodeJS + MongoDB - Simple API for Authentication,
Registration and User Management. But to get up and running quickly
just follow the below steps.
- Install MongoDB Community
Server from https://www.mongodb.com/download-center.
- Run MongoDB, instructions are
available on the install page for each OS at https://docs.mongodb.com/manual/administration/install-community/
- Install NodeJS and NPM
from https://nodejs.org.
- Download or clone the project
source code from https://github.com/cornflourblue/node-mongo-registration-login-api
- Install all required npm
packages by running npm install or npm i from
the command line in the project root folder (where the package.json is
located).
- Start the api by running npm
start from the command line in
the project root folder, you should see the message Server
listening on port 4000.
- Back in the Blazor WebAssembly
app, change the "fakeBackend" setting
to "false" in
the app settings file (/wwwroot/appsettings.json), then start the Blazor app and it should now be
hooked up with the Node + Mongo API.
Blazor WebAssembly Project Structure
The .NET Core CLI (dotnet) was used to generate the base project
structure with the command dotnet new blazorwasm -o BlazorApp, the CLI is also used to build and serve the
application. For more info about the .NET Core CLI see https://docs.microsoft.com/en-us/dotnet/core/tools/.
The tutorial project is organised into the
following folders:
Pages
ASP.NET Core Razor components that contain the
pages for the Blazor application. Each component specifies which route it is
bound to with a @page directive at the
top of the file (e.g. @page "/account/login" in the login component).
Shared
ASP.NET Core Razor components that can be used
in multiple areas of the application and are not bound to a specific route.
Services
Contain the core logic for the application and
handles most of the heavy lifting so page components can be kept as lean and
simple as possible. The services layer encapsulates all http communication with
backend apis and interaction with local storage, and exposes a simple set of
interfaces for the rest of the app to use.
Models
Represent the model data handled by the Blazor
application and transferred between components and services, including data
received in api responses and sent in requests. Form validation is implemented
in models with data annotation attributes (e.g. [Required]).
Helpers
Anything that doesn't fit into the above
folders.
wwwroot
The Blazor project "web root" folder
that contains static files including the root index.html file or host
page (/wwwroot/index.html),
css stylesheets, images and app settings (/wwwroot/appsettings.json). Everything in the wwwroot folder is
publicly accessible via a web request so make sure you only include static
files that should be public.
docs
You can ignore this folder, it just contains a
compiled demo of the code hosted on GitHub Pages at https://cornflourblue.github.io/blazor-webassembly-registration-login-example/
App Route View Component
Path: /Helpers/AppRouteView.cs
The app route view component is used inside
the app component and renders the page
component for the current route along with its layout.
If the page component for the route contains
an authorize attribute (@attribute [Authorize])
then the user must be logged in, otherwise they will be redirected to the login
page.
The app route view extends the built in
ASP.NET Core RouteView component and uses the base
class to render the page by calling base.Render(builder).
using BlazorApp.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using System;
using System.Net;
namespace BlazorApp.Helpers
{
public class AppRouteView : RouteView
{
[Inject]
public NavigationManager NavigationManager { get; set; }
[Inject]
public IAccountService AccountService { get; set; }
protected override void Render(RenderTreeBuilder builder)
{
var authorize = Attribute.GetCustomAttribute(RouteData.PageType, typeof(AuthorizeAttribute)) != null;
if (authorize &&
AccountService.User == null)
{
var returnUrl = WebUtility.UrlEncode(new Uri(NavigationManager.Uri).PathAndQuery);
NavigationManager.NavigateTo($"account/login?returnUrl={returnUrl}");
}
else
{
base.Render(builder);
}
}
}
}
Extension Methods
Path: /Helpers/ExtensionMethods.cs
The extension methods class adds a couple of
simple extension methods to the NavigationManager for accessing query string parameters in
the URL.
For more info see Blazor WebAssembly - Get Query String Parameters with
Navigation Manager.
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Specialized;
using System.Web;
namespace BlazorApp.Helpers
{
public static class ExtensionMethods
{
public static NameValueCollection QueryString(this NavigationManager navigationManager)
{
return HttpUtility.ParseQueryString(new Uri(navigationManager.Uri).Query);
}
public static string QueryString(this NavigationManager navigationManager, string key)
{
return navigationManager.QueryString()[key];
}
}
}
Fake Backend Handler
Path: /Helpers/FakeBackendHandler.cs
In order to run and test the Blazor
application without a real backend API, the example uses a fake backend handler
that intercepts the HTTP requests from the Blazor app and sends back
"fake" responses. The fake backend handler inherits from the ASP.NET
Core HttpClientHandler class and is configured
with the http client in Program.cs.
The fake backend contains a handleRoute() local function that checks if the
request matches one of the faked routes, at the moment these include requests
for handling registration, authentication and user CRUD operations. Matching
requests are intercepted and handled by one of the below // route functions, non-matching requests are sent through to
the real backend by calling base.SendAsync(request, cancellationToken);. Below the route functions there are // helper
functions for returning
different response types and performing small tasks.
using BlazorApp.Models.Account;
using BlazorApp.Services;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace BlazorApp.Helpers
{
public class FakeBackendHandler : HttpClientHandler
{
private ILocalStorageService _localStorageService;
public FakeBackendHandler(ILocalStorageService localStorageService)
{
_localStorageService = localStorageService;
}
protected override async
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// array in local storage
for registered users
var usersKey = "blazor-registration-login-example-users";
var users = await
_localStorageService.GetItem<List<UserRecord>>(usersKey) ?? new List<UserRecord>();
var method = request.Method;
var path =
request.RequestUri.AbsolutePath;
return await handleRoute();
async
Task<HttpResponseMessage> handleRoute()
{
if (path == "/users/authenticate" && method ==
HttpMethod.Post)
return await authenticate();
if (path == "/users/register" && method ==
HttpMethod.Post)
return await register();
if (path == "/users" && method ==
HttpMethod.Get)
return await getUsers();
if (Regex.Match(path, @"\/users\/\d+$").Success && method
== HttpMethod.Get)
return await getUserById();
if (Regex.Match(path, @"\/users\/\d+$").Success && method
== HttpMethod.Put)
return await updateUser();
if (Regex.Match(path, @"\/users\/\d+$").Success && method
== HttpMethod.Delete)
return await deleteUser();
// pass
through any requests not handled above
return await base.SendAsync(request, cancellationToken);
}
// route functions
async Task<HttpResponseMessage>
authenticate()
{
var bodyJson = await request.Content.ReadAsStringAsync();
var body = JsonSerializer.Deserialize<Login>(bodyJson);
var user = users.FirstOrDefault(x => x.Username ==
body.Username && x.Password == body.Password);
if (user == null)
return await error("Username or password is
incorrect");
return await ok(new {
Id = user.Id.ToString(),
Username = user.Username,
FirstName = user.FirstName,
LastName = user.LastName,
Token = "fake-jwt-token"
});
}
async Task<HttpResponseMessage>
register()
{
var bodyJson = await request.Content.ReadAsStringAsync();
var body = JsonSerializer.Deserialize<AddUser>(bodyJson);
if (users.Any(x => x.Username ==
body.Username))
return await error($"Username
'{body.Username}' is already taken");
var user = new UserRecord {
Id = users.Count > 0 ? users.Max(x => x.Id) + 1 : 1,
Username = body.Username,
Password = body.Password,
FirstName = body.FirstName,
LastName = body.LastName
};
users.Add(user);
await _localStorageService.SetItem(usersKey, users);
return await ok();
}
async
Task<HttpResponseMessage> getUsers()
{
if (!isLoggedIn()) return await unauthorized();
return await ok(users.Select(x => basicDetails(x)));
}
async
Task<HttpResponseMessage> getUserById()
{
if (!isLoggedIn()) return await unauthorized();
var user = users.FirstOrDefault(x => x.Id == idFromPath());
return await ok(basicDetails(user));
}
async
Task<HttpResponseMessage> updateUser()
{
if (!isLoggedIn()) return await unauthorized();
var bodyJson = await request.Content.ReadAsStringAsync();
var body = JsonSerializer.Deserialize<EditUser>(bodyJson);
var user = users.FirstOrDefault(x => x.Id == idFromPath());
// if
username changed check it isn't already taken
if (user.Username !=
body.Username && users.Any(x => x.Username == body.Username))
return await error($"Username
'{body.Username}' is already taken");
// only
update password if entered
if (!string.IsNullOrWhiteSpace(body.Password))
user.Password = body.Password;
// update
and save user
user.Username = body.Username;
user.FirstName = body.FirstName;
user.LastName = body.LastName;
await _localStorageService.SetItem(usersKey, users);
return await ok();
}
async
Task<HttpResponseMessage> deleteUser()
{
if (!isLoggedIn()) return await unauthorized();
users.RemoveAll(x => x.Id == idFromPath());
await _localStorageService.SetItem(usersKey, users);
return await ok();
}
// helper functions
async Task<HttpResponseMessage>
ok(object body = null)
{
return await jsonResponse(HttpStatusCode.OK, body ?? new {});
}
async
Task<HttpResponseMessage> error(string message)
{
return await jsonResponse(HttpStatusCode.BadRequest, new { message });
}
async
Task<HttpResponseMessage> unauthorized()
{
return await jsonResponse(HttpStatusCode.Unauthorized,
new { message = "Unauthorized" });
}
async
Task<HttpResponseMessage> jsonResponse(HttpStatusCode statusCode, object content)
{
var response = new HttpResponseMessage
{
StatusCode = statusCode,
Content = new StringContent(JsonSerializer.Serialize(content), Encoding.UTF8, "application/json")
};
// delay
to simulate real api call
await Task.Delay(500);
return response;
}
bool isLoggedIn()
{
return
request.Headers.Authorization?.Parameter == "fake-jwt-token";
}
int idFromPath()
{
return int.Parse(path.Split('/').Last());
}
dynamic basicDetails(UserRecord user)
{
return new {
Id = user.Id.ToString(),
Username = user.Username,
FirstName = user.FirstName,
LastName = user.LastName
};
}
}
}
// class for user records
stored by fake backend
public class UserRecord {
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
}
String Converter
Path: /Helpers/StringConverter.cs
The string converter is a custom JSON
converter used in the sendRequest<T>() method
of the http service to deserialize numbers as
strings from API responses.
This makes the Blazor app compatible with APIs
that return either integer id or string id properties in JSON responses. Without the string
converter, an API that returns an int id results in the error: The JSON
value could not be converted to System.String. Path: $.id.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace BlazorApp.Helpers
{
public class StringConverter : JsonConverter<string>
{
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// deserialize numbers as
strings.
if (reader.TokenType ==
JsonTokenType.Number)
{
return reader.GetInt32().ToString();
}
else if (reader.TokenType ==
JsonTokenType.String)
{
return reader.GetString();
}
throw new System.Text.Json.JsonException();
}
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
writer.WriteStringValue(value);
}
}
}
Add User Model
Path: /Models/Account/AddUser.cs
The add user model represents the data and
validation rules for registering or adding a new user. The model is bound to
the register form and add user form, which use it to pass form data
to the AccountService.Register() method to create new user accounts.
using System.ComponentModel.DataAnnotations;
namespace BlazorApp.Models.Account
{
public class AddUser
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
[Required]
public string Username { get; set; }
[Required]
[MinLength(6, ErrorMessage = "The Password field must
be a minimum of 6 characters")]
public string Password { get; set; }
}
}
Edit User Model
Path: /Models/Account/EditUser.cs
The edit user model represents the data and
validation rules for updating an existing user account. The model is bound to
the edit user form which uses it to pass form
data to the AccountService.Update() method.
using System.ComponentModel.DataAnnotations;
namespace BlazorApp.Models.Account
{
public class EditUser
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
[Required]
public string Username { get; set; }
[MinLength(6, ErrorMessage = "The Password field must
be a minimum of 6 characters")]
public string Password { get; set; }
public EditUser() { }
public EditUser(User user)
{
FirstName = user.FirstName;
LastName = user.LastName;
Username = user.Username;
}
}
}
Login Model
Path: /Models/Account/Login.cs
The login model represents the data and
validation rules for logging into the Blazor app. The model is bound to
the login form which uses it to pass form
data to the AccountService.Login() method.
using System.ComponentModel.DataAnnotations;
namespace BlazorApp.Models.Account
{
public class Login
{
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
}
}
Alert Model
Path: /Models/Alert.cs
The alert model defines the properties of an
alert, and the AlertType enum defines the
different types of alerts supported by the application.
The model and enum are used by the alert service and alert component for sending, receiving
and displaying alerts in the blazor app.
namespace BlazorApp.Models
{
public class Alert
{
public string Id { get; set; }
public AlertType Type { get; set; }
public string Message { get; set; }
public bool AutoClose { get; set; }
public bool KeepAfterRouteChange { get; set; }
public bool Fade { get; set; }
}
public enum AlertType
{
Success,
Error,
Info,
Warning
}
}
User Model
Path: /Models/User.cs
The user model defines the properties of a
user account, it's used by the account service for handling user data
returned from the api, and other parts of the application for passing user data
around.
The IsDeleting property is used by the users home page to show a spinner while a
user account is deleting.
namespace BlazorApp.Models
{
public class User
{
public string Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
public string Token { get; set; }
public bool IsDeleting { get; set; }
}
}
Account Imports File
Path: /Pages/Account/_Imports.razor
Razor imports files include directives that
are automatically applied to all other razor components in the same folder and
subfolders. An _Imports.razor file can be
added to any folder and can include any razor directive.
By adding the below @layout directive to this imports file, all
blazor components in the /Pages/Account folder will use the account layout.
@layout Layout
Account Layout Component
Path: /Pages/Account/Layout.razor
The account layout component is a nested
layout for all pages in the /Pages/Account folder, it simply wraps the @Body in a div with some bootstrap classes to set the width and alignment
of all of the account pages. It is set as the layout for all account pages in
the account imports file.
The OnInitialized() Blazor lifecycle method is used to automatically redirect
the user to the home page if they are already logged in.
@inherits LayoutComponentBase
@layout MainLayout
@inject IAccountService AccountService
@inject NavigationManager NavigationManager
<div class="col-md-6 offset-md-3 mt-5">
@Body
</div>
@code {
protected
override void OnInitialized()
{
//
redirect to home if already logged in
if
(AccountService.User != null)
{
NavigationManager.NavigateTo("");
}
}
}
Login Page Component
Path: /Pages/Account/Login.razor
The login page component contains a login form
with username and password fields. It is built with the ASP.NET Core <EditForm> and <InputText> components, and displays validation
messages for invalid fields when the user attempts to submit the form or when a
field is changed.
On valid submit the AccountService.Login(model) method is called from the OnValidSubmit() method, if login is successful the user
is redirected back to the original page they were trying to access (or the home
page by default), if there is an error the error message is displayed with the
alert service.
The <DataAnnotationsValidator /> component enables support for validating
the form using the data annotations attributes on the Model class that is bound to the form
(e.g. [Required]), and the <ValidationMessage
For="..." /> components
display the validation message below each field. The login model class contains properties for
each of the fields in the form along with validation rules defined using data
annotations attributes.
For more info on ASP.NET Core Blazor forms and
validation Blazor WebAssembly - Form Validation Example.
@page "/account/login"
@inject IAccountService AccountService
@inject IAlertService AlertService
@inject NavigationManager NavigationManager
<div class="card">
<h4 class="card-header">Login</h4>
<div class="card-body">
<EditForm Model="@model" OnValidSubmit="OnValidSubmit">
<DataAnnotationsValidator />
<div class="form-group">
<label>Username</label>
<InputText @bind-Value="model.Username" class="form-control" />
<ValidationMessage For="@(() => model.Username)" />
</div>
<div class="form-group">
<label>Password</label>
<InputText @bind-Value="model.Password" type="password" class="form-control" />
<ValidationMessage For="@(() => model.Password)" />
</div>
<button disabled="@loading" class="btn btn-primary">
@if (loading)
{
<span class="spinner-border spinner-border-sm mr-1"></span>
}
Login
</button>
<NavLink href="account/register" class="btn btn-link">Register</NavLink>
</EditForm>
</div>
</div>
@code {
private
Models.Account.Login model = new Models.Account.Login();
private
bool loading;
private
async void OnValidSubmit()
{
//
reset alerts on submit
AlertService.Clear();
loading = true;
try
{
await AccountService.Login(model);
var returnUrl = NavigationManager.QueryString("returnUrl") ??
"";
NavigationManager.NavigateTo(returnUrl);
}
catch (Exception ex)
{
AlertService.Error(ex.Message);
loading = false;
StateHasChanged();
}
}
}
Logout Page Component
Path: /Pages/Account/Logout.razor
The logout page component logs out of the
example app by calling AccountService.Logout() from the Blazor OnInitialized() lifecycle method. The logout method of
the account service redirects to the user to
the login page after logout.
The @layout MainLayout directive is used to override the layout
in the account imports file to disable the
automatic redirect to the home page for logged in users.
@page "/account/logout"
@layout MainLayout
@inject IAccountService AccountService
@code {
protected override async void OnInitialized()
{
await AccountService.Logout();
}
}
Register Page Component
Path: /Pages/Account/Register.razor
The register page component contains a simple
registration form with fields for first name, last name, username and password.
It is built with the .NET Core <EditForm> and <InputText> components, and displays validation messages
for invalid fields when the user attempts to submit the form or when a field is
changed.
On valid submit the AccountService.Register(model) method is called from the OnValidSubmit() method, if registration is successful
the user is redirected to the login page with a success alert message, if there
is an error the error message is displayed with the alert service.
The <DataAnnotationsValidator /> component enables support for validating
the form using the data annotations attributes on the Model class that is bound to the form
(e.g. [Required]), and the <ValidationMessage
For="..." /> components
display the validation message below each field. The add user model class contains properties
for each of the fields in the form along with validation rules defined using
data annotations attributes.
For more info on ASP.NET Core Blazor forms and
validation Blazor WebAssembly - Form Validation Example.
@page "/account/register"
@inject IAccountService AccountService
@inject IAlertService AlertService
@inject NavigationManager NavigationManager
<div class="card">
<h4 class="card-header">Register</h4>
<div class="card-body">
<EditForm Model="@model" OnValidSubmit="OnValidSubmit">
<DataAnnotationsValidator />
<div class="form-group">
<label>First Name</label>
<InputText @bind-Value="model.FirstName" class="form-control" />
<ValidationMessage For="@(() => model.FirstName)" />
</div>
<div class="form-group">
<label>Last Name</label>
<InputText @bind-Value="model.LastName" class="form-control" />
<ValidationMessage For="@(() => model.LastName)" />
</div>
<div class="form-group">
<label>Username</label>
<InputText @bind-Value="model.Username" class="form-control" />
<ValidationMessage For="@(() => model.Username)" />
</div>
<div class="form-group">
<label>Password</label>
<InputText @bind-Value="model.Password" type="password" class="form-control" />
<ValidationMessage For="@(() => model.Password)" />
</div>
<button disabled="@loading" class="btn btn-primary">
@if (loading)
{
<span class="spinner-border spinner-border-sm mr-1"></span>
}
Register
</button>
<NavLink href="account/login" class="btn btn-link">Cancel</NavLink>
</EditForm>
</div>
</div>
@code {
private
AddUser model = new AddUser();
private
bool loading;
private
async void OnValidSubmit()
{
//
reset alerts on submit
AlertService.Clear();
loading = true;
try
{
await AccountService.Register(model);
AlertService.Success("Registration successful",
keepAfterRouteChange: true);
NavigationManager.NavigateTo("account/login");
}
catch (Exception ex)
{
AlertService.Error(ex.Message);
loading = false;
StateHasChanged();
}
}
}
Users Imports File
Path: /Pages/Users/_Imports.razor
Razor imports files include directives that
are automatically applied to all other razor components in the same folder and
subfolders. An _Imports.razor file can be
added to any folder and can include any razor directive.
By adding the below @layout directive to this imports file, all
blazor components in the /Pages/Users folder will use the users layout.
@layout Layout
Add User Page Component
Path: /Pages/Users/Add.razor
The add user page component contains a form
for creating a new user with fields for first name, last name, username and
password. It is built with the .NET Core <EditForm> and <InputText> components, and displays validation
messages for invalid fields when the user attempts to submit the form or when a
field is changed.
On valid submit the AccountService.Register(model) method is called from the OnValidSubmit() method, if registration is successful
the user is redirected to the users list page with a success alert message, if
there is an error the error message is displayed with the alert service.
The <DataAnnotationsValidator /> component enables support for validating
the form using the data annotations attributes on the Model class that is bound to the form
(e.g. [Required]), and the <ValidationMessage
For="..." /> components
display the validation message below each field. The add user model class contains properties
for each of the fields in the form along with validation rules defined using
data annotations attributes.
For more info on ASP.NET Core Blazor forms and
validation Blazor WebAssembly - Form Validation Example.
@page "/users/add"
@attribute [Authorize]
@inject IAlertService AlertService
@inject IAccountService AccountService
@inject NavigationManager NavigationManager
<h1>Add User</h1>
<EditForm Model="@model" OnValidSubmit="OnValidSubmit">
<DataAnnotationsValidator />
<div class="form-row">
<div class="form-group col">
<label>First Name</label>
<InputText @bind-Value="model.FirstName" class="form-control" />
<ValidationMessage For="@(() => model.FirstName)" />
</div>
<div class="form-group col">
<label>Last Name</label>
<InputText @bind-Value="model.LastName" class="form-control" />
<ValidationMessage For="@(() => model.LastName)" />
</div>
</div>
<div class="form-row">
<div class="form-group col">
<label>Username</label>
<InputText @bind-Value="model.Username" class="form-control" />
<ValidationMessage For="@(() => model.Username)" />
</div>
<div class="form-group col">
<label>Password</label>
<InputText @bind-Value="model.Password" type="password" class="form-control" />
<ValidationMessage For="@(() => model.Password)" />
</div>
</div>
<div class="form-group">
<button disabled="@loading" class="btn btn-primary">
@if (loading)
{
<span class="spinner-border spinner-border-sm mr-1"></span>
}
Save
</button>
<NavLink href="users" class="btn btn-link">Cancel</NavLink>
</div>
</EditForm>
@code {
private
AddUser model = new AddUser();
private
bool loading;
private
async void OnValidSubmit()
{
loading = true;
try
{
await AccountService.Register(model);
AlertService.Success("User added successfully",
keepAfterRouteChange: true);
NavigationManager.NavigateTo("users");
}
catch (Exception ex)
{
AlertService.Error(ex.Message);
loading = false;
StateHasChanged();
}
}
}
Edit User Page Component
Path: /Pages/Users/Edit.razor
The edit user page component contains a form
for updating an existing user with fields for first name, last name, username
and password. It is built with the .NET Core <EditForm> and <InputText> components, and displays validation
messages for invalid fields when the user attempts to submit the form or when a
field is changed.
The OnInitializedAsync() Blazor lifecycle method is used to fetch
the user account details from the account service and populate the model bound
to the form. A loading spinner is displayed while the account details are
loading.
On valid submit the AccountService.Update(model) method is called from the OnValidSubmit() method, if the update is successful the
user is redirected to the users list page with a success alert message, if
there is an error the error message is displayed with the alert service.
The <DataAnnotationsValidator /> component enables support for validating
the form using the data annotations attributes on the Model class that is bound to the form
(e.g. [Required]), and the <ValidationMessage
For="..." /> components
display the validation message below each field. The edit user model class contains properties
for each of the fields in the form along with validation rules defined using
data annotations attributes.
For more info on ASP.NET Core Blazor forms and
validation Blazor WebAssembly - Form Validation Example.
@page "/users/edit/{Id}"
@attribute [Authorize]
@inject IAlertService AlertService
@inject IAccountService AccountService
@inject NavigationManager NavigationManager
<h1>Edit User</h1>
@if (model != null)
{
<EditForm Model="@model" OnValidSubmit="OnValidSubmit">
<DataAnnotationsValidator />
<div class="form-row">
<div class="form-group col">
<label>First Name</label>
<InputText @bind-Value="model.FirstName" class="form-control" />
<ValidationMessage For="@(() => model.FirstName)" />
</div>
<div class="form-group col">
<label>Last Name</label>
<InputText @bind-Value="model.LastName" class="form-control" />
<ValidationMessage For="@(() => model.LastName)" />
</div>
</div>
<div class="form-row">
<div class="form-group col">
<label>Username</label>
<InputText @bind-Value="model.Username" class="form-control" />
<ValidationMessage For="@(() => model.Username)" />
</div>
<div class="form-group col">
<label>
Password
<em>(Leave blank to keep the same password)</em>
</label>
<InputText @bind-Value="model.Password" type="password" class="form-control" />
<ValidationMessage For="@(() => model.Password)" />
</div>
</div>
<div class="form-group">
<button disabled="@loading" class="btn btn-primary">
@if (loading)
{
<span class="spinner-border spinner-border-sm mr-1"></span>
}
Save
</button>
<NavLink href="users" class="btn btn-link">Cancel</NavLink>
</div>
</EditForm>
}
else
{
<div class="text-center p-3">
<span class="spinner-border
spinner-border-lg align-center"></span>
</div>
}
@code {
private
EditUser model;
private
bool loading;
[Parameter]
public
string Id { get; set; }
protected
override async Task OnInitializedAsync()
{
var
user = await AccountService.GetById(Id);
model = new EditUser(user);
}
private
async void OnValidSubmit()
{
loading = true;
try
{
await
AccountService.Update(Id, model);
AlertService.Success("Update successful",
keepAfterRouteChange: true);
NavigationManager.NavigateTo("users");
}
catch (Exception ex)
{
AlertService.Error(ex.Message);
loading = false;
StateHasChanged();
}
}
}
Users Home Page Component
Path: /Pages/Users/Index.razor
The users home page component displays a list
of all users and contains buttons for adding, editing and deleting users.
The OnInitializedAsync() Blazor lifecycle method is used to fetch
all user accounts from the account service and make them available to the
template via the users property.
The DeleteUser() method first sets the property user.IsDeleting
= true so the template
displays a spinner on the delete button, it then calls AccountService.Delete() to delete the user and removes the
deleted user from the users list so it is
removed from the UI.
@page "/users"
@attribute [Authorize]
@inject IAccountService AccountService
<h1>Users</h1>
<NavLink href="users/add" class="btn btn-sm btn-success mb-2">Add User</NavLink>
<table class="table table-striped">
<thead>
<tr>
<th style="width: 30%">First Name</th>
<th style="width: 30%">Last Name</th>
<th style="width: 30%">Username</th>
<th style="width: 10%"></th>
</tr>
</thead>
<tbody>
@if
(users != null)
{
foreach
(var user in users)
{
<tr>
<td>@user.FirstName</td>
<td>@user.LastName</td>
<td>@user.Username</td>
<td style="white-space: nowrap">
<NavLink
href="@($"users/edit/{user.Id}")" class="btn btn-sm
btn-primary mr-1">Edit</NavLink>
<button @onclick="@(() =>
DeleteUser(user.Id))" disabled="@user.IsDeleting" class="btn btn-sm btn-danger btn-delete-user">
@if
(user.IsDeleting)
{
<span class="spinner-border
spinner-border-sm"></span>
}
else
{
<span>Delete</span>
}
</button>
</td>
</tr>
}
}
@if
(loading)
{
<tr>
<td colspan="4" class="text-center">
<span class="spinner-border spinner-border-lg align-center"></span>
</td>
</tr>
}
</tbody>
</table>
@code {
private
bool loading;
private
IList<User> users;
protected override async Task OnInitializedAsync()
{
loading = true;
users = await AccountService.GetAll();
loading = false;
}
private
async void DeleteUser(string id)
{
var
user = users.First(x => x.Id == id);
user.IsDeleting = true;
await AccountService.Delete(id);
users.Remove(user);
StateHasChanged();
}
}
Users Layout Component
Path: /Pages/Users/Layout.razor
The users layout component is a nested layout
for all pages in the /Pages/Users folder, it
simply wraps the @Body in a couple
of div tags with some bootstrap classes to set
the width, padding and alignment of all of the users pages. It is set as the
layout for all users pages in the users imports file.
@inherits LayoutComponentBase
@layout MainLayout
<div class="p-4">
<div class="container">
@Body
</div>
</div>
Home Page Component
Path: /Pages/Index.razor
The home page component displays a simple
welcome message with the current user's first name and a link to the users
section.
The [Authorize] attribute restricts this page to authenticated users.
@page "/"
@attribute [Authorize]
@inject IAccountService AccountService
<div class="p-4">
<div class="container">
<h1>Hi
@AccountService.User.FirstName!</h1>
<p>You're logged in with
Blazor WebAssembly!!</p>
<p><NavLink href="users">Manage Users</NavLink></p>
</div>
</div>
Launch Settings
Path: /Properties/launchSettings.json
The launch settings file contains settings
that are used when you run the example Blazor application on your local
development machine.
The "BlazorApp" profile is used when you run the Blazor
app using the .NET Core CLI (dotnet run), and the "IIS Express" profile is used when you run the Blazor
app from Visual Studio.
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:25181",
"sslPort": 44330
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"BlazorApp": {
"commandName": "Project",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Account Service
Path: /Services/AccountService.cs
The account service handles communication
between the Blazor app and the backend api for everything related to user
accounts. It contains methods for the login, logout and registration, as well
as and standard CRUD methods for retrieving and modifying user data.
On successful login the User property is set to the value returned
from the api, and the user object is stored in browser local storage to keep
the user logged in between page refreshes and browser sessions. If you prefer
not to use local storage you can simply remove references to the local storage
service from the account service and the application will continue to work
correctly, except for the "stay logged in" feature.
The User property provides access to the currently logged in user
to any other component, for example the main layout component uses it to
show/hide the main navigation bar and set a css class on the app container div.
The Initialize() method is called from Program.cs on startup to assign the "user" object from local storage to the User property, which enables the user to stay
logged in between page refreshes and browser sessions. This couldn't be put
into the constructor because getting data from local storage is an async
action.
using BlazorApp.Models;
using BlazorApp.Models.Account;
using Microsoft.AspNetCore.Components;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BlazorApp.Services
{
public interface IAccountService
{
User User { get; }
Task Initialize();
Task Login(Login model);
Task Logout();
Task Register(AddUser model);
Task<IList<User>> GetAll();
Task<User> GetById(string id);
Task Update(string id, EditUser model);
Task Delete(string id);
}
public class AccountService : IAccountService
{
private IHttpService _httpService;
private NavigationManager _navigationManager;
private ILocalStorageService _localStorageService;
private string _userKey = "user";
public User User { get; private set; }
public AccountService(
IHttpService httpService,
NavigationManager navigationManager,
ILocalStorageService localStorageService
) {
_httpService = httpService;
_navigationManager = navigationManager;
_localStorageService = localStorageService;
}
public async Task Initialize()
{
User = await _localStorageService.GetItem<User>(_userKey);
}
public async Task Login(Login model)
{
User = await _httpService.Post<User>("/users/authenticate", model);
await _localStorageService.SetItem(_userKey, User);
}
public async Task Logout()
{
User = null;
await _localStorageService.RemoveItem(_userKey);
_navigationManager.NavigateTo("account/login");
}
public async Task Register(AddUser model)
{
await _httpService.Post("/users/register", model);
}
public async Task<IList<User>> GetAll()
{
return await
_httpService.Get<IList<User>>("/users");
}
public async Task<User> GetById(string id)
{
return await _httpService.Get<User>($"/users/{id}");
}
public async Task Update(string id, EditUser model)
{
await _httpService.Put($"/users/{id}", model);
// update stored user if
the logged in user updated their own record
if (id == User.Id)
{
// update
local storage
User.FirstName = model.FirstName;
User.LastName = model.LastName;
User.Username = model.Username;
await _localStorageService.SetItem(_userKey, User);
}
}
public async Task Delete(string id)
{
await _httpService.Delete($"/users/{id}");
// auto logout if the
logged in user deleted their own record
if (id == User.Id)
await Logout();
}
}
}
Alert Service
Path: /Services/AlertService.cs
The alert service acts as the bridge between
any component or class in the Blazor application and the alert component that actually displays
the alert messages. It contains methods for sending, clearing and subscribing
to alert messages.
The service uses C# events and delegates to
enable communication with other components, for more information on how this
works see the tutorial Blazor WebAssembly - Communication Between Components.
using BlazorApp.Models;
using System;
namespace BlazorApp.Services
{
public interface IAlertService
{
event Action<Alert> OnAlert;
void Success(string message, bool keepAfterRouteChange = false, bool autoClose = true);
void Error(string message, bool keepAfterRouteChange = false, bool autoClose = true);
void Info(string message, bool keepAfterRouteChange = false, bool autoClose = true);
void Warn(string message, bool keepAfterRouteChange = false, bool autoClose = true);
void Alert(Alert alert);
void Clear(string id = null);
}
public class AlertService : IAlertService
{
private const string _defaultId = "default-alert";
public event Action<Alert> OnAlert;
public void Success(string message, bool keepAfterRouteChange = false, bool autoClose = true)
{
this.Alert(new Alert
{
Type = AlertType.Success,
Message = message,
KeepAfterRouteChange = keepAfterRouteChange,
AutoClose = autoClose
});
}
public void Error(string message, bool keepAfterRouteChange = false, bool autoClose = true)
{
this.Alert(new Alert
{
Type = AlertType.Error,
Message = message,
KeepAfterRouteChange = keepAfterRouteChange,
AutoClose = autoClose
});
}
public void Info(string message, bool keepAfterRouteChange = false, bool autoClose = true)
{
this.Alert(new Alert
{
Type = AlertType.Info,
Message = message,
KeepAfterRouteChange = keepAfterRouteChange,
AutoClose = autoClose
});
}
public void Warn(string message, bool keepAfterRouteChange = false, bool autoClose = true)
{
this.Alert(new Alert
{
Type = AlertType.Warning,
Message = message,
KeepAfterRouteChange = keepAfterRouteChange,
AutoClose = autoClose
});
}
public void Alert(Alert alert)
{
alert.Id = alert.Id ?? _defaultId;
this.OnAlert?.Invoke(alert);
}
public void Clear(string id = _defaultId)
{
this.OnAlert?.Invoke(new Alert { Id = id });
}
}
}
HTTP Service
Path: /Services/HttpService.cs
The HTTP service is a lightweight wrapper for
the .NET Core HttpClient to simplify the
code for making HTTP requests from other services, and to implement the
following:
- add JWT token to HTTP
Authorization header for API requests when the user is logged in.
- automatically logout of the
Blazor app when a 401 Unauthorized response is received from the API.
- on error response throw an
exception with the message from
the response body.
The HTTP service is used by the account service.
using BlazorApp.Helpers;
using BlazorApp.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace BlazorApp.Services
{
public interface IHttpService
{
Task<T> Get<T>(string uri);
Task Post(string uri, object value);
Task<T> Post<T>(string uri, object value);
Task Put(string uri, object value);
Task<T> Put<T>(string uri, object value);
Task Delete(string uri);
Task<T> Delete<T>(string uri);
}
public class HttpService : IHttpService
{
private HttpClient _httpClient;
private NavigationManager _navigationManager;
private ILocalStorageService _localStorageService;
private IConfiguration _configuration;
public HttpService(
HttpClient httpClient,
NavigationManager navigationManager,
ILocalStorageService localStorageService,
IConfiguration configuration
) {
_httpClient = httpClient;
_navigationManager = navigationManager;
_localStorageService = localStorageService;
_configuration
= configuration;
}
public async Task<T> Get<T>(string uri)
{
var request = new HttpRequestMessage(HttpMethod.Get, uri);
return await sendRequest<T>(request);
}
public async Task Post(string uri, object value)
{
var request = createRequest(HttpMethod.Post, uri, value);
await sendRequest(request);
}
public async Task<T> Post<T>(string uri, object value)
{
var request = createRequest(HttpMethod.Post, uri, value);
return await sendRequest<T>(request);
}
public async Task Put(string uri, object value)
{
var request = createRequest(HttpMethod.Put, uri, value);
await sendRequest(request);
}
public async Task<T> Put<T>(string uri, object value)
{
var request = createRequest(HttpMethod.Put, uri, value);
return await sendRequest<T>(request);
}
public async Task Delete(string uri)
{
var request = createRequest(HttpMethod.Delete, uri);
await sendRequest(request);
}
public async Task<T> Delete<T>(string uri)
{
var request = createRequest(HttpMethod.Delete, uri);
return await sendRequest<T>(request);
}
// helper methods
private HttpRequestMessage createRequest(HttpMethod method, string uri, object value = null)
{
var request = new HttpRequestMessage(method, uri);
if (value != null)
request.Content = new StringContent(JsonSerializer.Serialize(value), Encoding.UTF8, "application/json");
return request;
}
private async Task sendRequest(HttpRequestMessage request)
{
await addJwtHeader(request);
// send request
using var response = await _httpClient.SendAsync(request);
// auto logout on 401
response
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
_navigationManager.NavigateTo("account/logout");
return;
}
await handleErrors(response);
}
private async Task<T> sendRequest<T>(HttpRequestMessage request)
{
await addJwtHeader(request);
// send request
using var response = await _httpClient.SendAsync(request);
// auto logout on 401 response
if (response.StatusCode ==
HttpStatusCode.Unauthorized)
{
_navigationManager.NavigateTo("account/logout");
return default;
}
await handleErrors(response);
var options = new JsonSerializerOptions();
options.PropertyNameCaseInsensitive = true;
options.Converters.Add(new StringConverter());
return await response.Content.ReadFromJsonAsync<T>(options);
}
private async Task addJwtHeader(HttpRequestMessage request)
{
// add jwt auth header if
user is logged in and request is to the api url
var user = await _localStorageService.GetItem<User>("user");
var isApiUrl =
!request.RequestUri.IsAbsoluteUri;
if (user != null && isApiUrl)
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", user.Token);
}
private async Task handleErrors(HttpResponseMessage response)
{
// throw exception on error
response
if
(!response.IsSuccessStatusCode)
{
var error = await
response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
throw new Exception(error["message"]);
}
}
}
}
Local Storage Service
Path: /Services/LocalStorageService.cs
The local storage service is a lightweight
wrapper for the .NET Core IJSRuntime service to simplify getting, setting and removing items
from browser local storage. It is used by the account service for getting and setting
the currently logged in user.
using Microsoft.JSInterop;
using System.Text.Json;
using System.Threading.Tasks;
namespace BlazorApp.Services
{
public interface ILocalStorageService
{
Task<T> GetItem<T>(string key);
Task
SetItem<T>(string key, T value);
Task RemoveItem(string key);
}
public class LocalStorageService : ILocalStorageService
{
private IJSRuntime _jsRuntime;
public LocalStorageService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task<T> GetItem<T>(string key)
{
var json = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", key);
if (json == null)
return default;
return JsonSerializer.Deserialize<T>(json);
}
public async Task SetItem<T>(string key, T value)
{
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, JsonSerializer.Serialize(value));
}
public async Task RemoveItem(string key)
{
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", key);
}
}
}
Alert Component
Path: /Shared/Alert.razor
The alert component is responsible for
rendering alert messages sent to the alert service. It accepts an optional Id parameter that defaults to "default-alert" that specifies which alerts are rendered
by the component, e.g. an alert component with id "default-alert" will render alert (Models.Alert) objects from the alert service that have the
same id ("default-alert").
The OnInitialized() method subscribes to the AlertService.OnAlert event, this enables the alert component
to be notified whenever an alert message is sent to the alert service and add
it to the alerts list for
display. Alerts are cleared when an alert with a null message is received from
the alert service. The OnInitialized() method also subscribes to the NavigationManager.LocationChanged event so it can automatically clear
alerts on route changes.
The Dispose() method unsubscribes from the alert service and navigation
manager when the component is destroyed to prevent memory leaks from orphaned
subscriptions. The alert component implements the IDisposable interface to add support for the Dispose() method.
The RemoveAlert() method removes the specified alert object from the alerts list which removes it from the UI.
The CssClass() method returns bootstrap classes for each alert type to
style the alert, if you're using something other than bootstrap you can change
the CSS classes returned to suit your application.
@implements IDisposable
@inject IAlertService AlertService
@inject NavigationManager NavigationManager
@foreach (var alert in alerts)
{
<div class="@CssClass(alert)">
<a class="close" @onclick="@(() =>
RemoveAlert(alert))">×</a>
<span>@alert.Message</span>
</div>
}
@code {
[Parameter]
public
string Id { get; set; } = "default-alert";
[Parameter]
public
bool Fade { get; set; } = true;
private
List<Models.Alert> alerts = new List<Models.Alert>();
protected override void OnInitialized()
{
//
subscribe to new alerts and location change events
AlertService.OnAlert += OnAlert;
NavigationManager.LocationChanged += OnLocationChange;
}
public
void Dispose()
{
//
unsubscribe from alerts and location change events
AlertService.OnAlert -= OnAlert;
NavigationManager.LocationChanged -= OnLocationChange;
}
private
async void OnAlert(Models.Alert alert)
{
//
ignore alerts sent to other alert components
if
(alert.Id != Id)
return;
//
clear alerts when an empty alert is received
if
(alert.Message == null)
{
// remove alerts without the 'KeepAfterRouteChange' flag set to true
alerts.RemoveAll(x =>
!x.KeepAfterRouteChange);
// set the 'KeepAfterRouteChange' flag to false for the
// remaining alerts so they are removed on the next clear
alerts.ForEach(x => x.KeepAfterRouteChange = false);
}
else
{
// add alert to array
alerts.Add(alert);
StateHasChanged();
// auto close alert if required
if (alert.AutoClose)
{
await Task.Delay(3000);
RemoveAlert(alert);
}
}
StateHasChanged();
}
private
void OnLocationChange(object sender, LocationChangedEventArgs e)
{
AlertService.Clear(Id);
}
private
async void RemoveAlert(Models.Alert alert)
{
//
check if already removed to prevent error on auto close
if
(!alerts.Contains(alert)) return;
if
(Fade)
{
// fade out alert
alert.Fade
= true;
// remove alert after faded out
await Task.Delay(250);
alerts.Remove(alert);
}
else
{
// remove alert
alerts.Remove(alert);
}
StateHasChanged();
}
private
string CssClass(Models.Alert alert)
{
if
(alert == null) return null;
var
classes = new List<string> { "alert", "alert-dismissable",
"mt-4", "container" };
var
alertTypeClass = new Dictionary<AlertType, string>();
alertTypeClass[AlertType.Success] = "alert-success";
alertTypeClass[AlertType.Error] = "alert-danger";
alertTypeClass[AlertType.Info] = "alert-info";
alertTypeClass[AlertType.Warning] = "alert-warning";
classes.Add(alertTypeClass[alert.Type]);
if
(alert.Fade)
classes.Add("fade");
return string.Join(' ', classes);
}
}
Main Layout Component
Path: /Shared/MainLayout.razor
The main layout component is the default
layout for the Blazor application, it contains the main nav bar for the app
which is only displayed when the user is logged in, the global <Alert
/> component, and
the @Body keyword to specify the location to
render each page component based on the current route / path. The main layout
is set as the default layout in the app component.
@inherits LayoutComponentBase
@inject IAccountService AccountService
@inject NavigationManager NavigationManager
@if (LoggedIn)
{
<!-- nav -->
<nav class="navbar navbar-expand
navbar-dark bg-dark">
<div class="navbar-nav">
<NavLink href="" Match="NavLinkMatch.All" class="nav-item nav-link">Home</NavLink>
<NavLink href="users" class="nav-item nav-link">Users</NavLink>
<NavLink href="account/logout" class="nav-item nav-link">Logout</NavLink>
</div>
</nav>
}
<div class="app-container @(LoggedIn ? "bg-light" : "")">
<Alert />
@Body
</div>
@code {
public
bool LoggedIn
{
get
{ return AccountService.User != null; }
}
}
App CSS
Path: /wwwroot/css/app.css
The app css contains custom styles for the
Blazor application.
The blazor-error-ui styles were part of the initial generated project and are
for styling uncaught exceptions in a yellow bar at the bottom of the window.
The validation classes were copied from
Bootstrap 4.5.0 and renamed to their equivalent Blazor classes to make
validation messages appear correctly with Bootstrap styling on the forms in the
tutorial example.
a { cursor: pointer }
.app-container {
min-height: 320px;
overflow: hidden;
}
.btn-delete-user {
width: 40px;
text-align: center;
box-sizing: content-box;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem
1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
/*
below
styles were copied from bootstrap 4.5.0 and renamed to be compatible with
blazor
-
'.validation-message' == bootstrap '.invalid-feedback'
-
'.invalid' == bootstrap '.is-invalid'
*/
.validation-message {
display: none;
width: 100%;
margin-top: 0.25rem;
font-size: 80%;
color: #dc3545;
}
.invalid ~ .validation-message {
display: block;
}
.form-control.invalid {
border-color: #dc3545;
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg
xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none'
stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6'
r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle
cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.form-control.invalid:focus {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
textarea.form-control.invalid {
padding-right: calc(1.5em + 0.75rem);
background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);
}
.form-check-input.invalid ~ .form-check-label {
color: #dc3545;
}
.form-check-input.invalid ~ .validation-message {
display: block;
}
App Settings
Path: /wwwroot/appsettings.json
The app settings file contains config
variables used by the Blazor application.
To disable the fake backend and send HTTP
requests through to the "apiUrl" change the "fakeBackend" setting to "false".
{
"apiUrl": "http://localhost:4000",
"fakeBackend": "true"
}
Root index.html file (host page)
Path: /wwwroot/index.html
The Blazor host page is the initial file
loaded by the browser that kicks everything off, it loads the blazor.webassembly.js script that downloads and initializes
the .NET runtime and our compiled Blazor application.
<!DOCTYPE html>
<html>
<head>
<base href="/" />
<title>ASP.NET Core Blazor
WebAssembly - User Registration and Login Example</title>
<meta name="viewport" content="width=device-width,
initial-scale=1">
<!-- bootstrap css
-->
<link href="//netdna.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />
<!-- custom css -->
<link href="css/app.css" rel="stylesheet" />
</head>
<body>
<app>Loading...</app>
<div id="blazor-error-ui">
An
unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">X</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>
Razor Imports File
Path: /_Imports.razor
Razor imports files include directives that
are automatically applied to all other razor components in the same folder and
subfolders. An _Imports.razor file can be
added to any folder and can include any razor directive.
By adding the below @using statements to this imports file in the
root folder, all blazor components in the application have access to the
namespaces without needing to add any @using statements themselves.
@using BlazorApp
@using BlazorApp.Helpers
@using BlazorApp.Models
@using BlazorApp.Models.Account
@using BlazorApp.Services
@using BlazorApp.Shared
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using System.Web
App Component
Path: /App.razor
The app component is the root component in the
Blazor tutorial application and contains a Router component with Found and NotFound templates.
If the route is found (i.e. bound to a page
component with the @page directive) then
the route data is passed to the AppRouteView to render the page.
If the route is not found the user is
redirected to the home page.
@inject NavigationManager NavigationManager
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AppRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
@{
// redirect to home page if not found
NavigationManager.NavigateTo("");
}
</NotFound>
</Router>
Blazor App csproj
Path: /BlazorApp.csproj
The csproj (C# project) is an MSBuild based
file that contains target framework and NuGet package dependency information
for the Blazor application.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="3.2.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="3.2.1" PrivateAssets="all" />
<PackageReference Include="System.Net.Http.Json" Version="3.2.1" />
</ItemGroup>
</Project>
Blazor App Program
Path: /Program.cs
The program class the main entry point to
start the application, it sets the root component, configures dependency
injection, performs service initialization and starts the Blazor app.
using BlazorApp.Helpers;
using BlazorApp.Services;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace BlazorApp
{
public class Program
{
public static async Task Main(string[] args)
{
var builder =
WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services
.AddScoped<IAccountService, AccountService>()
.AddScoped<IAlertService, AlertService>()
.AddScoped<IHttpService, HttpService>()
.AddScoped<ILocalStorageService, LocalStorageService>();
// configure http client
builder.Services.AddScoped(x => {
var apiUrl = new Uri(builder.Configuration["apiUrl"]);
// use
fake backend if "fakeBackend" is "true" in appsettings.json
if (builder.Configuration["fakeBackend"] == "true")
{
var fakeBackendHandler = new FakeBackendHandler(x.GetService<ILocalStorageService>());
return new HttpClient(fakeBackendHandler) {
BaseAddress = apiUrl };
}
return new HttpClient() { BaseAddress = apiUrl };
});
var host = builder.Build();
var accountService =
host.Services.GetRequiredService<IAccountService>();
await accountService.Initialize();
await host.RunAsync();
}
}
}
No comments:
Post a Comment