【ASP.NET Core】【Blazor Server】Try SPA
Tags: csharp, aspnetcore, blazor
Published: 2020-08-21 02:11:21

## Intro

* [【ASP.NET Core】Try Blazor(Blazor Server)](https://dev.to/masanori_msl/asp-net-core-try-blazor-blazor-server-4j34)

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
```csharp
...
        [Route("/")]
        [Route("/{page}")]
        public ActionResult OpenPage(string page)
        {
            return View("Views/_Host.cshtml");            
        }
...
```

_Host.cshtml just calls a Blazor class for routing.

#### _Host.cshtml
```csharp
@using BlazorSample.Views;
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
```

#### App.razor
```csharp
@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
```csharp
@inherits LayoutComponentBase
@Body
```

#### SearchPage.razor
```csharp
@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".

* [ASP.NET Core Blazor routing | Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing?view=aspnetcore-3.1)
* [Create and use ASP.NET Core Razor components | Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/blazor/components/?view=aspnetcore-3.1)
* [Project structure for Blazor apps | Microsoft Docs](https://docs.microsoft.com/en-us/dotnet/architecture/blazor-for-web-forms-developers/project-structure)

### _Layout.cshtml
Now, the structure of SearchPage like below.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/nlbwzoldu564ra714lz5.png)

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

#### _Host.cshtml
```csharp
@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 &lt;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
```csharp
...
        [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
```csharp
@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.

```csharp
@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](https://dev-to-uploads.s3.amazonaws.com/i/i0m8j97d482q504rdv73.png)

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

#### SearchPage.razor
```csharp
...
@foreach (var item in Products)
{
    <SearchResultRow Product=item></SearchResultRow>
}
...
```

#### SearchResultRow.razor
```csharp
@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
```csharp
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
```csharp
@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.

* [Partial Classes and Methods - C# Programming Guide | Microsoft Docs](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods)
* [Learn About Partial Class In C#](https://www.c-sharpcorner.com/article/learn-about-partial-class-in-c-sharp/)