Azure Functionsで初めてのPython関数を作ったら躓くの話

なんとはなしに、Function AppをPythonで作ってみるテストで、いきなり躓いたのでメモ。

Azure で初めての Python 関数を作成する | Microsoft Docs
https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-create-first-function-python

基本的な手順は、上記に書いてあるので、素直にコマンドを打てばいいのだけど、どうやら一カ所だけ「書いてない」ので、そこで頓挫してしまう。

仮想環境をアクティブにする


python3.6 -m venv .env
source .env/bin/activate

プロジェクトを作成する

Pythonの仮想環境で動かしているので、”–worker-runtime python” は必要なし。


func init MyFunctionProj

関数を作成する

プロジェクトのフォルダに移動してから、func new する


cd MyFunctionProj
func new

コード(__init__.py)を見る


import logging
import azure.functions as func
def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')
    name = req.params.get('name')
    if not name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            name = req_body.get('name')
    if name:
        return func.HttpResponse(f"Hello {name}!")
    else:
        return func.HttpResponse(
             "Please pass a name on the query string or in the request body",
             status_code=400
        )

ローカルで実行する


func host start


http://localhost:7071/api/HttpTrigger

にブラウザでアクセスすればOK.

デプロイする

あらかじめ Azure に作成しておいた「azfunc-python」という名前のFunction Appにデプロイします。


func azure functionapp publish azfunc-python

こんな風に、エラー出ます。


Getting site publishing info...
pip download -r /home/masuda/azfunc/MyFunctionProj/requirements.txt --dest /tmp/azureworkeriyncp4zc
There was an error restoring dependencies.ERROR: ['pip', 'download', '-r', '/home/masuda/azfunc/MyFunctionProj/requirements.txt', '--dest', '/tmp/azureworkeriyncp4zc'] failed with exit code 1

何度もやっても駄目、環境を変えても駄目なんですが、どうやら「requirements.txt」の中にある「pkg-resources==0.0.0」を削除すればいいらしいのです。


azure-functions==1.0.0b3
azure-functions-worker==1.0.0b4
grpcio==1.14.2
grpcio-tools==1.14.2
pkg-resources==0.0.0	★ここを削除
protobuf==3.7.1
six==1.12.0

なんでテンプレートがこの行を作るのか、なぜこの行を消すとうまくいくのかよくわからんのですが、vi などで消します。
Git あたりで見ると、去年の暮れあたりでバグっぽい雰囲気があるのですが、解決されたのかどうかよくわかりません。クローズはされているけど。

Azure Portalで見てみる

ポータルで見ると、無事アップロードできました。

めでたしめでたし。

カテゴリー: 開発 | Azure Functionsで初めてのPython関数を作ったら躓くの話 はコメントを受け付けていません

Access形式でバックアップしてSQL Serverに手軽に戻すC#な方法

SQL Server Management Studio の「データのエクスポート」を使うと、SQL Server から手軽にバックアップが取れる。バックアップというよりも、どこかに移動したいときに使うわけだが、Access 形式で取るのが一番手軽だと思う。

image

問題

顧客の SQL Server なデータを、開発用の SQL Server に吸い上げて、検証環境を作ることはよくやる。確実な方法は、SQL Server のバックアップをコピーするのがベストなのだけど、残念ながらバージョンが違うとこれがうまくいかない。バージョンが違うのだから、バージョンを揃えた環境を整えるのが筋なんだけど、古めのデータベース(SQL Server 7 とか)とか微妙な差異があって、いろいろなバージョンを揃えておくのも以外と面倒くさい。できれば、開発環境の SQL Server をそのまま使っておきたい。

SSMS の「データのインポート」がそのまま動けばいいのだが、Access 形式にエクスポートしたときにカラムの型が違ってしまっていて、インポート時にエラーになる。それに PK の扱いもややこしくて、ちまちま設定をしながらインポートをしないといけない。ひとつふたつのテーブルならば、手作業でもいいのだけど、

  • 大量のテーブル(20以上ある)をインポートするのに、いちいち手作業は大変
  • 何度もインポートするので手作業は大変

解決方法

ということで、緩い感じで、Access から SQL Server へインポートできないものか?と考えていた。で、結局 C# でスクリプト的なものを書くことにする。

顧客 SQL Server → Access → 開発 SQL Server

という流れなので、SQL Server 同士の型は一緒になっている。最初の「顧客 SQL Server → Access 」は問題がないので、後の「Access → 開発 SQL Server」をスクリプト化する。


public class DBBackup
{
    public string AccessMdbPath { get; set; }
    public string SqlServerConnectionString { get; set; }

    public bool Restore( string tableName )
    {
        // Accessから読み込み
        OleDbConnection cn = new OleDbConnection();
        cn.ConnectionString = @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + AccessMdbPath; // MDB名など
        var dt = new DataTable();
        var da = new OleDbDataAdapter($"select * from {tableName}", cn);
        da.Fill(dt);
        // SQL Severへ書き出し
        var cnn = new SqlConnection(SqlServerConnectionString);
        // 指定テーブル内を消去
        var cmd = new SqlCommand($"TRUNCATE TABLE {tableName}", cnn);
        cmd.Connection.Open();
        cmd.ExecuteNonQuery();
        cmd.Connection.Close();
        // バルクコピーで挿入
        var bc = new SqlBulkCopy(cnn);
        cnn.Open();
        bc.DestinationTableName = tableName;
        bc.WriteToServer(dt);
        cnn.Close();

        return true;
    }

}

こんな感じに

  • OleDb で Access MDB ファイルから読み込む
  • SqlBulkCopy を使って、SQL Server にバルクコピーする

バルクコピーが一番早いし、こうじゃないと実行時に時間が掛かってしまう。

使い方

テーブル名は Access でも SQL Server でも一緒なので、DBBackup::Restore を使ってがしがしと戻していく。テーブル名は自動で取得できるけど、今回の場合は、移行不要なテーブルもあるので、必要なものだけピックアップしている。


private void clickLoadAccess(object sender, RoutedEventArgs e)
{
    var dbb = new DBBackup()
    {
        AccessMdbPath = @"営業管理_20190304.mdb",
        SqlServerConnectionString = "Data Source=.;Initial Catalog=営業管理2;Integrated Security=True;",
    };

    var tables = new List()
    {
        "タイトルファイル2",
        "タイトルファイル属性",
        "プログラム一覧",
        "パスワード",
        "ユーザマスタ",
...
    };

    foreach ( var name in tables )
    {
        dbb.Restore( name );
        System.Diagnostics.Debug.WriteLine("テーブル: " + name);
    }
    MessageBox.Show("保存しました");
}
カテゴリー: 開発, C# | Access形式でバックアップしてSQL Serverに手軽に戻すC#な方法 はコメントを受け付けていません

サブドメインでアクセスしてDockerコンテナの指定ポートに割り当てる

ほどよく、Azure Functions と Docker の組み合わせがうまくいくことが分かったので、実運用でのパターンを考えてみる。Docker は内部のポートを外部のポートへ -p オプションを使ってフォワードして、外部から http://servername.com:8000/ のようにアクセスできる。だが、こうすると Docker を動かして機能を増やすたびにポートを開放しないといけなくて、結構リスキーだったりする。

ならば、host://func.servername.com/ のようにサブドメイン名をしておいて、これを http://servername.com:8000/ にフォワードしてくれるように設定すれば、開放するポートは 80 番だけで済む。これを実現するのが「リバースプロキシ」なんだけど、Apache や Nginx でその機能がある。

Apacheでリバースプロキシ – Qiita https://qiita.com/tac0x2a/items/1ad05f204ac5ab679fb2

ひとまず、Apache を使って動作を確認してみよう。

目的

リバースプロキシを使って、サブドメインを指定すると、内部で起動している Docker コンテナに接続できるようにする。

実現方法

  • ローカルで動作している Linux サーバー「luna.local」を使う。
  • Docker コンテナを -p 8000:80 で起動しておく
  • Windows 10 から azfunc.luna.local にアクセスすると、luna.local:8000 とアクセスしたと同じように設定する

設定手順

リバースプロキシの設定

Linux サーバーの luna.local に apache2 をインストールして、


/etc/apache2/sites-available/azfunc.luna.local.conf


を設定する。


<VirtualHost *:80>
  ServerAdmin webmaster@azfunc.luna.local
  ErrorLog /var/log/apache2/error.log
  CustomLog /var/log/apache2/access.log combined
  LogLevel warn
  ProxyPreserveHost On
  ServerName azfunc.luna.local
  ProxyPass        / http://localhost:8000/
  ProxyPassReverse / http://localhost:8000/
</VirtualHost>

これでリバースプロキシを使って、azfunc.luna.local のアクセスが http://localhost:8000/ にフォワードされる。

proxy 用のコマンドを動かして、apache2 をリブートする。


$ sudo a2enmod proxy
$ sudo a2enmod proxy_http
$ sudo a2ensite azfunc.luna.local.conf
$ sudo service apache2 restart

Docker の起動


sudo docker run -it --rm -p 8000:80 moonmile/sample-azure-docker

あらかじめ、hub.docker.com にアップロード済みの moonmile/sample-azure-docker をダウンロードして実行する。

sample-azure-docker は、func init と func new して作った .NET 環境の Azure Function である。

Windows 10 の hosts 設定

実は、luna.local は、DNS を使っているのではなくて、ubuntu 上で avahi-daemon を起動している。なので、Windows 10 から luna.local は見れるけど、azfunc.luna.local は見れない。

これを見れるようにするため、Windows 10 の hosts に以下の行を追加する。IP は Docker が動いている Linux サーバーの IP になる。この場合は、luna.local と同じ値になる。


192.168.1.11 azfunc.luna.local

ping azfunc.luna.local として通知できているか確認しておく。

ブラウザからアクセス

ブラウザから http://azfunc.luna.local/ へアクセスすると、無事 Function App の最初のページが表示される。これでうまく通達ができていることがわかる。

image

再びブラウザで、URLアドレス http://azfunc.luna.local/api/HttpTrigger1?name=masuda にアクセスすると、「Hello, masuda」と表示されることがわかる。

image

これでうまくローカルのFunction Appが動いていることが確認できた。

まとめ

構造的にはこんな形になっている。

image

  1. ブラウザからサブドメイン付きで呼び出す
  2. Apache2 は 80 番で受ける
  3. リバースプロキシで、指定サブドメインは 8000 番へ送る
  4. Dockerは 8000 番で受けたものを、コンテナの 80 番へ送る
  5. .NET Core + Function App が動作する

いわゆる、本番の Azure 上で sample-azure-docker.azurewebsites.net のようにアクセスしている部分を、Azure 無し&ローカル環境で構築できるというわけだ。当然のことながら、Azure の機能は使えないけど、Azure Functions だけの機能を使って試したいというのは結構簡単に実現できることが分かる。特に、本番の Azure Functions で Linux 環境+Docker の組み合わせにしようとすると、Linux の VM が必要になってしまい、月額5,000円位かかることになる。これはちょっと高い。

なので、一般的な VPS を借りてきて、そこの中でサブドメイン付きで作れれば、.NET Core + Azure Functions な環境を Docker で動かすのも悪くないかなと思うのだがどうだろうか。たぶん、Docker の運用系の使い方としては、こういうサブドメインを経由させるほうが一般的だと思う。

実際は認証やSSLなどは、HTTPヘッダの部分をリバースプロキシのところで設定しないといけないのだが。まあ、ひとまずローカルな Linux サーバーで確認がとれたといことで。

あとで、nginx のリバースプロキシも確認してみる。

カテゴリー: Azure, Docker, NET Core | サブドメインでアクセスしてDockerコンテナの指定ポートに割り当てる はコメントを受け付けていません

Docker Toolbox で Azure Functionsを動かす

Hyper-V が動かない環境の場合、Docker Toolbox を使えばよいので、使ってみる。内部で VirtualBox を動かしているので、仮想OSを VMWare で作っていると変わらないのだが、UI で操作できる「Kitematic」が意外と便利なので。

Docker Toolboxのインストール:Windows編 – Qiita https://qiita.com/maemori/items/52b1639fba4b1e68fccd

ツールをインストールすると、3つのアイコンが作られる。Docker Quickstart Terminal は通常のコマンドライン、Kitematic (Alpha) が GUI ツールである。

image

Kitematic を起動して Docker Hub のユーザー名とパスワードを入力する。ここから Docker イメージをダウンロードする。

image

あらかじめ、Azure Functions で作った Dockerfile があるので「moomile」で検索する。公開済みの moonmile/sample-azure-docker を Create する

image

うまく起動ができると、80 番ポートで待っている状態になる。このままではアクセスできないので、

image

Docker の 80 番ポートを、8000番にフォワードする。いわゆる –p 80:8000 ということ。

これで、http://192.168.99.100:8000 でアクセスができるようになる。Docker コンテナをリブートしておく。

image

Docker の IP アドレス(192.168.99.100固定なのか?)を確認して、ブラウザから、http://192.168.99.100:8000 でアクセスする。Function App のトップページが開かれている。内部としては、Azure のエミュレータが動いている形になる。

image

Function App の HttpTrigger1 関数を呼び出すためにブラウザで、http://192.168.99.100:8000/api/HttpTrigger1?name=masuda を指定すると「Hello, masuda」で返される。

image

これで、Linux 環境がなくても、自由に Azure Function + Docker が試せますね。本家の Azure Function の Linux&Docker は結構高いので、ローカルで動かせると便利だろうということで。

あとで、サブドメインを使ってポートフォワードする方法を合わせて試してみる。

カテゴリー: Azure, NET Core | Docker Toolbox で Azure Functionsを動かす はコメントを受け付けていません

Azure FunctionsのDocker版をローカルで起動する

Azure Functions で「なでしこ」が動くとか「COBOL」が動くとかという記事があったハズなのだが、これ難しいのでは?とは思っていたが、実は内部で Docker が動いているだけだった、という話。

Azure でサーバーレスなFunction Appを作るときに、現状ではWindows環境にすると「dotnet」と「node」、「java」が選べるようになっている。それぞれ VM 環境を Azure 側で用意してそれの上で動かすのだから、このあたりの環境は Azure = Microsoft 社が用意するんだろうなぁというのが想像できる。

image

もうひとつ、Linux 環境というのがあって、こっちは .NET と JavaScript と python が選べる。Windows のほうに python のがないのは何故?とか PHP は何処で動かすのか?と不思議に思っていたのだが、実は PHP のようなほかの言語は、Linux 環境での「Docker イメージ」のほうに作ることになる。

image

Function App は内部的には App Service と同じなので、つまりは VM 上で動いている。App Service のほうは、HTTP プロトコルの受け口から作るけど、Function App の場合は MVC でいうところの Controller のメソッドが直接呼び出される形になっている、と思えばいい。

となると、この「Docker イメージ」というやつは、

  • 内部で、HTTP サーバーが起動している
  • クライアントHTTP リクエストをすると、内部の MVC の Controller らしきものが起動される。
  • Function Appの「関数」として、static なコードが呼び出される。

というスタイルになればいい。実は「内部の MVC の Controller らしきもの」のように分離されてなくても、単純に HTTP リクエストで呼び出されたときにレスポンスを返す、という通常の HTTP サーバーがあればいいのだ。

Ubuntu で func init してみる

Ubuntu 上で、Function Appを作るために「func init」することができる。このときに docker スイッチを付けると、Dockerfile を作ってくれる。

func init –docker

作成された Dockerfile を見てみると、


FROM microsoft/dotnet:2.1-sdk AS installer-env

COPY . /src/dotnet-function-app
RUN cd /src/dotnet-function-app &amp;&amp; \
mkdir -p /home/site/wwwroot &amp;&amp; \
dotnet publish *.csproj --output /home/site/wwwroot

FROM mcr.microsoft.com/azure-functions/dotnet:2.0
ENV AzureWebJobsScriptRoot=/home/site/wwwroot

COPY --from=installer-env ["/home/site/wwwroot", "/home/site/wwwroot"]

となっている。これは .NET 環境(C#)なのだが、なるほど、dotnet コマンドでビルドして publish しているだけだ。*.csproj なプロジェクトは所謂 ASP.NET MVC プロジェクトみたいなものなので、ここで適当な関数が呼びだされていることになる。

試しに func start host してみる

func start host してみよう。

image

のように、普通に azure function のエミュレータが起動する。つまり、Docker の中身は azure function と同じなわけだ。まあ、同じだからいいんだけど。

docker build して docker run する

Dockerfile があるのだから、ローカルな Linux 環境でも実行ができるはずだ。Ubuntu に docker を入れた状態で、

sudo docker build -t moonmile/azure-docker:ver0.2 .

のようにビルドをすると、ビルドができる。Nuget で関連するアセンブリも持ってくる。

image

次のように docker で実行する。

sudo docker run -it -p 8000:80  moonmile/azure-docker:ver0.2

image

すると、http://localhost:8000/api/HttpTrigger1?name=masuda でアクセスできるようになる。

注意しないといけないのは、関数が呼び出されるときに AuthorizationLevel.Function のままだと、ユーザー名/パスワードを要求されるので(Basic認証なのか?)、AuthorizationLevel.Anonymous に設定して、匿名でもアクセスできるようにする。

自前の Dockerfile が作れる?

そんな訳で、.NET 環境の場合は、AzureWebJobsScriptRoot=/home/site/wwwroot なところでルートが指定されているから、このあたりで外側から指定の関数を呼び出されているんだろうなあ、ということが想像できる。

となると、PHP とか他のプログラム言語で構築する場合も、HTTP リクエスト受け付けるように Dockerfile を作ってやれば、Azure Functionsの中身を「自分で使いやすい言語」に切り替えられるだろう、ってことが分かる。Linux 環境に限られるのだが。

カテゴリー: 開発, Azure | Azure FunctionsのDocker版をローカルで起動する はコメントを受け付けていません

開発環境を仮想OS上に保管する実験

最近の開発環境と言えば、Docker で構築しておいてビルド環境を配布、というのが主流なのだが、Visual Studio 絡みで開発環境を用意するとそうもいかない。.NET Core で作っておけば Docker のコマンドラインで済むのだけど、Visual Studio でビルドということになると(*.sln を呼び出して msbuild しても状況は変わらない?Windows の GUI が必要だから)、なんらかの形で OS ごとまるっと開発環境を残すことになる。

クライアントアプリの開発となると、GUI が必須になるので、

  • 顧客の主たる OS(検証機込み)
  • 開発環境としての Visual Studio
  • 動作環境としての SQL Server 諸々

ということになって、OS/Visual Studio/SQL Server はそれぞれ当時のバージョン込みで用意しておくことになる。顧客の主に動作している OS ってのは、Windows 7 だったり、それ以前だったりすることもあるので、場合によっては、検証機/開発機の2つの環境を残すことになる。クライアントアプリの場合、デバッグ実行で動作確認することが多いので、大抵の場合「検証環境のOS」=「開発環境のOS」となることが多い。古い IE とかを動かす場合は別途検証機をいくつか用意することになる。

ちなみに IE の検証環境は、https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/ から、各 IE が入った仮想環境をダウンロードできる。動作チェックをしたい場合は、ここから逐次ダウンロードすればよいだろう。たぶん、将来的にも残っていくものと思われる。

どの仮想環境を使うか?

いくつかの仮想環境があるが、

  • Hyper-V
  • VMWare Player
  • QEMU

を検討してみた。VirtualBox もあるのだけど、これはちょっと慣れていなくて試していない。主に VMWare Workstation で仮想環境を構築してきたので、こっちのほうが慣れているというのもある。

Windows 10 上で構築するので、Hyper-V のほうが良さそうに見えるのだが、実は Hyper-V で開発環境を残してしまうと、

  • Linux をホストにできない
  • Android のエミュレータと競合しがち

という欠点がある。Android エミュレータのほうは、Hyper-V のほうを使えばいいじゃないか?という話もあるのだが、ホストが Windows に固定されてしまうのが微妙に痛い。手元で主に VM 環境を動かしているのが Ubuntu ということもあり(Windows Update のリブートに嫌気がさしてしまったので)、Linux でホストするという点も考慮しておきたい。もっとも、最近では、Hyper-V の vhdx なファイルを VMWare の vmdk にしてくれるツール StarWind V2V Converter もあるので、Hyper-V で作成しておいて、後からコンバートするという方法もある。コンバートツールには、QEMU-img を使う方法もある。

そんな訳で、Windows 上の Hyper-V じゃなくて、VMWare の Workstation Player を使って構築してみたのだが、さて、この VMWare をお客の環境に入れていいものかどうか?(最終的には、開発環境自体をお客自身が管理できる状態にしておきたい)というのが疑問になった。顧客のサーバー環境の片隅にでも、開発環境が VM 上で動いていれば、それはそれで便利なのだ。

建前上ではあるけれど、受託開発の場合は開発が済んだら一切合切を、開発会社のPC から消し去ってしまうのだ。仕様書とか試験のデータとかも、ISO9000的な保管の意味からすると、漏洩リスクを持つよりも「消して」しまうのが一番よい。実際、契約上も仕事が終わったら「消す」ことになっている。だが、実際のところは、瑕疵とか修正とかの問題があって、開発環境諸々を PC に残すことになる。不具合の発生や要求が発生するたびに、お客に開発環境を要求するのも大変だし、再び開発環境を整えるのは大変だ。だから、建前上は「消した」ことになっているのだが、なんらかの形で開発環境は残る。かなり矛盾しているが、仕事の効率上仕方がないところがある。

だが、できることならば「消す」なり、なんらかの形でバックアップで保存という形にしておきたい。大容量の HDD の片隅(場合によっては社内の大量ストレージ用のサーバー)の置くのもいいのだけど、バックアップに時間がかかるし、そもそも HDD が故障したときにそれらの開発環境がごっそり消えてしまうことになる。本来ならば「消した」ものであっても、実際改修などが起これば消してしまうと不便なわけで、じゃあ、何処に残しておけばよいか?という問題が残ってくる。

なので、残しておくは、お客さんの運用環境のどこかに置いておくのがベターだ。保守契約を結んでいれば、なんらかの社内ストレージに残しておけばよいのだが、とびとびの改修案件ということになると、継続的なバックアップ環境は顧客自身が持っておくほうがよい。かつては、実機 PC で保存しておくのがよいのだが、じゃあ、仮想環境においておくのがよいのでは?と思っている。

このとき、運用環境をできるだけ弄らないで、開発環境を仮想化しておきたい。となると、運用環境に VMWare のソフトウェアを入れるのはどうなのか?って話になる。固いこと言わなければまあいいじゃん、ってことになるけど、ちょっと考えてみる。

なので、QEMU ではどうか?ってのを思いついた。ストレージも顧客サーバーの HDD/SSD 上に置くのではなくて、USB メモリに置けばいいのではないか?QEMU だったらソフトウェアのエミュレートなので、ソフトウェアを Windows に入れなくて済む。必要なときは、USB メモリを差して使えばいいのでは?と考えてみた。

SD カードのアクセススピード

実は、いきなり USB メモリ(SD カード)上に VMWare の仮想ファイルを作ってアクセスしたのだが、条件によっては実用に耐えない。どうも、やたらに遅いので CrystalDiskMark を使ってアクセススピードを確認してみる。

上から、SSD, HDD, USB2.0+SDカード, USB3.0+SDカードの順になる。実際に利用したいのは「USB3.0+SDカード」のパターンで、高速な USB3.0 ならば安価なSDカード(USBメモリ)でも HDD の変わりになるのではないか?と思ったからだ。SD カードは東芝の128GBを購入して確認している。値段が3,000円弱なので、高価なSSDを買うより断然安く済む。

SSD TS480G
image

HDD WD20EZRX
image

Toshiba UHS-I microSDXC 128GB + ELECOM USB 2.0 MR-K011
image

Toshiba UHS-I microSDXC 128GB + iBUFFALO USB 3.0 BSCR27U3BK
image

結果を見るとわかるのだが、

  • HDD WD20EZRX
  • Toshiba UHS-I microSDXC 128GB + iBUFFALO USB 3.0 BSCR27U3BK

が、ほぼ同じになる。ということは、通常の物理HDD に仮想ストレージを用意したときと同じぐらいのスピードが SDカード+USB3.0 の組み合わせでは出るのではないか?という想定である。ただし、SDカードのほうが、シーケンシャルの書き込み(Seq Q32T1の Writeのほう)が5倍位遅いので、なんらかの大量書き込みをしたときは遅い、という予想が立つ。

実際、SDカードに仮想領域を作って VMWare Player で動かしてみると、それなりに Windows 10 が立ち上がってくる。SSD に仮想領域を作ったときよりは遅いが、まあ、なんとかなるという程度だ。しかし、Visual Studio 2017 を起動したときが非常に遅い。起動しきれば、なんとかなるのだが、起動が相当遅い。これは、仮想に割り当てるメモリを4GB程度にしても変わらない(メモリを2GBにすると、仮想上の Windows 10 の起動自体がかなり危うくて遅い)。あと、VMWare をサスペンドするときにもかなり遅い。これは、シーケンシャルな書き込みが HDD よりも SDカードのほうがずっと遅いことが原因である。Visual Studio の起動が遅いのは、おそらくスワップファイルの書き込みが大量発生しているからだろう。ビルド自体は問題ない(小さなファイルの書き込みなので)ので、スワップファイルのような大きなファイルの書き込みに遅さが際立つと思われる。

VMWare Player で作るファイルは一括ファイルと分割ファイルがあるのだが、SD カード上では「一括ファイル」じゃないと重くて動かない。どうやら、分割ファイルだと読み込みを遅い領域に入ってしまうらしく使い物にならない。

そんな訳で、SD カード時に仮想な Windows 10 を入れるのはちょっと大変、っぽいことが分かる。ちなみに、Windows 10 + Visual Studio + SQL Server + Excel の組み合わせだと、使用済みの容量が32GB 程度で収まる。先行きの Windows Update 等を考えても 64 GB あれば十分だろう。これが、小さめのノートPC(Surface も含む)に Visual Studio 入れられない理由でもある。Visual Studio Code ならばなんとかなるけど。

ラズパイの場合、Raspbian が SD カードに入るわけだが、Linux から大量書き込みが発生すると OS が重たくなるのはこの現象だろう。Linux の場合は、アクセス方法がちょっと違うかもしれないが。

あと、試しに Windows 7 で開発環境を SD カード上に作ってみたが、比較的 Winodws 10 よりも軽くなる。これは、Windows 10 が高速な SSD に最適化されているのだが、Windows 7 は低速な HDD に最適化されていた、からだろうと思う。HDD が遅めの場合は、あえて Windows 7 上に開発環境を構築するのも良いかもしれない。

QEMU で Windows の開発環境を動かせるか?

VMWare Player だと顧客PCに対して「インストール」が必要になるので、どの PC でも良い、という訳にはいかなくなる。そうなると、インストール不要な QEMU を使ったらいいんじゃないか?と思って考えてみた。

無料のWindows仮想マシンをQEMUで動かす – Qiita https://qiita.com/Hiroki_Kawakami/items/b18f6c50df4ce215e40d

ここの記事を参考にすると、qemu 上でも Winodws 10 を動かせそうな感じではあるんだが、どうなのか?と思って、VMWare で作成した vmdk を変換して使ってみたのだが…結論から言えば使い物にならなかった。

  • ゲスト OS の Windows 10 から、外部のサーバー等が見えない。
  • 何故か、ゲスト OS 内で何か操作(ビルドとか)をした後に、2回目の起動をすると無反応になってしまう。

外部サーバー見えない問題は、ネットワークがホストのPCしか参照できない仕組みになっているので、QEMU で Raspberry Pi をエミュレートする でやったように、-net nic –net tapi,ifname=TAP32 のようにブリッジ接続を設定すればよいはずなのだが、どうもこれを指定した時点で起動できなくなってしまう。また、なんか操作をしたあとに、もう一度立ち上げると、ハングアップしてしまうのは致命的だ。そのほかにもマウスポインタの位置がずれたままで操作できなくなってしまう。たぶん、ゲスト OS の相性の問題になるのだと思うけど、開発環境を残す意味として「途中でなんかの原因で立ち上がらなくなってしまう」のは非常に困るので、この件に関しては QEMU は却下する。

VMWare 上から CrystalDiskMark してみる

Windows 10 の場合
image

Windows 7の場合
image

USB 3.0のSDカード上にあるVMファイルを使って起動して、Windows 10/7 でベンチマークを取ってみる。驚いたことに、SSD へのアクセス並みにアクセススピードが出ている。これが本当にスピードが上がったのか、計算上だけの問題なのかはわからないが、ゲスト OS 内から見たら HDD アクセススピードは結構なスピードでアクセスできる(というように見えるだけ?)ということになる。

ただし、VMWare 外から見たアクセススピードは下記のように断然遅いので、サスペンドのときとかは十分注意する必要がある。

Toshiba UHS-I microSDXC 128GB + iBUFFALO USB 3.0 BSCR27U3BK
image

結論

そんな訳で、現在の私の開発環境からいくと、

  • 顧客サーバーに VMWare Player をインストールする
  • 顧客サーバーの HDD に仮想ファイル64GBを置いて使う

のが開発環境を顧客の環境で動かすときの最適値らしい。USB 3.0 にメモリを差しこんで、開発環境を別に作ることもできるが、必要な仮想HDDの容量が、64GB 程度で済むならばそれでいいだろう。逆に言えば、安価なノートPCに開発環境を作るとき、ストレージが64GB程度あれば、Visual Studio+SQL Serverなどを入れた .NETの開発環境が作れるというわけだ。32GB 程度しかストレージがないときは、食いつぶしてしまうので駄目。なんらかの形で、SD カードに追い出さないといけない。

安価なノートPCで、DockerとかをSDカードに追い出した場合はどうなるのだろう。他にも、QEMU な環境でも Linux の場合だったら比較的動くのかもしれない。これは後で試してみよう。

カテゴリー: 開発 | 開発環境を仮想OS上に保管する実験 はコメントを受け付けていません

Azure Function から Redmine の API を呼び出す

サーバーレスな Azure Functions の練習がてら、Redmine の API を呼び出してみるテストを晒しておきます。Azure Functions の詳しい説明は、https://docs.microsoft.com/ja-jp/azure/azure-functions/ を参考にしてもらうとして、一番手軽なのは HttpTrigger である。普通の Web API と同じように URL アドレス経由で呼び出して、指定のファンクションを実行する。「ファンクション」とは言っても、F# とか Scala のような「関数型」のファンクションではないのだけど、ある意味「ステートレス」ということでは、AWS の Lambda もファンクションだし、Azure Functions もファンクションなので、「関数」って訳語が付けられているのだが。果たしてこれは、正しいのか?ってのは微妙だ。

ローカルにFunction Appを作る

Function Appは、Azure 上で作って運用するのだけど、実はローカルなPC上でもAzure Functionsを実行できる。

Azure Functions Core Tools のインストール
https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-run-local

をインストールすると、ローカルPCでテスト実行ができるようになる。
実は、このツールは、Visual Studio でAzure Functionsのプロジェクトを作って実行すると、自動でインストールされる。

ちょうど、ASP.NET Core MVC のアプリをテスト実行したときと同じように、.NET Core 上のコマンドが動いてAzure Functionsのエミュレータが起動する。

ファンクションを作る

関数の作り方は非常に簡単だ。ちょうど、ASP.NET MVC の Controller にメソッドを作るようにファンクションを作っていく。ASP.NET MVC の Controller と違うのは、static クラスに static メソッドとして定義されているところだ。
ASP.NET MVC の場合も HTTPプロトコルでステートレスなのだから、意味あいとしは同じと言えば同じなのだが。ひとまず、Azure側からstatic関数として呼び出されることになる。だから「ステートレス」っていのが明確になっている。

public class App
{
    public const string ApiKey = &quot;<api_key>&quot;
    public const string BaseUrl = &quot;<redmine_url>&quot;;
}

public static class ProjectFunc
{
    private static string ApiKey = App.ApiKey;
    private static string BaseUrl = App.BaseUrl;
    private static HttpClient client = new HttpClient();
    private static int _count = 0;  // 呼び出しカウンタ

    /// <summary>
    /// プロジェクトリストを取得
    /// </summary>
    /// <param name=&quot;req&quot;></param>
    /// <param name=&quot;log&quot;></param>
    /// <returns></returns>
    [FunctionName(&quot;GetProjects&quot;)]
    public static IActionResult GetList(
        [HttpTrigger(AuthorizationLevel.Function, &quot;get&quot;, &quot;post&quot;, Route = &quot;Project/List&quot;)]
            HttpRequest req, TraceWriter log)
    {
        var json = client.GetStringAsync($&quot;{BaseUrl}/projects.json?key={ApiKey}&quot;).Result;
        _count++;
        log.Info($&quot;count: {_count}&quot;);
        var data = JsonConvert.DeserializeObject<ProjectList>(json);
        foreach ( var proj in data.projects )
        {
            log.Info($&quot;{proj.id}: {proj.name}&quot;);
        }
        return new OkObjectResult(json);
    }

    [FunctionName(&quot;GetProject&quot;)]
    public static IActionResult Get(
        [HttpTrigger(AuthorizationLevel.Function, &quot;get&quot;, &quot;post&quot;, Route = &quot;Project/Get&quot;)]
            HttpRequest req, TraceWriter log)
    {
        string id = req.Query[&quot;id&quot;];
        var json = client.GetStringAsync($&quot;{BaseUrl}/projects/{id}.json?key={ApiKey}&quot;).Result;
        var proj = JsonConvert.DeserializeObject<Project>(json);
        log.Info($&quot;{proj.id}: {proj.name}&quot;);

        return new OkObjectResult(json);
    }
}

HttpTrigger は URLアドレス経由で呼び出されるので、URLはFunction Appで使われる API KEY を含めて、

https://sample-azfunc-dotnet.azurewebsites.net/api/HttpTrigger1?code=xxxxxxxx

な形で呼び出されるのだが、ここではローカルのテスト環境なのでcodeを省略して、

http://localhost:7071/api/Project/List

のように呼び出すことができる。
実は、呼び出すときの関数名は FunctionName 属性で設定するのだけど、この属性でフォルダーを掘ることができない。引数についている HttpTrigger.Route にフォルダー付きの呼び出しを付けられるので、これを使うと Project/List とか Project/Get?id=100 のような Web API っぽい形に変えられる。

Redmine の JSON をクラスにする

Redmine を API で呼び出すと、JSON形式あるいはXML形式で受け取ることができる。以前は、これを手作業でクラスに直していた(コンバーターも手で作ってた)のだが、Visual Studio で「編集」→「形式を選択して貼り付け」を使うと、元のJSON形式のデータから、
C#のクラスに直すことができる。

ルートのノードが「Rootobject」となるので、適宜「ProjectList」とか「ProjectItem」とかに直してやる。
これと、JsonConvert.DeserializeObject メソッドを組み合わせると、JSON形式のデータを一気にクラスに直せる。

実行してみる

Visual Studio 上でデバッグ実行をして、

ブラウザから、http://localhost:7071/api/Project/List を実行すると、別途 Redmine が動いているサーバーに接続して、JSON形式で受け取ることができる。

各種 WEB API の呼び出し形式がまちまちだったりするので、こんな風にFunction Appを使って、統一的にアクセスできるようにコンバートすると GUI 側の変更が楽になるのではないかな、と試作&思索しているところ。

カテゴリー: 開発, Azure Functions, C# | Azure Function から Redmine の API を呼び出す はコメントを受け付けていません

ClosedXMLとMVVMパターンを良好な関係にしてみる

MVVMパターンを考えるとき、ViewはXAMLとかGUIだろうという固定観点があるが、実はそうではない。と思うのだがどうだろうか?いわゆる、ASP.NET MVC の View にWeb APIを割り当てると View ってのは XML や JSON になる。クライアントから見れば XMLやJSON はデータだが、MVCパターンで作られたWebアプリにしてみれば、XMLやJSONをViewに見立てることができる。実際、Web APIを作るとき複雑なXMLを返す場合は、Viewの機能を使ったほうが楽だったりする。

ClosedXMLをViewに見立てる

CloseXMLを使って、Excelに値を書き込もうとするとき Cell(row,column)あるはCell(“A1”)を使うわけだけど、ここの書き込み先をXAMLのバインドのように書けないだろうか?ってのを思いついた。

で、ClosedXML.Report https://github.com/ClosedXML/ClosedXML.Report はテンプレート用のExcelを作っておいて、あらかじめ名前を付けておく。

確かに、こんな風に Excel に名前を付けておいて、そこを目指して書き込むのもいいんんだが、なんらかのマークを書いておかなければいけないのが微妙な感じがする。実は、XAML も {Binding propName} という形で XAML 自身に記述するので似たようなことをやっているのだが。

これ、マークを View のほうに書いているけど、じゃあ ViewModel のほうに書くのはどうか?とおもってみたのがこれ。

テンプレートとなるExcelはふつうに記述しておく。

書き込んだ結果はこっち

Excelへの書き込み位置は、ViewModelに属性として記述する。

public class ViewModel : ObservableObject
{
    private string _Name, _Address;
    private string _ModifiedDate;

    [ExcelBinding(Address: &quot;B2&quot;, Property: &quot;Value&quot;)]
    public string Name { get => _Name; set => SetProperty(ref _Name, value, nameof(Name)); }
    [ExcelBinding(Address: &quot;B3&quot;, Property: &quot;Value&quot;)]
    public string Address { get => _Address; set => SetProperty(ref _Address, value, nameof(Address)); }
    // 更新日時
    [ExcelBinding(Address: &quot;A4&quot;, Property: &quot;Value&quot;)]
    public string ModifiedDate { get => _ModifiedDate; set => SetProperty(ref _ModifiedDate, value, nameof(ModifiedDate)); }
}

ViewModelからClosedXMLを通してExcelに書き込むコードはこんな感じ。

ExcelBind _bind;
ViewModel _vm;
public void LoadExcel()
{
    string tableName = &quot;Personal&quot;;
    string path = @&quot;C:\Users\masuda\Documents\サンプルテンプレート.xlsx&quot;;
    using (var wb = new XLWorkbook(path))
    {
        var sh = wb.Worksheets.FirstOrDefault(t => t.Name == tableName);
        _bind = new ExcelBind(sh);
        _vm = new ViewModel();
        _bind.DataContext = _vm;

        _vm.Name = &quot;Tomoaki Masuda&quot;;
        _vm.Address = &quot;Itabash-ku Tokyo in Japan&quot;;
        _vm.ModifiedDate = DateTime.Now.ToString();     // 更新日を表示
        wb.SaveAs(@&quot;C:\Users\masuda\Documents\sample_output.xlsx&quot;);
    }
}

当然のことながら、ClosedXML にはバインディング処理はないので、ClosedXMLのXLWorkbookとIXLWorksheetをラップするような橋渡しのExcelBindクラスを作る。ExcelBindクラスにはMVVMパターンのDataContextプロパティを持っているので、これにバインドする。

ViewModel の各種プロパティに設定をすると INotifyPropertyChanged インタフェースを使って、View に見立てられた ClosedXML に値を書き込むという仕組み。ClosedXML は入力インターフェースを持たないので、ClosedXML からの TextChenge などのイベント受信は不要(実は自動計算などがあるから、無いわけではないのだが)というわけだ。
ViewModel クラスは普通の MVVM パターンと同じようにプロパティに値を設定すればよい。

内部実装

ざっと内部的な実装を

目的のセルを設定するExcelBinding属性

“A1″や行列で指定する。将来的にはセルの名前を設定できるのもよいだろう。

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class ExcelBindingAttribute : Attribute
{
    public string SheetName { get; }
    public string Address { get; }
    public int Row { get; }
    public int Column { get; }
    public string PropertyName { get; }
    public ExcelBindingAttribute(string Sheet, string Address, string Property )
    {
        this.SheetName = Sheet;
        this.Address = Address;
        this.PropertyName = Property;
    }
    public ExcelBindingAttribute(string Sheet, int Row, int Column, string Property )
    {
        this.SheetName = Sheet;
        this.Row = Row;
        this.Column = Column;
        this.PropertyName = Property;
    }
    public ExcelBindingAttribute(string Address, string Property = "Value") 
    {
        this.SheetName = "";
        this.Address = Address;
        this.PropertyName = Property;
    }
    public ExcelBindingAttribute(int Row, int Column, string Property = "Value")
    {
        this.SheetName = "";
        this.Row = Row;
        this.Column = Column;
        this.PropertyName = Property;
    }
}

橋渡しをする ExcelBind クラス

内部的にClosedXMLのXLWorkbookかIXLWorksheetを持たせる。DataContextプロパティに設定した ViewModel の PropertyChanged イベントをフックして、ClosedXML 経由で目的のセルの値を変更する。プロパティは、簡易のため Value のみに固定している。

GetDataContext メソッドは、逆向きで Excel シートから ViewModel を作るときに使う。

public class ExcelBind
{
    private XLWorkbook _workbook;
    private IXLWorksheet _worksheet;

    public ExcelBind(XLWorkbook book) { _workbook = book; }
    public ExcelBind(IXLWorksheet sheet) { _worksheet = sheet; }


    private object _DataContext = null;
    public object DataContext
    {
        get => _DataContext;
        set
        {
            if (_DataContext != value)
            {
                _DataContext = value;
                if ( _DataContext is INotifyPropertyChanged )
                    (_DataContext as INotifyPropertyChanged).PropertyChanged += _DataContext_PropertyChanged;
            }
        }
    }
    private void _DataContext_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (_DataContext == null) return;
        // 1.ViewModelのプロパティ名からリフレクションで値を取得
        var pi = _DataContext.GetType().GetProperty(e.PropertyName);
        if (pi == null) return;
        // 2.属性から設定先のClosedXMLのプロパティを取得
        var attr = Attribute.GetCustomAttribute(pi, typeof(ExcelBindingAttribute)) as ExcelBindingAttribute;
        // 3.設定先にリフレクションで値を代入
        try
        {
            if (string.IsNullOrEmpty(attr.Address))
            {
                // Row,Column 指定の場合
                if (_worksheet != null)
                    _worksheet.Cell(attr.Row, attr.Column).Value = pi.GetValue(_DataContext);
                if (_workbook != null && _workbook.Worksheet(attr.SheetName) != null)
                    _workbook.Worksheet(attr.SheetName).Cell(attr.Row, attr.Column).Value = pi.GetValue(_DataContext);
            }
            else
            {
                // Address 指定の場合
                if (_worksheet != null)
                    _worksheet.Cell(attr.Address).Value = pi.GetValue(_DataContext);
                if (_workbook != null && _workbook.Worksheet(attr.SheetName) != null)
                    _workbook.Worksheet(attr.SheetName).Cell(attr.Address).Value = pi.GetValue(_DataContext);
            }
        }
        catch { } // 例外は無視する
    }

    public void SetDataContext(object context) { _DataContext = context ; }
    public T GetDataContext<T>( T o = null ) where T : class, new() {
        if ( o == null ) o = new T();
        // バインド先のデータを workbook/worksheet から取り出す
        // 1.プロパティ一覧を取得
        var props = o.GetType().GetProperties();
        foreach ( var pi in props )
        {
            // 2.ExcelBindingAttribute属性がついているプロパティを取り出す
            var attr = Attribute.GetCustomAttribute(pi, typeof(ExcelBindingAttribute)) as ExcelBindingAttribute;
            if ( attr != null )
            {
                IXLWorksheet sh = _worksheet;
                if ( sh == null )
                {
                    if (_workbook != null && _workbook.Worksheet(attr.SheetName) != null)
                        sh = _workbook.Worksheet(attr.SheetName);
                }
                if ( sh == null ) { return o; }

                // 4.ViewModel のプロパティに値を設定する
                if (string.IsNullOrEmpty(attr.Address))
                {
                    pi.SetValue(o, sh.Cell(attr.Row, attr.Column).Value);
                }
                else
                {
                    pi.SetValue(o, sh.Cell(attr.Address).Value.ToString());
                }
            }
        }
        return o;
    }
}

逆に Excel シートから ViewModel を構成する

ExcelBindのGetDataContextメソッドを使って、ViewModelを構成する例。これを使うと、Excel シートに記入してもらって、そこから値を取り出すのが楽にできるのでは?と考えたりする。

private void clickGetFromExcel(object sender, RoutedEventArgs e)
{
    string tableName = &quot;Personal&quot;;
    string path = @&quot;C:\Users\masuda\Documents\sample_output.xlsx&quot;;
    using (var wb = new XLWorkbook(path))
    {
        var sh = wb.Worksheets.FirstOrDefault(t => t.Name == tableName);
        _bind = new ExcelBind(sh);
        // ViewModel を再構成
        var vm = _bind.GetDataContext<ViewModel>();

        System.Diagnostics.Debug.WriteLine(vm.Name);
        System.Diagnostics.Debug.WriteLine(vm.Address);
        System.Diagnostics.Debug.WriteLine(vm.ModifiedDate);

        MessageBox.Show(vm.Name);
    }
}

ViewModelからPropertyChangedを受ければViewになるのか?

直接 ClosedXMLに渡すわけではないが、ExcelBindクラスを媒介して値のやり取りができている。WPFやUWPのようなユーザーインタフェースはないが、ViewModelクラスのプロパティを使ってView(この場合はExcelシート)にアクセスできている。
この部分、構造が簡単な XML ならばデータとして扱うのだが、ExcelのOpenXML形式のように複雑怪奇なXMLの場合、ピンポイントで修正することを考えると「View」として扱うのがベターではないかと思っている。
ViewModelのプロパティは、別途DIなどでXMLやJSONから読み込むようにすれば、消えてしまった InfoPath のようにできるのではないな、と。

このあたりを少し整理して、NuGet に挙げられるようにする予定。

カテゴリー: 開発, C# | ClosedXMLとMVVMパターンを良好な関係にしてみる はコメントを受け付けていません

ClosedXMLを使って、超高速にリスト形式の帳票を作成する

xlsx 形式な Excel ファイルを高速に読み込めたということは、ひょっとして高速に書き込めるのでは?と思って書いてみたのがこれ。
あらかじめ Excel で作ったテンプレートを用意しおいて、行を追加しているだけ。行数分コピーしているのは3行目のセル/行に書式が設定してあるから。こうしておくと罫線とか色とかフォントとかをコードで指定しなくて済む。

public void 印刷()
{
    var path = AppDomain.CurrentDomain.BaseDirectory + @&quot;template\協力会社一覧.xlsx&quot;;
    using (var wb = new XLWorkbook(path))
    {
        var sh = wb.Worksheets.First();
        // あらかじめ行数分用意しておく
        var rg = sh.Row(3);
        for (int i = 0; i < this.Items.Count - 1; i++)
        {
            rg.CopyTo(sh.Row(i + 4));
        }

        int r = 2;
        foreach (var it in this.Items)
        {
            sh.Cell(r, 1).Value = it.業者ID;
            sh.Cell(r, 2).Value = it.業者名称;
            sh.Cell(r, 3).Value = it.担当者;
            sh.Cell(r, 4).Value = it.役職;
			// 途中は省略
            r++;
        }
        // テンポラリファイルに保存
        var temp = System.IO.Path.GetTempFileName() + &quot;.xlsx&quot;;
        wb.SaveAs(temp);
        System.Diagnostics.Debug.WriteLine(temp);
        // テンポラリファイルを開いて印刷
        var xapp = new Excel.Application();
        var xwb = xapp.Workbooks.Open(temp);
        var xsh = xwb.Worksheets[1] as Excel.Worksheet;
        xsh.PrintOutEx();
        xwb.Close(false);
        xapp.Quit();
        System.IO.File.Delete(temp);
    }
}

どうやら、ClosedXML には印刷機能がないらしいので、印刷のほうは Microsoft.Office.Interop.Excel を呼び出している。それでもスピードは体感で10倍以上にはなる。書き込みも相当スピードアップされるらしい。
実はレポート出力用に ClosedXML.Report https://github.com/ClosedXML/ClosedXML.Report というのもあるのだが、行単位の一覧程度ならばこの方式で十分だろう。表形式じゃないレポート形式の場合は、別途変換してみよう。

実行例

テンプレート用の Excel ファイルはこんな感じで1行だけ作っておく。フォントの設定とか文字列の折り返し、罫線などをあらかじめ Excel 上で設定してあるので、3行目を CopyTo するだけでよい.

これを Excel COM を使って印刷する。OpenXML/ClosedXML が印刷機能を持っていればよいのだが、どうやら XML の読み書きの機能だけでレンダリングはないらしい。実際は PDF に落とせばよい(PDFから印刷する方法もあるので)ので、iText とかの PDF 出力を使えばよいらしいのだが、まあ、印刷はプリンタ独自の設定も含むことがあるので Excel COM を使ったほうがよいだろう。

Windows 10 で印刷した結果がこちら。Excel からの出力先を PDF にすると手軽に PDF ファイルの落とせる(Excel のエクスポート機能を使ってもよい)。

帳票は Excel 形式で残したほうがよい場合と、Excel 形式のような「編集できる形式」では残してはいけない場合がある。編集不可にしたいときは PDF にするのが常なのだが、これは税務処理などで金額の修正があると困る場合によく使われる。Excel 形式のまま残すと改竄されてしまうので、わざわざ PDF で残すのだ。もっとも、詳しい人ならば暗号化していないと PDF の内部で修正が出来てしまうのだが…まあ、一般的には「PDF だと修正できない」ので大丈夫と思ってよい。きちんとやる場合は PDF に暗号をかけるか、Excel 形式のままハッシュ値を保存して暗号化(いわゆるブロックチェーンな方法)をとればよい。

 

カテゴリー: C# | ClosedXMLを使って、超高速にリスト形式の帳票を作成する はコメントを受け付けていません

MVVM パターンでリモート操作の設計を考察

以前から考えていることに、MVVM パターンの ViewModel を使った遠隔操作がある。そもそも MV* パターン(MVVM とか MVC とか)の場合、View を独立させることができるので、View から離れている ViewModel や Model は複数の View に対応していてもよいだろう、という発想がある。Visual Studio のプロジェクトテンプレートだと、ひとつの View に対してひとつの ViewModel が対応するように作られるけど。まあ、それはそれとして。

image

MVVM パターンの View が GUI 以外にあるのだろうか?と考えてみたのだが、そのほかはないだろう。MVC パターンを使って Web API を作ることはよくあるけど、Web API や RESTful 程度に整理されてしまうと、XAML の入れ子の部分とか複雑怪奇なリストが作れるとかの意味がなくなってしまう。データストアとしての XAML と使う以外の場合、データバインドやイベント/Command パターンを使う場合は、もっとランダムにアクターからのイベントが発生するときに XAML の構造が力を発揮する。アクセス方法が簡単あるいは整理されているならば、わざわざ View からイベントを取る必要がない(イベント駆動ではないのだから)。

というわけで、ひとまずランダムかつインタラクティブな入力としての View/XAML を考えるうえで、GUI を対象にする(インタラクティブなものとして、複数のセンサー等も考えてみたけど、それも XAML で扱うほど複雑ではない気がする)。

image

逆に言えば、人が手入力をして、人の目で確認して、再び人が手入力するという手順は、きわめて「遅い」手段である。例えば、ロボット入力(RPAを含む)ならば、多少回りくどい手順を追加しても確実に入力できる方法を取ることができる。だが、人力の場合は、間違いがあり間違いを目で確かめて、再入力するという手間がかかる。これこそが GUI の特徴でもある…ということにしておこう。

XAML を動的入れ替え&VMのリモート化

動的に XAML が入れ替えられると仮定しよう(実際、WPFの場合、XamlReaderを使うと入れ替えられる)。動的に XAML を生成しなくても、画面遷移の中で View を切り替える場合も同じ方法になる。

image

ひとつの VM に対して、複数の View を持つ。ここの利点は、

  • 後から XAML を入れ替えることができる(動的に更新が可能)
  • 画面遷移のときに、複数の View のメモリを共有している

というものがある。動的入れ替えのほうは、ブラウザから Webサーバーにアクセスする Web アプリケーションに似ている。ただし、Web アプリの場合は、内部データをサーバー側のセッションに持つことが多いのだが、ここでは VM 上に持たせる。SignalR はこれを JavaScript で実現したものだ。表面上の View はいわゆる「ユーザーインターフェース」なので頻繁に変わることがある。ユーザーによるカスタマイズもあるし、見た目を変えたバージョンアップもある。View > ViewModel > Model の順に更新頻度が変わるのがベターだ。

そして、ViewModel 同士をインターネット回線で通信させる。従来ならば View にべったりくっついた ViewModel を作るところだが、本来の MVVM パターンを意識するならば、クライアント/サーバーの2つの ViewModel に分離することが可能なはずだ。通信回線が遅いという前提もあるのだが、ここではそれなりに早い回線であると仮定しよう。

幸いにしてかつここが MVVM パターンの最大の特徴なのだが、View と ViewModel とのインターフェースは、INotifyPropertyChanged と ICommand を実装する(面倒なので、ICommand のほうは省いてしまうことが多いけど)。この部分の実装は定型のパターンがあるのだけど、実際は独自に作っても良い。このインターフェースを利用して VM 同士の相互通信を可能にすればよい。アスペクト指向の変型判と言っても良い。

XAML の動的入れ替えのみ

話をもうちょっと簡単にするために、要素をひとつずつみてみよう。

image

たとえば、同じ ViewModel を複数の View/XAML で共有するパターンを考える。この場合、画面遷移をおこなっても同じ VM を共有するので内部的なコピーが発生しない。View のコンストラクタに対して ViewModel のオブジェクトを引き渡すだけでよい。通常は引き渡すときに ID のみとかを渡すところなのだが、List-Detail の画面遷移だと、List 側に既に Detail の情報を持っていることが多い。内部的に Detail の Item だけを渡してもいいのだけど、新規作成や削除などで元の List を変更することを考えれば、List 自体を引き渡してしまってもよい。当然、画面から戻ってきたときに追加/削除することもできるけど、Xamarin.Forms や UWP の場合にはダイアログ形式で遷移するか画面を切り替えるかで作りが違うので、ViewModel の動きを統一したいときには、モーダル/モードレス関係なく遷移先に子画面で List の更新をしてしまったほうが問題が少なかったりする。

となれば、動的に「未知な」 View/XAML を切り替えたときに、ひとつの ViewModel で対応するにはどうしたらいいだろうか?という問題がある。

  • 未知な View に対して、常に ViewModel を更新する
  • 未知な View のエラー部分は、ViewModel 側で無視する

という2つのパターンがある。前者のほうは正統ではあるのだが、頻繁に更新されうる View に対して頻繁に ViewModel を更新するのはあまり適切ではない。現在の ViewModel にあわせて View を構成するのも筋ではあるのだが(きちんと制御された View ならそうだろう)。

ならば、ちょっと緩い形で実装された View(間違いがある HTML 形式のようなものだ)に対して、緩く ViewModel が対応してくれてもよい。なので、例外が出てもフェールセーフで動いてくれる ViewModel が必要になるし、動的切り替えの XamlReader では例外が出ないことが望ましい。現状の XamlReader では、プロパティやイベント先がないと例外が発生してしまうので、この用途には向かない。

複数の VM を複数クライアントで共有する

MVVM パターンの ViewModel が、クライアントとサーバーで分離できると仮定するならば、別々の PC で動いている View を手軽に共有できるだろう。この共有度ぐあいはネットワークのスピードに依るのだが、プロパティの変更イベントと View への操作のコマンド制御が通れば、うまくいくと思われる。

image

このように分離していくと、View 内のコード(いわゆるコードビハイド)の意味が明確になってくる。

  • 特定の View 内で閉じるものは、コードビハイドでよい。
  • Model や他PC に影響を与えるもの/与えられるものは、ViewModel に記述する

という使い分けになる。

例えば、画面をクリックしたときに色を変えるとか、数値のフォーマットを変えるとかを XAML の Converter を作ることが多いのだが、これをコードビハイドで実装しても問題ないことがわかる。ひとつの PC 上のひとつの View で閉じているからだ。実装的には、XAML に付随させるほうがよいので JavaScript のような View に埋め込まれるスクリプト言語を使ったほうが便利だろう(アセンブリの配布は少々面倒なので)。逆に、Model に影響を与えるようなロジックは ViewModel に実装せざるを得ず、この部分は通信を含めたコードになる。

異なる PC で View を同期させるためには、サーバー側の VM にはブロードキャスト的な通信が必要になる。このブロードキャストは、INotifyPropertyChanged のインプリメントとして書かれるだろうから、利用時に強く意識されることはないはずだ。このあたりはアスペクト指向的に属性で記述することもできれば、なんらかの実装クラスを継承して使うことになる。少なくとも、本来の ViewModel の実装と、通信を行うための実装を分離する必要がある。

その先は

ひとまず、思考実験はここまで。XamlReader の再実装のほうは、.NET Core 化された WPF のコードを利用すればいいと思う。以前 Xamarin.Forms で試してみて例外をトラップするところまではできたので。リモート可能な ViewModel のほうは、アスペクト指向のように属性でやってみたいところだけど、このあたりは仮コードを作ってみることにしよう。

カテゴリー: 設計 | MVVM パターンでリモート操作の設計を考察 はコメントを受け付けていません