loading...

【ASP.NET Core】【Blazor Server】Try SPA

masanori_msl profile image Masui Masanori ・4 min read

Intro

This time I will try creating Single Page Application.
I want to know how to route Blazor pages, and if I can create hierarchical components, and etc..

Routing

First, I add "localhost:5000/" and "localhost:5000/{PageName}" as Blazor page routes.

HomeController.cs

...
        [Route("/")]
        [Route("/{page}")]
        public ActionResult OpenPage(string page)
        {
            return View("Views/_Host.cshtml");            
        }
...

_Host.cshtml just calls a Blazor class for routing.

_Host.cshtml

@using BlazorSample.Views;
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))

App.razor

@using BlazorSample.Views.Shared;
<Router AppAssembly="@typeof(Startup).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <p>Sorry, there's nothing at this address.</p>
    </NotFound>
</Router>

MainLayout.razor is common layout for Blazor.
It's as same as _Layout.cshtml for Razor.

MainLayout.razor

@inherits LayoutComponentBase
@Body

SearchPage.razor

@page "/SearchPage";

<input type="text" @bind="productName">
<button @onclick="UpdateValue">Update</button>
@code{
    public string productName = "";

    public async Task UpdateValue()
    {
        productName = "Hello World!";
    }
}

In Blazor, the page path is decided by "@page".
So I can access the SearchPage with "localhost:5000/SearchPage".

_Layout.cshtml

Now, the structure of SearchPage like below.
Alt Text

But if you don't use any other razor pages, you can remove "_ViewStart.cshtml" and merge "_Layout.cshtml" into "_Host.cshtml".

_Host.cshtml

@using BlazorSample.Views;

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>@ViewData["Title"]</title>
        <base href="/">
        <script type="text/javascript">
            if (/MSIE \d|Trident.*rv:/.test(navigator.userAgent)) {
                document.write('<script src="https://polyfill.io/v3/polyfill.min.js?features=Element.prototype.closest%2CIntersectionObserver%2Cdocument.querySelector%2Cfeatures=Array.prototype.forEach%2CNodeList.prototype.forEach"><\/script>');
                document.write('<script src="js/blazor.polyfill.min.js"><\/script>');
            }
        </script>
    </head>
    <body>        
        <div>Hello Host</div>
        @(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
        <script src="_framework/blazor.server.js"></script>
    </body>
</html>

I also can write common layout in "MainLayout.razor".
But because I can't write <script>, I have to write it in "_Layout.cshtml" or "_Host.csthml".

ViewData

Because Blazor can't access "ViewData" and "ViewBag", so I have to control them in Controller classes or Razor files.

HomeController.cs

...
        [Route("/")]
        [Route("/{page}")]
        public ActionResult OpenPage(string page)
        {
            ViewData["Title"] = GetTitle(page);
            return View("Views/_Host.cshtml");            
        }
        private string GetTitle(string page)
        {
            switch(page)
            {
                case "SearchPage":
                    return "Search";
                default:
                    return "Home";
            }
        }
...

Send data from Router to Component

Last time, I could send date from Razor to Blazor by "[Parameter]".
How about from Router?

It is as same as from Razor to Blazor.

App.razor

@using BlazorSample.Views.Shared;
<Router AppAssembly="@typeof(Startup).Assembly">
    <Found Context="routeData">
        @{
            var values = routeData.RouteValues as Dictionary<string, object> ?? new Dictionary<string, object>() ;
            values.Add("Name", "Hello World");

            var newRouteData = new RouteData(routeData.PageType, values);
        }
        <RouteView RouteData="@newRouteData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
...
</Router>

Similarly, I can set default page when the route isn't found.

@using BlazorSample.Views.Shared;
<Router AppAssembly="@typeof(Startup).Assembly">
...
    <NotFound>
        @{
            var routeData = new RouteData(typeof(SearchPage), new Dictionary<string, object>());
        }
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </NotFound>
</Router>

Create hierarchical components

In Angular, a component can have child components.
Alt Text

How about Blazor?
It's just as same as Angular.

SearchPage.razor

...
@foreach (var item in Products)
{
    <SearchResultRow Product=item></SearchResultRow>
}
...

SearchResultRow.razor

@using Models;
<div class="search_result_row">
    <div class="search_result_row_id">@Product.Id</div>
    <div class="search_result_row_name">@Product.Name</div>
</div>
@code{
    [Parameter]
    public Product Product {get; set; }
}

Separate C# codes from Blazor HTML

When I create small components, I can write both HTML and C# codes in one file.
But when it become more larger and complicated, I want to separate them.

I can move C# codes by partial class.

SearchPage.razor.cs

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Models;

namespace BlazorSample.Views
{
    public partial class SearchPage
    {
        [Inject]
        public Services.IBlazorService Blazor{ get; set; }
        [Parameter]
        public string Name {get; set;}
        public string productName = "";
        public List<Product> Products = new List<Product>
        {
            new Product
            {
                Id = 0,
                Name = "Hello",
            },
            new Product
            {
                Id = 1,
                Name = "World",
            },
        };
        public async Task UpdateValue()
        {
            var product = await Blazor.GetMessageAsync("Hello");
            Console.WriteLine(productName);
            Console.WriteLine(Name);
            productName = product.Name;
        }
    }
}

Now I can remove C# codes from "SearchPage.razor".

SearchPage.razor

@page "/SearchPage";

<input type="text" @bind="productName">
<button @onclick="UpdateValue">Update</button>
@foreach (var item in Products)
{
    <SearchResultRow Product=item></SearchResultRow>
}

One important thing is making those namespaces the same.
Or "SearchPage.razor" can't find and get compiling errors.

Posted on by:

masanori_msl profile

Masui Masanori

@masanori_msl

Programmer, husband, father I love C#, TypeScript, etc.

Discussion

pic
Editor guide