Combining Blazor WebAssembly and .Net MVC application - part 2
An
Posted on August 14, 2023
In this article we will continue our journey of combining Blazor WebAssembly and .NET MVC application in a single project. This is part 2 of a series of articles that explains how to use Blazor components in MVC views and vice versa. If you haven’t read part 1 yet, I recommend you to do so before continuing.
In part 1, we have seen how to create a new Blazor WebAssembly project and add it as a reference to our existing MVC project. We have also seen how to configure our MVC project to serve the Blazor files.
In this part, we will use Blazor WebAssembly componet in .NET MVC application to create a simple CRUD data application. Continue use the project in part 1, and the existed FetchData component in Blazor WebAssembly project.
Add the component to MVC application to show the list
First, create the Models
folder in Blazor WebAssemply project then move the WeatherForecast
class in FetchData component to the folder. Add the Id
field in WeatherForecast
class:
namespace BlazorApp.Models;
public class WeatherForecast
{
public string? Id { get; set; }
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Then, in the HomeController.cs
in MVC project, add some code to simulation data service:
public class HomeController : Controller
{
//simulation data service
private static readonly List<WeatherForecast> weatherForecasts = new()
{
new WeatherForecast { Id = Guid.NewGuid().ToString(), Date = DateOnly.Parse("2022-01-06"), TemperatureC = 1, Summary = "Freezing", },
new WeatherForecast { Id = Guid.NewGuid().ToString(), Date = DateOnly.Parse("2022-01-07"), TemperatureC = 14, Summary = "Bracing", },
new WeatherForecast { Id = Guid.NewGuid().ToString(), Date = DateOnly.Parse("2022-01-08"), TemperatureC = -13, Summary = "Freezing", },
new WeatherForecast { Id = Guid.NewGuid().ToString(), Date = DateOnly.Parse("2022-01-09"), TemperatureC = -16, Summary = "Balmy", },
new WeatherForecast { Id = Guid.NewGuid().ToString(), Date = DateOnly.Parse("2022-01-10"), TemperatureC = -2, Summary = "Chilly", },
};
//the api to return data
public IActionResult WeatherForecasts()
{
return Json(weatherForecasts);
}
In the view Index.cshtml
, replace code to like this:
<component type="typeof(BlazorApp.Pages.FetchData)" render-mode="WebAssemblyPrerendered"></component>
@section Scripts{
<script src="_framework/blazor.webassembly.js"></script>
}
Add the HttpClient to the .NET MVC application by this code in Program.cs
file:
//add httpclient
builder.Services.AddScoped(sp => new HttpClient());
This is because the FetchData component use HttpClient
@inject HttpClient Http
so we need to add the HttpClient to the .NET MVC project, because when the .NET MVC application pre-render FetchData component, it must create a HttpClient instance and inject to component.
Last, in the FetchData component in Blazor WebAssembly, replace the OnInitializedAsync
method with this:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("Home/WeatherForecasts");
//force view update
StateHasChanged();
}
}
Run the .NET MVC project and see the result:
Add the Create function
In .NET MVC project, add create action in HomeController
as a create api:
[HttpPost]
public IActionResult WeatherForecasts([FromBody] WeatherForecast model)
{
if (ModelState.IsValid)
{
model.Id = Guid.NewGuid().ToString();
weatherForecasts.Add(model);
}
return Json(weatherForecasts);
}
Change the FetchData component like this to add the Create method on view:
@using BlazorApp.Models;
@inject HttpClient Http
<PageTitle>Weather forecast</PageTitle>
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<EditForm Model="@createModel" OnValidSubmit="OnCreateFormSubmit" id="form-create">
<DataAnnotationsValidator />
<ValidationSummary />
</EditForm>
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<InputDate class="form-control" @bind-Value="@createModel.Date" form="form-create"></InputDate>
</td>
<td>
<InputNumber class="form-control" @bind-Value="@createModel.TemperatureC" form="form-create"></InputNumber>
</td>
<td>
@createModel.TemperatureF
</td>
<td>
<InputText class="form-control" @bind-Value="@createModel.Summary" form="form-create"></InputText>
</td>
<td>
<button type="submit" class="btn btn-primary" form="form-create">Add</button>
</td>
</tr>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
<td></td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("Home/WeatherForecasts");
//force view update
StateHasChanged();
}
}
WeatherForecast createModel = new();
async Task OnCreateFormSubmit()
{
using var createReq = await Http.PostAsJsonAsync("Home/WeatherForecasts", createModel);
createReq.EnsureSuccessStatusCode();
forecasts = await createReq.Content.ReadFromJsonAsync<WeatherForecast[]>();
}
}
Save file then run the .NET MVC applicaton:
The Edit function
Add the Edit function is same as the Create function, first, add the api action in HomeController
:
[HttpPut]
public IActionResult WeatherForecasts(string id, [FromBody] WeatherForecast model)
{
if (ModelState.IsValid)
{
var weatherForecast = weatherForecasts.FirstOrDefault(x => x.Id == id);
if (weatherForecast != null)
{
weatherForecast.Date = model.Date;
weatherForecast.TemperatureC = model.TemperatureC;
weatherForecast.Summary = model.Summary;
}
}
return Json(weatherForecasts);
}
In the WeatherForecast
class, add the IsEditting
field to mark the editting entity:
public bool IsEditting { get; set; }
Then edit the FetchData component like this:
@using BlazorApp.Models;
@inject HttpClient Http
<PageTitle>Weather forecast</PageTitle>
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<EditForm Model="@createModel" OnValidSubmit="OnCreateFormSubmit" id="form-create">
<DataAnnotationsValidator />
<ValidationSummary />
</EditForm>
<EditForm Model="@editModel" OnValidSubmit="OnEditFormSubmit" id="form-edit">
<DataAnnotationsValidator />
<ValidationSummary />
</EditForm>
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<InputDate class="form-control" @bind-Value="@createModel.Date" form="form-create"></InputDate>
</td>
<td>
<InputNumber class="form-control" @bind-Value="@createModel.TemperatureC" form="form-create"></InputNumber>
</td>
<td>
@createModel.TemperatureF
</td>
<td>
<InputText class="form-control" @bind-Value="@createModel.Summary" form="form-create"></InputText>
</td>
<td>
<button type="submit" class="btn btn-primary" form="form-create">Add</button>
</td>
</tr>
@foreach (var forecast in forecasts)
{
if (forecast.IsEditting)
{
<tr>
<td>
<InputDate class="form-control" @bind-Value="@editModel.Date" form="form-edit"></InputDate>
</td>
<td>
<InputNumber class="form-control" @bind-Value="@editModel.TemperatureC" form="form-edit"></InputNumber>
</td>
<td>
@editModel.TemperatureF
</td>
<td>
<InputText class="form-control" @bind-Value="@editModel.Summary" form="form-edit"></InputText>
</td>
<td>
<button type="submit" class="btn btn-outline-primary" form="form-edit">Save</button>
<button type="button" class="btn btn-outline-secondary" @onclick="() => { forecast.IsEditting = false; }">Cancel</button>
</td>
</tr>
}
else
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
@if (forecasts.All(s => !s.IsEditting))
{
<td>
<button type="button" class="btn btn-outline-primary" @onclick="() => { (editModel = forecast).IsEditting = true; }">Edit</button>
</td>
}
else
{
<td></td>
}
</tr>
}
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("Home/WeatherForecasts");
//force view update
StateHasChanged();
}
}
WeatherForecast createModel = new();
async Task OnCreateFormSubmit()
{
using var createReq = await Http.PostAsJsonAsync("Home/WeatherForecasts", createModel);
createReq.EnsureSuccessStatusCode();
forecasts = await createReq.Content.ReadFromJsonAsync<WeatherForecast[]>();
}
WeatherForecast editModel = new();
async Task OnEditFormSubmit()
{
using var createReq = await Http.PutAsJsonAsync($"Home/WeatherForecasts/{editModel.Id}", editModel);
createReq.EnsureSuccessStatusCode();
forecasts = await createReq.Content.ReadFromJsonAsync<WeatherForecast[]>();
}
}
Now, run the .NET MVC application:
The Delete function
In the HomeController
, add this method:
[HttpDelete]
public IActionResult WeatherForecasts(string id)
{
var weatherForecast = weatherForecasts.FirstOrDefault(x => x.Id == id);
if (weatherForecast != null)
{
weatherForecasts.Remove(weatherForecast);
}
return Json(weatherForecasts);
}
In FetchData component, add delete method:
async Task OnDeleteSubmit(string id)
{
using var createReq = await Http.DeleteAsync($"Home/WeatherForecasts/{id}");
createReq.EnsureSuccessStatusCode();
forecasts = await createReq.Content.ReadFromJsonAsync<WeatherForecast[]>();
}
Then add Delete button after Edit button:
<td>
<button type="button" class="btn btn-outline-primary" @onclick="() => { (editModel = forecast).IsEditting = true; }">Edit</button>
<button type="button" class="btn btn-outline-danger" @onclick="async () => await OnDeleteSubmit(forecast.Id!)">Delete</button>
</td>
Run the application and done:
Use component param to serve data before render component
Before render Blazor WebAssembly component, we can pass param to it to serve data, it make component has data already before the component rendered.
In the HomeController
, change Index method like this:
public IActionResult Index()
{
return View(weatherForecasts.ToArray());
}
And this code in FetchData component and comment OnAfterRenderAsync
method:
private WeatherForecast[]? forecasts;
[Parameter] public WeatherForecast[]? initForecasts { get; set; }
protected override void OnInitialized()
{
forecasts = initForecasts;
}
//protected override async Task OnAfterRenderAsync(bool firstRender)
//{
// if (firstRender)
// {
// forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("Home/WeatherForecasts");
// //force view update
// StateHasChanged();
// }
//}
Now, pass data to component by component param:
@using BlazorApp.Models;
@model WeatherForecast[]
<component type="typeof(BlazorApp.Pages.FetchData)" render-mode="WebAssemblyPrerendered" param-initForecasts=@Model></component>
Run the application and we will see the data is loaded without the component call api get data.
Add Antiforgery to more secure
We can use the Antiforgery in Asp.Net to application more secure.
First, add this code in Program.cs
of .NET MVC to add Antiforgery and config it option:
//add antiforgery use request header
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
});
Add [AutoValidateAntiforgeryToken]
attribute in which want method to apply Antiforgery:
[HttpPost]
[AutoValidateAntiforgeryToken]
public IActionResult WeatherForecasts([FromBody] WeatherForecast model)
{
if (ModelState.IsValid)
{
model.Id = Guid.NewGuid().ToString();
weatherForecasts.Add(model);
}
return Json(weatherForecasts);
}
Declare antiRequestToken
in FetchData component:
[Parameter] public required string antiRequestToken { get; set; }
Use antiRequestToken
in request:
async Task OnCreateFormSubmit()
{
//remove if existed and add anti token in request header
Http.DefaultRequestHeaders.Remove("X-CSRF-TOKEN");
Http.DefaultRequestHeaders.TryAddWithoutValidation("X-CSRF-TOKEN", antiRequestToken);
using var createReq = await Http.PostAsJsonAsync("Home/WeatherForecasts", createModel);
createReq.EnsureSuccessStatusCode();
forecasts = await createReq.Content.ReadFromJsonAsync<WeatherForecast[]>();
}
Pass anti token from view to component:
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery antiforgery
@{
ViewData["Title"] = "Home Page";
string requestToken = antiforgery.GetAndStoreTokens(Context).RequestToken!;
}
<component type="typeof(BlazorApp.Pages.FetchData)" render-mode="WebAssemblyPrerendered" param-initForecasts=@Model param-antiRequestToken="@requestToken"></component>
Done, run the application and see the Antiforgery working now.
Conclusion
In conclusion, this blog post has shown how to combine Blazor WebAssembly and .NET MVC application in a single project. This approach allows us to use the benefits of both technologies, such as the performance and interactivity of Blazor and the flexibility and compatibility of MVC. We have seen how to create a new Blazor WebAssembly project and reference it in the existing MVC project, how to configure the routing and hosting for both frameworks, and how to use Blazor components in MVC views and vice versa. We have also learned how to use some of the services provided by Blazor, such as the IJSRuntime or the state management.
I hope you have found this blog post useful and interesting please share it with your friends. Thank you for reading.
This post use the Visual Studio 2022 and .NET 7.0, use other version may not working. You can find full post and source code of this sample in here.
Posted on August 14, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.