Blazorで、電卓アプリをつくってみる

新人研修や、新しいフレームワークが登場したとき、`Hello World`と並び、電卓アプリを作る人も多いのではないでしょうか。
今回はBlazor Client-Sideアプリで、簡単な電卓アプリを作り、PWA化してみたいと思います。

[Demo](https://calc-f25aa.firebaseapp.com/)
![1.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110304/71b4f6da-aacc-ea69-27f4-9e0dbde99231.jpeg)


### 開発環境
Windows10
.Net Core 3.1
Visual Studio2019 Preview 16.5.0


# 電卓画面の実装
電卓画面はシンプルに１つのページなので、`Index.razor` に実装します。電卓アプリの計算はチョンボして`System.Data.DataTable`に任せ、画面は数式と結果を表示するようにします。


```javascript:Index.razor
@page "/"

<style>
    .calbtn {
        height: 45px;
        font-size: 30px;
        border: solid 1px;
        text-align: center;
    }

    .num {
        height: 45px;
        font-size: 30px;
        border: solid 1px;
        text-align: right;
    }
</style>
<div class="container-fluid">

    @*式を表示*@
    <div class="row">
        <div class="col-12 rounded-lg calbtn ">@str</div>
    </div>
    @*結果を表示*@
    <div class="row">
        <div class="col-12 rounded-lg num">@result</div>
    </div>

    @*ボタン類*@
    <div class="row">
        <div class="col-3 rounded-lg calbtn " @onclick="allclear">AC</div>
        <div class="col-3 rounded-lg calbtn " @onclick="@(_=>str+="(")">(</div>
        <div class="col-3 rounded-lg calbtn " @onclick="@(_=>str+=")")">)</div>
        <div class="col-3 rounded-lg calbtn " @onclick="@(_=>str+="/")">/</div>
    </div>
    <div class="row">
        <div class="col-3 rounded-lg calbtn " @onclick="@(_=>str+="7")">7</div>
        <div class="col-3 rounded-lg calbtn " @onclick="@(_=>str+="8")">8</div>
        <div class="col-3 rounded-lg calbtn " @onclick="@(_=>str+="9")">9</div>
        <div class="col-3 rounded-lg calbtn " @onclick="@(_=>str+="*")">*</div>
    </div>
    <div class="row">
        <div class="col-3 rounded-lg calbtn " @onclick="@(_=>str+="4")">4</div>
        <div class="col-3 rounded-lg calbtn " @onclick="@(_=>str+="5")">5</div>
        <div class="col-3 rounded-lg calbtn " @onclick="@(_=>str+="6")">6</div>
        <div class="col-3 rounded-lg calbtn " @onclick="@(_=>str+="-")">-</div>
    </div>
    <div class="row">
        <div class="col-3 rounded-lg calbtn " @onclick="@(_=>str+="1")">1</div>
        <div class="col-3 rounded-lg calbtn " @onclick="@(_=>str+="2")">2</div>
        <div class="col-3 rounded-lg calbtn " @onclick="@(_=>str+="3")">3</div>
        <div class="col-3 rounded-lg calbtn " @onclick="@(_=>str+="+")">+</div>
    </div>
    <div class="row">
        <div class="col-6 rounded-lg calbtn" @onclick="@(_=>str+="0")">0</div>
        <div class="col-3 rounded-lg calbtn" @onclick="@(_=>str+=".")">.</div>
        <div class="col-3 rounded-lg calbtn" @onclick="calc">=</div>
    </div>
</div>
@code{

    /// <summary>
    /// 数式
    /// </summary>
    string str = "";
    
    /// <summary>
    /// 結果
    /// </summary>
    string result = "0";

    /// <summary>
    /// ACボタン
    /// </summary>
    void allclear()
    {
        str = "";
        result = "0";
    }

    /// <summary>
    /// 計算処理
    /// </summary>
    void calc()
    {
        var dt = new System.Data.DataTable();
        try
        {
            result = dt.Compute(str.Trim(), "").ToString();
        }
        catch
        {
            result = "Error!!";
        }
    }
}
```
# MainLayout.razorの編集
デフォルトの`MainLayout.razor`では、アプリバーやサイドバーが表示されてしまうので、メインコンテンツのみ表示されるように編集します。

```javascript:MainLayout.razor
@inherits LayoutComponentBase
@*コメントアウト ->
        <div class="sidebar">
            <NavMenu />
        </div>
<- コメントアウト *@

    <div class="main">
    @*コメントアウト ->
    <div class="top-row px-4">
        <a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
    </div>
    <- コメントアウト*@
    <div class="content px-4">
        @Body
    </div>
</div>
```

# PWA化
Blazor Client-Sideアプリでは、はじめからPWA(Progressive Web Apps)に対応していませんが、PWA化する方法はいくつかあります。今回は必要なファイルを`wwwroot`フォルダに配置して対応していきたいと思います。

## manifest.jsonの作成
PWA化するには、`manifest.json`を用意する必要があります。ファイルを用意し、`wwwroot`直下に配置します。このファイルに誤りがあると、ブラウザにPWAアプリケーションとして正常に認識されないので、はまりどころとなります。

```json:manifest.json
{
  "name": "Calculator",
  "short_name": "Calc",
  "start_url": "/",
  "theme_color": "#000000",
  "background_color": "#FFFFFF",
  "display": "standalone",
  "orientation": "portrait",
  "icons": [
    {
      "src": "images/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "images/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
```
URLの入力は不要なので、`display`には`standalone`を指定し、縦の表示にしたいので`orientation`を`portrait`に指定します。

# serviceworker.jsの作成
オフラインで実行させるために、`serviceworker.js`を用意します。ファイルを用意し、`wwwroot`直下に配置します。
こちらのサイトを参考に、`serviceworker.js`を作成します。

[Create Progressive Web Apps with .NET using Blazor](https://medium.com/@k.l.mueller/create-progressive-web-apps-with-net-using-blazor-6aa719e38000)


`serviceworker.js`は、初回ロード時にオフライン実行用にキャッシュを作成するため、キャッシュさせたいファイルを列挙しています。面倒くさいですが誤りがあると、オフラインで実行できないので注意が必要です。

```javascript:serviceworker.js
console.log("This is service worker talking");
var cacheName = 'blazor-pwa-sample';
var filesToCache = [
    //wwwroot
    '/',
    '/index.html',
    '/manifest.json',
    '/serviceworker.js',
    '/favicon.ico',
    //css
    '/css/site.css',
    '/css/bootstrap/bootstrap.min.css',
    '/css/open-iconic/font/css/open-iconic-bootstrap.min.css',
    '/css/open-iconic/font/fonts/open-iconic.woff',
    //images
    '/images/icon-192x192.png',
    '/images/icon-512x512.png',
    // カレントプロジェクトのdll
    '/_framework/_bin/BlazorApp40.dll',
    //.net framework
    '/_framework/blazor.webassembly.js',
    '/_framework/blazor.boot.json',
    '/_framework/wasm/mono.js',
    '/_framework/wasm/mono.wasm',
    '/_framework/_bin/Microsoft.AspNetCore.Authorization.dll',
    '/_framework/_bin/Microsoft.AspNetCore.Blazor.dll',
    '/_framework/_bin/Microsoft.AspNetCore.Blazor.HttpClient.dll',
    '/_framework/_bin/Microsoft.AspNetCore.Components.dll',
    '/_framework/_bin/Microsoft.AspNetCore.Components.Forms.dll',
    '/_framework/_bin/Microsoft.AspNetCore.Components.Web.dll',
    '/_framework/_bin/Microsoft.AspNetCore.Metadata.dll',
    '/_framework/_bin/Microsoft.Bcl.AsyncInterfaces.dll',
    '/_framework/_bin/Microsoft.Extensions.DependencyInjection.Abstractions.dll',
    '/_framework/_bin/Microsoft.Extensions.DependencyInjection.dll',
    '/_framework/_bin/Microsoft.Extensions.Logging.Abstractions.dll',
    '/_framework/_bin/Microsoft.Extensions.Options.dll',
    '/_framework/_bin/Microsoft.Extensions.Primitives.dll',
    '/_framework/_bin/Microsoft.JSInterop.dll',
    '/_framework/_bin/Mono.Security.dll',
    '/_framework/_bin/Mono.WebAssembly.Interop.dll',
    '/_framework/_bin/mscorlib.dll',
    '/_framework/_bin/System.ComponentModel.DataAnnotations.dll',
    '/_framework/_bin/System.Core.dll',
    '/_framework/_bin/System.Data.dll',
    '/_framework/_bin/System.dll',
    '/_framework/_bin/System.Net.Http.dll',
    '/_framework/_bin/System.Numerics.dll',
    '/_framework/_bin/System.Runtime.CompilerServices.Unsafe.dll',
    '/_framework/_bin/System.Text.Encodings.Web.dll',
    '/_framework/_bin/System.Text.Json.dll',
    '/_framework/_bin/System.Xml.dll',
    '/_framework/_bin/WebAssembly.Bindings.dll',
    '/_framework/_bin/WebAssembly.Net.Http.dll'
];

self.addEventListener('install', function (e) {
    console.log('[ServiceWorker] Install');
    e.waitUntil(
        caches.open(cacheName).then(function (cache) {
            console.log('[ServiceWorker] Caching app shell');
            return cache.addAll(filesToCache);
        })
    );
});

self.addEventListener('activate', event => {
    event.waitUntil(self.clients.claim());
});

self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request, { ignoreSearch: true }).then(response => {
            return response || fetch(event.request).then(response => {
                caches.put(event.request, response.clone());
                return response;
            });
        })
    );
});
```

# index.htmlの編集
`wwwroot/index.html`を編集し、`manifest.json`の読み込みの追加と、`serviceworker.js`が動作するようにセットします。
今回は、iPhoneにインストールしたいため、いくつかの設定値を追加してセットしています。

```html:index.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!--user-scalable=n　を追加して、拡大縮小を禁止する-->
    <meta name="viewport" content="width=device-width, user-scalable=n" />
    <title>Calculator</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/site.css" rel="stylesheet" />

    <!--追加-->
    <link rel="manifest" href="manifest.json" />
    <!--For iPhone/iPad-->
    <link rel="apple-touch-icon" href="images/icon-192x192.png" sizes="192x192" />
</head>

<body>
    <app>Loading...</app>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>

    <!--追加-->
    <script>
        if ('serviceWorker' in navigator) {
            console.log('Registering service worker now');
            navigator.serviceWorker.register('/serviceworker.js')
                .then(function () {
                    console.log('Service Worker Registered');
                });
        }
    </script>
</body>

</html>
```

# 動作確認

PWAとして正常に認識されると、様々なプラットフォームにインストールできるようになります。
![4.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110304/e2e5d788-695e-83d2-7481-4f6cad6797bc.jpeg)

オフラインでもキャッシュにより、動作が確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110304/2fff8e30-7eca-c19d-3cd2-c4d1509b083b.png)

PWAといっても、実態はWEBブラウザですが、BlazorによりC#でiPhoneやmacOSで動くアプリケーションを作る事ができます。