Combining Blazor WebAssembly and .Net MVC application - part 2

anpx00

An

Posted on August 14, 2023

Combining Blazor WebAssembly and .Net MVC application - part 2

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

Add the HttpClient to the .NET MVC application by this code in Program.cs file:

//add httpclient
builder.Services.AddScoped(sp => new HttpClient());
Enter fullscreen mode Exit fullscreen mode

This is because the FetchData component use HttpClient

@inject HttpClient Http
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
Enter fullscreen mode Exit fullscreen mode

Run the .NET MVC project and see the result:

Blazor WebAssemply and .NET MVC

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);
    }
Enter fullscreen mode Exit fullscreen mode

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[]>();
    }
}
Enter fullscreen mode Exit fullscreen mode

Save file then run the .NET MVC applicaton:

Blazor WebAssembly Create function

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);
    }
Enter fullscreen mode Exit fullscreen mode

In the WeatherForecast class, add the IsEditting field to mark the editting entity:

public bool IsEditting { get; set; }
Enter fullscreen mode Exit fullscreen mode

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[]>();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, run the .NET MVC application:

Blazor WebAssemply edit funtion

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);
    }
Enter fullscreen mode Exit fullscreen mode

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[]>();
    }
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Run the application and done:

Blazor WebAssembly delete function

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());
    }
Enter fullscreen mode Exit fullscreen mode

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();
    //    }
    //}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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";
});
Enter fullscreen mode Exit fullscreen mode

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);
    }
Enter fullscreen mode Exit fullscreen mode

Declare antiRequestToken in FetchData component:

[Parameter] public required string antiRequestToken { get; set; }
Enter fullscreen mode Exit fullscreen mode

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[]>();
    }
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
anpx00
An

Posted on August 14, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related