【ASP.NET Core】Try Blazor(Blazor Server)
Tags: csharp, aspnetcore, blazor
Published: 2020-08-15 09:35:19

## Intro

Though I have been interested in Blazor, I have never tried it.
But in last update(ASP.NET Core Preview 7), there were many update about Blazor.

* [ASP.NET Core Updates in .NET 5 Preview 7 | ASP.NET Blog](https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-5-preview-7/)

So I decided trying Blazor.

* [Blazor | Build client web apps with C# | .NET](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor)

Because if I actually use Blazor, I want to add it into existing projects, so I create an MVC project first.

Blazor has two types( Blazor Server and Blazor WebAssembly).

* [ASP.NET Core Blazor hosting models | Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-3.1)

Because I don't try PWA and I feel Blazor Server looks like more easy to understand, so I choose Blazor Server this time.

### Environments
* ASP.NET Core ver.5.0.100-preview.7.20366.6

### Base project
I create a empty project.
And I add Controller and Razor files.

#### Startup.cs
```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace BlazorSample
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
        }
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseRouting();
            app.UseStaticFiles();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}
```
#### HomeController.cs
```csharp
using Microsoft.AspNetCore.Mvc;
using Models;

namespace Controllers
{
    public class HomeController: Controller
    {
        [Route("/")]
        public ActionResult Index()
        {
            return View("Views/Index.cshtml");
        }
    }
}
```
#### Product.cs
```csharp
namespace Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}
```
#### _ViewStart.cshtml
```razor
@{
    Layout = "_Layout";
}
```
#### _Layout.cshtml
```html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>@ViewData["Title"]</title>
    </head>
    <body>
        @RenderBody()
    </body>
</html>
```
#### Index.cshtml
```csharp
@{
    ViewData["Title"] = "Home Page";
}
<h2>This is Index.cshtml</h2>
```

## Add Blazor
According the article, I can add Blazor into the project.

* [Using Blazor Components In An Existing MVC Application](https://chrissainty.com/using-blazor-components-in-an-existing-mvc-application/)

### Add middleware and mapping Blazors SignalR hub
#### Startup.cs
```csharp
...
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddServerSideBlazor();
            services.AddControllers();
        }
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseRouting();
            app.UseStaticFiles();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
                endpoints.MapBlazorHub();
            });
        }
...
```
#### _Imports.razor
```blazor
@using System
@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using BlazorSample
```
#### BlzrSample.razor
```blazor
<h1>Hello @Name!</h1>
<p>This is Blazor Components.</p>
<div>@SampleInfo</div>
<button @onclick="ShowConsole">Click</button>
@code {
    [Parameter]
    public string Name {get; set;}
    [Parameter]
    public string SampleInfo { get; set; }
    public string GetName(){
        var name = $"{Name} {DateTime.Now}";
        Console.WriteLine(name);
        return name;
    }
    public void ShowConsole()
    {
        SampleInfo = GetName();
        Console.WriteLine($"{SampleInfo}");
    }
}
```
#### Index.cshtml
```razor
@using BlazorSample.Views
@{
    ViewData["Title"] = "Home Page";
}
<h2>This is Index.cshtml</h2>
@(await Html.RenderComponentAsync<BlzrSample>(RenderMode.ServerPrerendered, new { Name = "BlazorSample" }))
```

### Result
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/pxxbcl1vvsxzsoggytxo.jpg)

When I click the button, div element value will be changed dynamically.

## IE11
Though IE11 can render the page, the click event isn't worked.
One of the solution is using Blazor.Polyfill.

* [GitHub - Daddoon/Blazor.Polyfill: Polyfills for Blazor (for Internet Explorer 11 support and some other browsers)](https://github.com/Daddoon/Blazor.Polyfill)
* [ASP.NET Core Blazor supported platforms | Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/blazor/supported-platforms?view=aspnetcore-3.1)

#### _Layout.cshtml
```csharp
<!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>
        @RenderBody()
    <script src="_framework/blazor.server.js"></script>
    </body>
</html>
```

I must add "base" tag or get an error.

## Control DOM
C#(Blazor) methods can't access DOM directly except using @foreach, etc.

```blazor
@* when I add or remove items into the "Numbers",
the numbers of div elements will be changed *@
@foreach(var n in Numbers)
{
    <div>@SampleInfo</div>
}
```

So I should call JavaScript methods to control DOM elements. 

## Interacts with JavaScript
### Call JavaScript from Blazor
I can call JavaScript methods by IJSRuntime.
Because I usually use, I add webpack and TypeScript.

#### Samples
#### package.json
```json
{
    "dependencies": {
        "ts-loader": "^8.0.2",
        "tsc": "^1.20150623.0",
        "typescript": "^3.9.7",
        "webpack": "^4.44.1",
        "webpack-cli": "^3.3.12"
    }
}
```
#### webpack.config.js
```javascript
var path = require('path');

module.exports = {
    mode: 'development',
    entry: {
        'blazorSample': './wwwroot/ts/blazor-sample.ts',
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            }
        ]
    },
    resolve: {
        extensions: [ '.tsx', '.ts', '.js' ]
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, './wwwroot/js'),
        library: 'Page',
        libraryTarget: 'umd'
    }
};
```
#### blazor-sample.ts
```typescript
export function callFromBlazor(name: string): string
{
    console.log("Hello Blazor");
    return `Hello Blazor ${name}`;
}
```
#### BlzrSample.razor
```blazor
@inject IJSRuntime JSRuntime;
...
<button @onclick="CallFromBlazor">Click2</button>

@code {
...
    private async Task CallFromBlazor()
    {
        var result = await JSRuntime.InvokeAsync<string>("Page.callFromBlazor", SampleInfo);
        Console.WriteLine(result);
    }
}
```
* [Call JavaScript functions from .NET methods in ASP.NET Core Blazor | Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/blazor/call-javascript-from-dotnet?view=aspnetcore-3.1)

### Call C# static methods from JavaScript
I can call c# static methods by DotNet.invokeMethodAsync.
First, I add type definition.

```
npm install --save @types/blazor__javascript-interop
```
* [@types/blazor__javascript-interop - npm](https://www.npmjs.com/package/@types/blazor__javascript-interop)

#### blazor-sample.ts
```typescript
...
export function callStaticFromJs() {
    // 1. Assembly name, 2. Method name, 3. Arguments
    DotNet.invokeMethodAsync("BlazorSample", "CallStaticFromJs", "FromJS")
        .then(result => console.log(result));
}
```
#### BlzrSample.razor
```blazor
...
<button onclick="Page.callStaticFromJs()">Click3</button>

@code {
...
    // must add JSInvokable and set public static method.
    [JSInvokable]
    public static async Task<string> CallStaticFromJs(string message)
    {
        Console.WriteLine($"Hello JS {message}");
        return await Task.FromResult("Hello static");
    }
...
```

* [Call .NET methods from JavaScript functions in ASP.NET Core Blazor | Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/blazor/call-dotnet-from-javascript?view=aspnetcore-3.1)

### Call C# instance methods from JavaScript
To call C# instance methods, I shall create instance in C# methods, and send it to JavaScript methods.

#### HelloHelper.cs
```csharp
using Microsoft.JSInterop;

public class HelloHelper
{
    public HelloHelper(string name)
    {
        Name = name;
    }
    public string Name { get; set; }
    // call from JavaScript
    [JSInvokable]
    public string SayHello() => $"Hello, {Name}!";
}
```
#### blazor-sample.ts
```typescript
...
// argument: HelloHelper instance
export function callInstanceFromJs(dotnetHelper: any) {
    return dotnetHelper.invokeMethodAsync('SayHello')
      .then((r: any) => console.log(r));
}
```
#### BlzrSample.razor
```blazor
@inject IJSRuntime JSRuntime;
...
<button @onclick="CallInstanceFromJs">Click4</button>

@code {
...
    public async Task CallInstanceFromJs()
    {
        using(var helperRef = DotNetObjectReference.Create(new HelloHelper("Js instance")))
        {
            await JSRuntime.InvokeAsync<string>(
                "Page.callInstanceFromJs", helperRef);
        }  
    }
...
```
Though I use using clause to create instance, in actual usages, I may separate disposing the instance from the creating method.

## Call server side methods
How to call server side methods from Blazor?
Blazor can use DI.
So I can inject service classes or any other classes.

* [ASP.NET Core Blazor lifecycle | Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle?view=aspnetcore-3.1)

#### IBlazorService.cs
```csharp
using System.Threading.Tasks;
using Models;

namespace Services
{
    public interface IBlazorService
    {
        Task<Product> GetMessageAsync(string Name);
    }
}
```
#### BlazorService.cs
```csharp
using System.Threading.Tasks;
using Models;
namespace Services
{
    public class BlazorService: IBlazorService
    {
        public async Task<Product> GetMessageAsync(string name)
        {
            return await Task.FromResult(new Product
            {
                Id = 2,
                Name = name,
            });
        }
    }
}
```
#### Startup.cs
```csharp
...
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
        services.AddServerSideBlazor();
        services.AddControllers();
        services.AddScoped<IBlazorService, BlazorService>();
    }
...
```
#### BlzrSample.razor
```blazor
@inject Services.IBlazorService _blazor
...
<button @onclick="CallServerside">Click5</button>

@code {
...
    public async Task CallServerside()
    {
        var result = await _blazor.GetMessageAsync(Name);
        if (result == null)
        {
            Console.WriteLine("Cannot get message");
            return;
        }
        Console.WriteLine($"Success ID: {result.Id} Name: {result.Name}");
    }
}
```