2020年11月24日火曜日

Blazor WebAssemblyをvscodeで触ってみた

どうも、もりもりです。

今月ついに.NET 5.0がリリースされましたね。 今回は.NETでSPA(SinglePageApplication)を実現できるWebフレームワークの BlazorWebAssemblyを触ってみました。 SPAとは、ページ遷移を行わない単一Webページで動作するWebアプリです。 一般的なWebアプリではJavaScriptを用いてSPAを実現しますが、 C#だけでできてしまうってC#erからしたら嬉しくないですか?

はじめに

Blazorには2種類のモデルがあり、サーバーサイドで動作するBlazor Serverと、 ブラウザ上で動作するBlazor WebAssemblyがあります。

Blazor Serverは、サーバー上の.NET Core環境で主な処理が行われ、サーバー側がブラウザのDOMを変更します。 Blazor WebAssemblyは、最初に.NETアセンブリとランタイムがブラウザにダウンロードされ ブラウザ上で.NETコードを実行し、別途プラグインなどは不要です。

vscodeでBlazor WebAssemblyプロジェクトを作成して、 さらにTodoMVC のようなTodoページを追加してみました。

プロジェクト作成

まずは任意の場所で下記コマンドを叩いてプロジェクトを作成します。

$ dotnet new blazorwasm -o BlazorWasmSample

プロジェクト構成

確認すると初めからテンプレート的な感じで3つほどページが用意されています。

BlazorWasmSample
  ├── App.razor
  ├── BlazorWasmSample.csprojs
  ├── Pages
  │   ├── Counter.razor
  │   ├── FetchData.razor
  │   └── Index.razor
  ├── Program.cs
  ├── Properties
  │   └── launchSettings.json
  ├── Shared
  │   ├── MainLayout.razor
  │   ├── MainLayout.razor.css
  │   ├── NavMenu.razor
  │   ├── NavMenu.razor.css
  │   └── SurveyPrompt.razor
  ├── _Imports.razor
  └── wwwroot
      ├── css
      │   ├── app.css
      │   ├── bootstrap
      │   │   ├── bootstrap.min.css
      │   │   └── bootstrap.min.css.map
      │   └── open-iconic
      │       ├── FONT-LICENSE
      │       ├── ICON-LICENSE
      │       ├── README.md
      │       └── font
      │           ├── css
      │           │   └── open-iconic-bootstrap.min.css
      │           └── fonts
      │               ├── open-iconic.eot
      │               ├── open-iconic.otf
      │               ├── open-iconic.svg
      │               ├── open-iconic.ttf
      │               └── open-iconic.woff
      ├── favicon.ico
      ├── index.html
      └── sample-data
          └── weather.json

ソースコードは下記に置いてます。

アプリ起動

ということで早速実行してみましょう。 BlazorWasmSampleまで移動して下記コマンドで実行します。

$ dotnet run

https://localhost:5001 にアクセスします。 シンプルで今風な感じの画面が表示されました。 Homeメニューには、おなじみの「Hello world」ですね。

Counterメニューをみてみましょう。 ボタンをポチポチするとカウントアップされていきます。

最後はFetch dataページです。 こちらはGrid的な表の画面です。 標準のtableなので編集やソートなどフィルタリングなどはできません。 このあたりはいい感じのパッケージがないかまた探してみます。

ソースを確認

Counterページのソースを確認してみましょう。

Counter.razor

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

これだけで書けてしまうんですね。 すごい。シンプル。

ボタンをクリックすると @code 内の変数 currentCount がインクリメントされ 画面上の @currentCount に反映されるようです。

Todoを追加してみる

◆ 追加 / 編集ファイル

では、TodoMVCのような ページを追加してみましょう。 追加、編集するファイルは下記の4つだけです。

BlazorWasmSample
  ├── Pages
  │   ├── [★] Todo.cs
  │   └── [★] Todo.razor
  ├── Shared
  │   ├── [★] NavMenu.razor
  ├── [★] TodoItem.cs
◆ TodoItemクラス追加

まずはTodoアイテムのクラスを追加します。

TodoItem.cs

public class TodoItem
{
    public string Content {get; set;}
    public bool IsDone {get; set;}
}
◆ Viewと処理を分けてファイルを追加

次に画面と処理ですが、コードが長くなってしまうのでrazorファイルにはViewのみとし、処理はpartialクラスにしてTodo.csと、ファイルを2つに分けて追加します。

Todo.razor

@page "/todo"

<h1>Todo</h1>

・・・

Todo.cs

namespace BlazorWasmSample.Pages
{
    public partial class Todo
    {
        ・・・
    }
}
◆ 入力したTodoアイテムを追加

Todoを入力してEnterでどんどん追加していくようにします。
※現状、Enterを2回押さないとリストに追加されないのでまた時間ある時にデバッグして調べてみます。。。

Todo.razor

<input style="width: 100%" placeholder="What needs to be done?" @bind="newItem" @onkeydown="@Enter" />

Todo.cs

// Todoアイテム全て
private List<TodoItem> allTodos = new List<TodoItem>();
// 追加するアイテム
private string newItem;
// 表示用のTodoアイテム
private List<TodoItem> todosForDisplay = new List<TodoItem>();
// 現在の表示状態
private State currentState;
// 表示状態
private enum State
{
    All,
    Active,
    Completed,
}

/// <summary>
/// アイテムを追加
/// </summary>
private void AddItem() {
    if (!string.IsNullOrWhiteSpace(newItem))
    {
        allTodos.Add(new TodoItem() { Content = newItem, });
        this.ShowTodo();
        newItem = string.Empty;
    }
}

/// <summary>
/// Todoアイテムを表示
/// </summary>
private void ShowTodo()
{
    switch (this.currentState)
    {
        case State.All:
            todosForDisplay = allTodos;
            break;
        case State.Active:
            todosForDisplay = allTodos.Where(x => !x.IsDone).ToList();
            break;
        case State.Completed:
            todosForDisplay = allTodos.Where(x => x.IsDone).ToList();
            break;
    }
}

/// <summary>
/// Enterキーイベント
/// </summary>
/// <param name="e">イベントデータ</param>
private void Enter(KeyboardEventArgs e)
{
    Console.WriteLine($"KeyboardEventArgs : Code[{e.Code}] Key[{e.Key}]");
    if (e.Key == "Enter")
    {
        this.AddItem();
    }
}
◆ Todoリスト表示

追加したアイテムは下記のように表示します。 TodoItem.IsDoneをCheckBoxに、TodoItem.ContentをTextBoxにバインドしてます。

Todo.razor

<ul>
    @foreach (var item in todosForDisplay)
    {
        <li>
            <input type="checkbox" @bind="item.IsDone" />
            <input @bind="item.Content"/>
        </li>
    }
</ul>
◆ Todoアイテムのチェック変更

追加したアイテムのCheckをONにすると取消線を入れ文字色をグレーに、OFFにすると黒に戻すようスタイルと処理を追加します。

Todo.razor

<ul>
    @foreach (var item in todosForDisplay)
    {
        <li>
            <input type="checkbox" @bind="item.IsDone" />
            <input @bind="item.Content" style="color: @GetTextStyles(item).foreColor; text-decoration: @GetTextStyles(item).decoration"/>
        </li>
    }
</ul>

色と装飾をTupleで返します。

Todo.cs

/// <summary>
/// Todoアイテムのテキストスタイルを取得
/// </summary>
/// <param name="item">Todoアイテム</param>
/// <returns>文字色と取消線を付けるかどうかのセット</returns>
private (string foreColor, string decoration) GetTextStyles(TodoItem item)
{
    return item.IsDone ? ("lightgray", "line-through") : ("black", "none");
}
◆ 未完了アイテムの残数表示

未完了のActiveなTodoItemのカウントを左下に表示します。View側に直接LINQでバインドしちゃいます。

Todo.razor

@allTodos.Count(x => !x.IsDone) items left
◆ 表示内容の切り替え

全て / 未完了 / 完了と表示を切り替えるボタンを追加し、メソッドをView側でバインドする。

Todo.razor

<button @onclick="ShowAll">All</button>
<button @onclick="ShowActive">Active</button>
<button @onclick="ShowCompleted">Completed</button>

Todo.cs

/// <summary>
/// Todoアイテム全て表示
/// </summary>
private void ShowAll() {
    currentState = State.All;
    this.ShowTodo();
}

/// <summary>
/// Todo未完了アイテム表示
/// </summary>
private void ShowActive() {
    currentState = State.Active;
    this.ShowTodo();
}

/// <summary>
/// Todo完了アイテム表示
/// </summary>
private void ShowCompleted() {
    currentState = State.Completed;
    this.ShowTodo();>
}
◆ 完了アイテムのクリア

完了アイテムが一つ以上ある場合はClear completedボタンを表示するようにし、 クリックで完了アイテムをメモリから削除する。

Todo.razor

<button @onclick="ClearCompleted" style="display: @GetDisplay()">Clear completed</button>

Todo.cs

/// <summary>
/// Todo完了アイテムをクリア
/// </summary>
private void ClearCompleted()
{
    var completedTodos = allTodos.Where(x => x.IsDone);
    allTodos = allTodos.Except(completedTodos).ToList();
    this.ShowTodo();
}

/// <summary>
/// ClearCompletedボタン表示スタイルを取得
/// </summary>
/// <returns>表示スタイル [inline-block / none]</returns>
private string GetDisplay()
{
    return allTodos.Any(x => x.IsDone) ? "inline-block" : "none";
}
◆ メニューへTodoページを追加

最後に、Todo.razorで指定した @page "/todo" へのリンクをメニューに追加します。

NavMenu.razor

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="todo">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Todo
            </NavLink>
        </li>
    </ul>
</div>

さいごに

記事の内容は長くなってしまいましたが、思ってたよりもスッキリとコードが書けるのでサクっと作れてしまいました。 ただブラウザにランタイムや.NETアセンブリをダウンロードして実行するので機能の多いアプリにしてしまうとダウンロードに時間がかかってしまうようです。 それでも手軽にSPAをC#で作れちゃうってなかなかイイですね。

今回追加したTodoはスタイルとか特にセットしてないので味気ない感じになってますが MatBlazorのようなマテリアルデザインのUIフレームワークもあり 面白そうなので近々使ってみようと思います。

以上、もりもりでした。

2020年11月16日月曜日

Googleのストレージポリシーが変わってしまう

こんにちは、やっまむーです。

先日、Googleからこんな発表がありました。

これまでGoogleフォトでは高画質に圧縮された写真や動画はストレージを圧迫する事なく、いくらでもアップロードできました。
ところが、2021年6月1日からはアップロードされたもの全てがストレージを使用するようになります。
他のサービスと比較して圧倒的に多い容量を無料で提供していたので、今までが特別だったとも言えます。

Pixelシリーズを使ってる人は

なんと、Pixelシリーズを使っている人はこのポリシー変更が適用されません。
このメールは対象のPixelシリーズを使っているアカウントに送られているようです。
私はPixel3XL、妻はPixel4aを使っており、二人とも届いていました。

Pixelシリーズの特典として、写真や動画を元の画質でアップロードしてもストレージを使用せずにアップロードできるというのがあります。
この特典はどうなるのかというと、別のページに明記されていました。

写真や動画のアップロード サイズを選択する
Pixel 3a~Pixel 5
写真と動画は高画質でアップロードすることも、元の画質でアップロードすることもできます。Pixel 3a~Pixel 5 のデバイスから高画質でアップロードした写真と動画は、無料、容量無制限で保存されます。元の画質でアップロードした写真と動画は、Google アカウントの保存容量を使用するようになります。詳しくは、アップロード サイズを変更する方法とストレージ プランをご覧ください。

Pixel 3 
2022 年 1 月 31 日まで、Pixel 3 から Google フォトにアップロードしたすべての写真と動画は元の画質で保存され、無料、容量無制限です。この日以前にアップロードした写真と動画は、その後も引き続き元の画質のまま無料で保存されます。2022 年 1 月 31 日以降は、新しい写真と動画は高画質でアップロードされ、無料で保存されます。新しい写真と動画を元の画質でアップロードする場合は、割り当てられている保存容量を使用するようになります。

Pixel3は元の画質でアップロードできる期間が決められていますが、高画質設定であれば無料で保存し続けられます。

気になるのは、今後発売されるPixelシリーズの場合どうなるかです。
ポリシーが変更されてしまいましたが、何らかの特典が付くのであれば端末変更をしやすいので是非ともお願いしたいところです。

ではではー。

2020年11月4日水曜日

「無限くら寿司」で【レシート画像のアップロードに失敗しました】とエラーになる件


こんにちは。よっしーです。

GoToEatキャンペーン始まりましたね。


巷では「無限くら寿司」が流行っている。ということで、

血税を少しでも回収するべく流行りに乗るべく、私も試してみました。


お店の予約は「EPARK」というスマホアプリからしかできず、

ポイント申請はWEBサイトからしかできずと、

正直手順が面倒です。しかもアプリが重い。。。


で、ポイント申請のため、レシートを撮影し、

アップロードする手順があるのですが、

ここで「罠」があります。


画像アップロードを行い、数時間後、以下のような失敗メールが届きました。

何度レシート画像を送り直しても同じ失敗メールが届きます。


画像が悪いのかと考え、写真を拡大してみたり、

画像の解像度を小さくしてみたりしましたが、

結果は変わらずでした。


で、いろいろと調べてみると、

同じような症状でハマっている人が多く、


・サーバーが込み合ってて待つしかない。

・無限対策された。


など、嘘かほんとかわからない情報が流れていました。


で、たどり着いた解決策が以下です。


アップロードする画像を選択後、利用金額を入力するフォームで

ソフトウェアキーボードのEnterを押さない。


ということでした。。。


金額をEnterで決定すると、意図しない状態で、

画像のアップロード処理に遷移するようで、

必ず失敗するようです。


ただのバグですやん。。。


ということで、金額入力後、ソフトウェアキーボード範囲外を

タップすることで、ソフトウェアキーボードを閉じ、

その後、アップロードボタンをタップすると、

数時間後に無事にポイントが付与されました。

この現象で困っている人が多いようなので、

皆さんの助けになれば幸いです。


システム側ではよ修正してよ。。。って話ですが。


ではまた~。