20200814のC#に関する記事は12件です。

ASP.NET Coreへの同時アクセス時の応答が遅くなるのはスレッドプールの挙動のため

はじめに

ASP.NET Coreへの同時アクセスの応答が妙に遅くなるので原因調査したら、スレッドプールの起動の問題、もっと言えば.NET環境での非同期処理の使い方の問題だったので、メモしておく。

実験環境

macOS 10.16.6
ASP.NET Core 3.1
dotnet new webapi コマンドで作成したWeb API
ab -n 100 -c 100 URL ab(Apache Bench)コマンドで同時100アクセスの時間を計測

同期処理の場合

以下の単純なAPI処理を用意する。

[HttpGet]
[Route("sync")]
public string Sync()
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    Thread.Sleep(1000);
    sw.Stop();
    this._logger.LogInformation(sw.ElapsedMilliseconds + "ms");
    return "sync";
}

1秒スリープなので1秒で応答が帰ってくるはず。

結果

ab -n 100 -c 100 http://localhost:5000/test/sync
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient).....done


Server Software:        Kestrel
Server Hostname:        localhost
Server Port:            5000

Document Path:          /test/sync
Document Length:        4 bytes

Concurrency Level:      100
Time taken for tests:   15.031 seconds
Complete requests:      100
Failed requests:        39
   (Connect: 0, Receive: 0, Length: 39, Exceptions: 0)
Total transferred:      8357 bytes
HTML transferred:       244 bytes
Requests per second:    6.65 [#/sec] (mean)
Time per request:       15031.445 [ms] (mean)
Time per request:       150.314 [ms] (mean, across all concurrent requests)
Transfer rate:          0.54 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    5   1.8      5       8
Processing:  1020 11970 2007.4  13046   14009
Waiting:        0 7868 6438.3  13046   14009
Total:       1020 11975 2006.7  13050   14011

Percentage of the requests served within a certain time (ms)
  50%  13050
  66%  13052
  75%  13053
  80%  13053
  90%  13055
  95%  13100
  98%  13943
  99%  14011
 100%  14011 (longest request)

最悪で14秒。遅い。一方ログを見ると各処理は1秒で終わっていることがわかる。

1004ms
1003ms
...
1000ms
1004ms

つまり処理が始まる前か終わった後の待ちが長いことがわかる。
これは何に待たされているのかを調べると、スレッドプールにスレッドを追加するのが待たされていることがわかった。

スレッドプールの説明はこちら
マネージドスレッドプール

ASP.NETによらず、.NET環境ではスレッドはプロセス自身が立ち上げて管理するより、タスクを使ってスレッドプールのスレッドを使い回すことが推奨されている。スレッドプールにはいくつかのスレッドが待機しており、タスク(ASP.NETではHTTPリクエストなど)がキューに入ると、空いているスレッドに割り当て、処理が終わると待機状態に戻す、という処理がされている。

ここで問題なのは、どの程度のスレッドがプールされるのか、ということだが、以下に記載されている。

スレッドプールの最小値

スレッドプールでは、カテゴリごとに指定された最小値に達するまで、要求に応じて新しいワーカースレッドまたは I/O完了スレッドが提供されます。
スレッドプールの最小値に達すると、追加のスレッドが作成されるか、いくつかのタスクが完了するまで待機状態になります。 .NET Framework 4 以降では、スループットを最適化するために、スレッドプールでワーカースレッドの作成と破棄が行われます。スループットは、タスクの単位時間あたりの完了数として定義されます。

まとめるとこんな感じ。

  • スレッドが即起動されるのは指定した最小値まで
  • 最小値以上の個数のスレッドは作成が待たされる
  • タスクの完了時間のあたりの完了数が最適になるように生成は調整される

スレッドは決まった数まではすぐに立ち上がるけど、それ以降は他の処理が終わるか、.NETの実行環境によって新しくスレッドが生成されるまで待たされるということ。

この決まった数というのは、ThreadPool.GetMinThreadsメソッドで取得できる。Macで確認すると、

ワーカースレッド:4
IO待ち:4

だった。これは既定では、スレッドの最小数はシステム上のプロセッサの数に設定されます。と書かれているようにCPUの数であり、他の環境で確認してもvCPUの数であった。vCPUが2のサーバーだと、即起動されるスレッドは2個まで、ということになる。スレッド2個とか少ないのでは?

以下のコードで処理が開始された時間とスレッドIDをログに出して確認してみる。

[HttpGet]
[Route("sync")]
public string Sync()
{
    var startTime = DateTimeOffset.Now;
    Stopwatch sw = new Stopwatch();
    sw.Start();
    Thread.Sleep(1000);
    sw.Stop();
    this._logger.LogInformation("{0,2}: {1:D2}:{2:D2}:{3:D2}:{4:D3} {5,5}ms",  Thread.CurrentThread.ManagedThreadId, startTime.Hour, startTime.Minute, startTime.Second, startTime.Millisecond, sw.ElapsedMilliseconds);
    return "sync";
}

結果はこちら。カッコでほぼ同じ時間に起動したスレッドの数を追記した。

   9: 18:31:40:893  1000ms (1個)
   9: 18:31:41:912  1004ms
   8: 18:31:41:912  1004ms
   4: 18:31:41:911  1004ms
  11: 18:31:41:912  1004ms
   6: 18:31:41:917  1004ms (5個)
  12: 18:31:42:907  1001ms
   4: 18:31:42:918  1000ms
   8: 18:31:42:918  1000ms
  11: 18:31:42:918  1000ms
   9: 18:31:42:919  1000ms
   6: 18:31:42:921  1004ms (6個)
   4: 18:31:43:919  1004ms
   8: 18:31:43:919  1004ms
  11: 18:31:43:919  1004ms
   9: 18:31:43:919  1004ms
   6: 18:31:43:926  1004ms (5個)
   4: 18:31:44:942  1003ms
  11: 18:31:44:943  1003ms
   9: 18:31:44:942  1003ms
   6: 18:31:44:945  1004ms
   8: 18:31:44:944  1004ms
  12: 18:31:44:958  1000ms (6個)
  13: 18:31:45:933  1004ms
   8: 18:31:45:956  1004ms
   9: 18:31:45:956  1005ms
  11: 18:31:45:958  1003ms
   6: 18:31:45:957  1003ms
  12: 18:31:45:961  1003ms
   4: 18:31:45:985  1001ms (7個)
  13: 18:31:46:940  1002ms
  14: 18:31:46:961  1004ms
   6: 18:31:46:968  1003ms
  11: 18:31:46:968  1003ms
  12: 18:31:46:968  1003ms
   9: 18:31:46:968  1004ms
   4: 18:31:46:988  1001ms
   8: 18:31:46:992  1003ms (8個)
  15: 18:31:47:940  1002ms
  13: 18:31:47:944  1004ms
  14: 18:31:47:968  1004ms
   6: 18:31:47:977  1004ms
   9: 18:31:47:979  1004ms
  12: 18:31:47:980  1003ms
   4: 18:31:47:990  1004ms
   8: 18:31:47:996  1004ms
  11: 18:31:48:078  1002ms (9個)
  13: 18:31:48:950  1000ms
  14: 18:31:48:973  1002ms
   6: 18:31:48:982  1003ms
  12: 18:31:48:984  1002ms
   9: 18:31:48:984  1003ms
   4: 18:31:48:995  1001ms
   8: 18:31:49:001  1004ms
  15: 18:31:49:081  1003ms
  11: 18:31:49:081  1003ms (9個)
  13: 18:31:49:951  1003ms
  16: 18:31:49:953  1003ms
  14: 18:31:49:977  1004ms
   6: 18:31:49:985  1004ms
   9: 18:31:49:987  1004ms
  12: 18:31:49:987  1004ms
   4: 18:31:49:997  1000ms
   8: 18:31:50:006  1000ms
  15: 18:31:50:085  1003ms
  11: 18:31:50:085  1003ms (10個)
  16: 18:31:50:957  1001ms
  14: 18:31:50:981  1004ms
   6: 18:31:50:990  1004ms
   9: 18:31:50:992  1004ms
  12: 18:31:50:992  1004ms
   4: 18:31:50:998  1004ms
   8: 18:31:51:007  1005ms
  13: 18:31:51:015  1003ms
  15: 18:31:51:089  1000ms
  11: 18:31:51:089  1000ms (10個)
  16: 18:31:51:962  1004ms
  14: 18:31:51:987  1001ms
   6: 18:31:51:995  1003ms
  12: 18:31:51:997  1003ms
   9: 18:31:51:996  1003ms
   4: 18:31:52:003  1002ms
  13: 18:31:52:021  1001ms
   8: 18:31:52:021  1001ms
  11: 18:31:52:090  1001ms
  15: 18:31:52:090  1001ms (10個)
  16: 18:31:52:967  1005ms
  14: 18:31:52:989  1002ms
   6: 18:31:52:999  1004ms
   9: 18:31:53:000  1004ms
  12: 18:31:53:000  1004ms
   4: 18:31:53:006  1003ms
   8: 18:31:53:026  1000ms
  13: 18:31:53:026  1000ms
  11: 18:31:53:092  1003ms
  15: 18:31:53:092  1003ms
  17: 18:31:53:092  1003ms (11個)
  16: 18:31:53:972  1002ms
  14: 18:31:53:992  1000ms
   6: 18:31:54:003  1000ms (3個)

徐々に使えるスレッドが増えていっている、というか徐々にしかスレッド数が増えないのがわかる。

最低スレッド数を上げる

一番簡単な解決策としては即立ち上げられるスレッドの個数を増やせばよい。
そのためには、ThreadPool.SetMinThreadsメソッドを使う。
とりあえず以下のようにワーカースレッドの最低数を100にしてみる。

public static void Main(string[] args)
{
    ThreadPool.SetMinThreads(100, 4);
    CreateHostBuilder(args).Build().Run();
}

abコマンドを実行すると、

ab -n 100 -c 100 http://localhost:5000/test/sync
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient).....done


Server Software:        Kestrel
Server Hostname:        localhost
Server Port:            5000

Document Path:          /test/sync
Document Length:        4 bytes

Concurrency Level:      100
Time taken for tests:   2.082 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      13700 bytes
HTML transferred:       400 bytes
Requests per second:    48.04 [#/sec] (mean)
Time per request:       2081.569 [ms] (mean)
Time per request:       20.816 [ms] (mean, across all concurrent requests)
Transfer rate:          6.43 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0   11   3.0     12      13
Processing:  1020 1055   5.1   1056    1061
Waiting:     1007 1054   6.3   1055    1060
Total:       1020 1066   7.0   1068    1074

Percentage of the requests served within a certain time (ms)
  50%   1068
  66%   1069
  75%   1070
  80%   1070
  90%   1072
  95%   1073
  98%   1073
  99%   1074
 100%   1074 (longest request)

ほぼ1秒で終了した。このことからも、やはり同時アクセス時の応答が遅くなるのは、スレッドの立ち上げが待たされているためだということがわかった。とはいえ以下の注意書きにもあるように、むやみにスレッド数を増やすのもよくないかもしれない。

スレッドプールの最小値

アイドルスレッドの最小数は、ThreadPool.SetMinThreads メソッドを使用して増やすことができます。 ただし、これらの値を必要以上に大きくすると、パフォーマンスの問題が発生する可能性があります。 同時に開始するタスクの数が多すぎる場合は、すべてのタスクで処理速度が低下する可能性があります。 ほとんどの場合、スレッドを割り当てるためのスレッド プール独自のアルゴリズムを使用することでスレッドプールのパフォーマンスが向上します。

IO同期待ちの場合

IO待ちの場合も見てみよう。
先ほどの1秒待って応答するAPI(http://localhost:5001 で待ち受けさせる)に対してリクエストするAPIを以下のように書く。非同期メソッドGetAsyncをResultで同期的に待つようにする。

[HttpGet]
[Route("io")]
public string Io()
{
    var startTime = DateTimeOffset.Now;
    Stopwatch sw = new Stopwatch();
    sw.Start();
    var response = client.GetAsync("http://localhost:5001/test/sync").Result;
    sw.Stop();
    this._logger.LogInformation("{0,2}: {1:D2}:{2:D2}:{3:D2}:{4:D3} {5,5}ms",  Thread.CurrentThread.ManagedThreadId, startTime.Hour, startTime.Minute, startTime.Second, startTime.Millisecond, sw.ElapsedMilliseconds);
    return "iosy";
}

これをワーカースレッド4、IO待ちスレッド4で起動させて、同じくabで同時100アクセスするとどうなるか。

ab -n 100 -c 100 -s 300 http://localhost:5000/test/io
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient).....done


Server Software:        Kestrel
Server Hostname:        localhost
Server Port:            5000

Document Path:          /test/io
Document Length:        4 bytes

Concurrency Level:      100
Time taken for tests:   65.697 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      13700 bytes
HTML transferred:       400 bytes
Requests per second:    1.52 [#/sec] (mean)
Time per request:       65696.601 [ms] (mean)
Time per request:       656.966 [ms] (mean, across all concurrent requests)
Transfer rate:          0.20 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    5   1.5      6       7
Processing:  1021 63970 6359.5  64624   64674
Waiting:     1014 63970 6360.2  64623   64674
Total:       1022 63975 6360.0  64630   64681

Percentage of the requests served within a certain time (ms)
  50%  64630
  66%  64640
  75%  64642
  80%  64648
  90%  64662
  95%  64673
  98%  64675
  99%  64681
 100%  64681 (longest request)

めっちゃくちゃ遅くなった。abのデフォルトタイムアウト30秒では足りないのでタイムアウト時間を伸ばさないといけなかったほど。ログはこんな感じになる(開始時間順に並べ直している)

   6: 21:09:51:730  1080ms
  11: 21:09:54:925  1006ms
   4: 21:09:55:949 63195ms
   6: 21:09:55:949 63129ms
   8: 21:09:55:949 62054ms
   9: 21:09:55:949 63110ms
  11: 21:09:55:954 63080ms
  12: 21:09:57:038 62084ms
  13: 21:09:57:540 61490ms
  14: 21:09:58:050 61037ms
  15: 21:09:59:057 59987ms
  16: 21:10:00:068 58999ms
  17: 21:10:01:073 58031ms
  18: 21:10:01:581 57523ms
  19: 21:10:02:584 56543ms
  20: 21:10:03:589 55471ms
  21: 21:10:04:607 54465ms
  22: 21:10:05:610 53450ms
  23: 21:10:06:616 52488ms
  24: 21:10:07:613 51514ms
  25: 21:10:08:620 50446ms
  26: 21:10:09:127 49940ms
  27: 21:10:10:145 48959ms
  28: 21:10:11:151 47928ms
  29: 21:10:12:147 46873ms
  30: 21:10:13:151 45952ms
  31: 21:10:13:658 45388ms
  32: 21:10:14:662 44427ms
  33: 21:10:15:665 43401ms
  34: 21:10:16:170 42933ms
  35: 21:10:16:678 42382ms
  36: 21:10:17:184 41877ms
  37: 21:10:17:685 41405ms
  38: 21:10:18:200 40873ms
  39: 21:10:19:223 39843ms
  40: 21:10:20:260 38832ms
  41: 21:10:21:219 37814ms
  42: 21:10:21:722 37305ms
  43: 21:10:22:227 36798ms
  44: 21:10:22:729 36313ms
  45: 21:10:23:284 35757ms
  46: 21:10:24:291 34757ms
  47: 21:10:25:298 33782ms
  48: 21:10:26:288 32781ms
  49: 21:10:27:264 31802ms
  50: 21:10:28:267 30822ms
  51: 21:10:28:769 30322ms
  52: 21:10:29:271 29777ms
  53: 21:10:29:776 29286ms
  54: 21:10:30:282 28779ms
  55: 21:10:30:787 28279ms
  56: 21:10:31:292 27757ms
  57: 21:10:31:797 27272ms
  58: 21:10:32:302 26748ms
  59: 21:10:32:808 26290ms
  60: 21:10:33:321 25733ms
  61: 21:10:34:337 24696ms
  62: 21:10:35:329 23731ms
  63: 21:10:35:834 23264ms
  64: 21:10:36:337 22761ms
  65: 21:10:36:841 22256ms
  66: 21:10:37:346 21757ms
  67: 21:10:37:851 21213ms
  68: 21:10:38:356 20671ms
  69: 21:10:38:860 20202ms
  70: 21:10:39:361 19700ms
  71: 21:10:39:866 19208ms
  72: 21:10:40:370 18694ms
  73: 21:10:40:871 18244ms
  74: 21:10:41:376 17740ms
  75: 21:10:41:877 17238ms
  76: 21:10:42:879 16245ms
  77: 21:10:43:381 15650ms
  78: 21:10:43:885 15195ms
  79: 21:10:44:390 14671ms
  80: 21:10:44:894 14229ms
  81: 21:10:45:398 13730ms
  82: 21:10:45:903 13225ms
  83: 21:10:46:408 12634ms
  84: 21:10:46:913 12210ms
  85: 21:10:47:418 11649ms
  86: 21:10:47:920 11154ms
  87: 21:10:48:422 10635ms
  88: 21:10:48:933 10199ms
  89: 21:10:49:433  9699ms
  90: 21:10:49:938  9195ms
  91: 21:10:50:443  8689ms
  92: 21:10:50:948  8142ms
  93: 21:10:51:453  7640ms
  94: 21:10:51:956  7183ms
  95: 21:10:52:460  6644ms
  96: 21:10:52:965  6052ms
  97: 21:10:53:470  5670ms
  98: 21:10:53:972  5143ms
  99: 21:10:54:477  4592ms
 100: 21:10:54:982  4158ms
 101: 21:10:55:486  3654ms
 102: 21:10:55:990  3155ms
 103: 21:10:56:492  2541ms
 104: 21:10:56:997  2149ms
 105: 21:10:57:501  1644ms

スレッドが500msもしくは1秒ごとに1個生成されて、ほぼ100個のスレッドが生成され(正確には98個)、ほぼ同じ時間に終了している。http://localhost:5000/test/io へのアクセスは徐々に処理されているが、http://localhost:5001/test/sync にはほぼ同じ時間にアクセスが届いている。

IO待ちスレッドを100に増やしても同程度。ワーカースレッドを100にすると1秒になった。

アクセス時にスレッドプールのキューに100個溜まって、それを処理するスレッドが順次生成されているが、それが全部消化されるまではIO処理に入っていない感じになっている。正確にはよくわからないが、WaitやResultで待つ処理はワーカースレッドのキューに入り、それが割り当てられない限りは非同期部分の処理にも進めないのかもしれない。この場合も想定される同時リクエスト数分のワーカースレッド数を設定しないとさばけないだろう。結果としては最悪なので、非同期処理を同期的に待つ、というのはあまりやらない方がよさそう。

IO非同期待ちの場合

ということでawaitで非同期に待って処理させるようにする。以下のようになる。

[HttpGet]
[Route("asyncio")]
public async Task<string> AsyncIo()
{
    var startTime = DateTimeOffset.Now;
    Stopwatch sw = new Stopwatch();
    sw.Start();
    var response = await client.GetAsync("http://localhost:5001/test/sync");
    sw.Stop();
    this._logger.LogInformation("{0,2}: {1:D2}:{2:D2}:{3:D2}:{4:D3} {5,5}ms",  Thread.CurrentThread.ManagedThreadId, startTime.Hour, startTime.Minute, startTime.Second, startTime.Millisecond, sw.ElapsedMilliseconds);
    return "ioas";
}

コントローラのメソッドにasyncをつけ、戻り値の型をTaskとし、GetAsyncをawaitで待つ。

これまでと同様、ワーカースレッド4、IO待ちスレッド4として起動し、abで同時100アクセスした。

ab -n 100 -c 100 http://localhost:5000/test/asyncio
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient).....done


Server Software:        Kestrel
Server Hostname:        localhost
Server Port:            5000

Document Path:          /test/asyncio
Document Length:        4 bytes

Concurrency Level:      100
Time taken for tests:   2.206 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      13700 bytes
HTML transferred:       400 bytes
Requests per second:    45.32 [#/sec] (mean)
Time per request:       2206.289 [ms] (mean)
Time per request:       22.063 [ms] (mean, across all concurrent requests)
Transfer rate:          6.06 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    5   2.0      4       9
Processing:  1015 1149  26.1   1156    1165
Waiting:     1015 1148  26.6   1155    1164
Total:       1023 1153  25.5   1159    1169

Percentage of the requests served within a certain time (ms)
  50%   1159
  66%   1162
  75%   1164
  80%   1164
  90%   1166
  95%   1167
  98%   1168
  99%   1169
 100%   1169 (longest request)

スレッド数が少なくても、ほぼ1秒で処理できたことがわかる。ASP.NET Core的にはこれが正解なんだろうと思う。

IO待ちに関する記事を読むと、IOの結果を待っている間にスレッドは存在しないとのこと。処理がawait client.GetAsyncにまでたどり着けばその時点でスレッドは解放され、次のスレッドを生成することができる。(Resultで待つようなことをしているとスレッドは解放されないものと思われる)

そして非同期待ちが終わると、その時点で処理がスレッドに割り当てられて処理が再開されるが、この時もスレッドは使われていないのでスレッドプールにある少数のスレッドを割り当てることができる。APIアクセスやDBアクセスなどIO待ちがメインの処理の場合は、awaitで呼び出すと少数のスレッドで多数のリクエストをさばけるようになる。基本的にはこの形で作るべきと思われる。

おわりに

スレッド、スレッドプール、タスク、async/awaitなど、C#や.NET Coreの非同期処理の勉強になった。
async/awaitは一見何やってるかわからないけど、わかればすごく便利。使えるところでは積極的に使っていこう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ASP.NET Coreへの同時アクセス時の応答が遅くなるのはスレッドプールの挙動のためかも

はじめに

ASP.NET Coreへの同時アクセスの応答が妙に遅くなるので原因調査したら、スレッドプールの起動の問題、もっと言えば.NET環境での非同期処理の使い方の問題だったので、メモしておく。

実験環境

macOS 10.16.6
ASP.NET Core 3.1
dotnet new webapi コマンドで作成したWeb API
ab -n 100 -c 100 URL ab(Apache Bench)コマンドで同時100アクセスの時間を計測

同期処理の場合

以下の単純なAPI処理を用意する。

[HttpGet]
[Route("sync")]
public string Sync()
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    Thread.Sleep(1000);
    sw.Stop();
    this._logger.LogInformation(sw.ElapsedMilliseconds + "ms");
    return "sync";
}

1秒スリープなので1秒で応答が帰ってくるはず。

結果

ab -n 100 -c 100 http://localhost:5000/test/sync
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient).....done


Server Software:        Kestrel
Server Hostname:        localhost
Server Port:            5000

Document Path:          /test/sync
Document Length:        4 bytes

Concurrency Level:      100
Time taken for tests:   15.031 seconds
Complete requests:      100
Failed requests:        39
   (Connect: 0, Receive: 0, Length: 39, Exceptions: 0)
Total transferred:      8357 bytes
HTML transferred:       244 bytes
Requests per second:    6.65 [#/sec] (mean)
Time per request:       15031.445 [ms] (mean)
Time per request:       150.314 [ms] (mean, across all concurrent requests)
Transfer rate:          0.54 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    5   1.8      5       8
Processing:  1020 11970 2007.4  13046   14009
Waiting:        0 7868 6438.3  13046   14009
Total:       1020 11975 2006.7  13050   14011

Percentage of the requests served within a certain time (ms)
  50%  13050
  66%  13052
  75%  13053
  80%  13053
  90%  13055
  95%  13100
  98%  13943
  99%  14011
 100%  14011 (longest request)

最悪で14秒。遅い。一方ログを見ると各処理は1秒で終わっていることがわかる。

1004ms
1003ms
...
1000ms
1004ms

つまり処理が始まる前か終わった後の待ちが長いことがわかる。
これは何に待たされているのかを調べると、スレッドプールにスレッドを追加するのが待たされていることがわかった。

スレッドプールの説明はこちら
マネージドスレッドプール

ASP.NETによらず、.NET環境ではスレッドはプロセス自身が立ち上げて管理するより、タスクを使ってスレッドプールのスレッドを使い回すことが推奨されている。スレッドプールにはいくつかのスレッドが待機しており、タスク(ASP.NETではHTTPリクエストなど)がキューに入ると、空いているスレッドに割り当て、処理が終わると待機状態に戻す、という処理がされている。

ここで問題なのは、どの程度のスレッドがプールされるのか、ということだが、以下に記載されている。

スレッドプールの最小値

スレッドプールでは、カテゴリごとに指定された最小値に達するまで、要求に応じて新しいワーカースレッドまたは I/O完了スレッドが提供されます。
スレッドプールの最小値に達すると、追加のスレッドが作成されるか、いくつかのタスクが完了するまで待機状態になります。 .NET Framework 4 以降では、スループットを最適化するために、スレッドプールでワーカースレッドの作成と破棄が行われます。スループットは、タスクの単位時間あたりの完了数として定義されます。

まとめるとこんな感じ。

  • スレッドが即起動されるのは指定した最小値まで
  • 最小値以上の個数のスレッドは作成が待たされる
  • タスクの単位時間のあたりの完了数が最適になるように生成は調整される

スレッドは決まった数まではすぐに立ち上がるけど、それ以降は他の処理が終わるか、.NETの実行環境によって新しくスレッドが生成されるまで待たされるということ。

この決まった数というのは、ThreadPool.GetMinThreadsメソッドで取得できる。Macで確認すると、

ワーカースレッド:4
IO待ち:4

だった。これは既定では、スレッドの最小数はシステム上のプロセッサの数に設定されます。と書かれているようにCPUの数であり、他の環境で確認してもvCPUの数であった。vCPUが2のサーバーだと、即起動されるスレッドは2個まで、ということになる。スレッド2個とか少ないのでは?

以下のコードで処理が開始された時間とスレッドIDをログに出して確認してみる。

[HttpGet]
[Route("sync")]
public string Sync()
{
    var startTime = DateTimeOffset.Now;
    Stopwatch sw = new Stopwatch();
    sw.Start();
    Thread.Sleep(1000);
    sw.Stop();
    this._logger.LogInformation("{0,2}: {1:D2}:{2:D2}:{3:D2}:{4:D3} {5,5}ms",  Thread.CurrentThread.ManagedThreadId, startTime.Hour, startTime.Minute, startTime.Second, startTime.Millisecond, sw.ElapsedMilliseconds);
    return "sync";
}

結果はこちら。カッコでほぼ同じ時間に起動したスレッドの数を追記した。

   9: 18:31:40:893  1000ms (1個)
   9: 18:31:41:912  1004ms
   8: 18:31:41:912  1004ms
   4: 18:31:41:911  1004ms
  11: 18:31:41:912  1004ms
   6: 18:31:41:917  1004ms (5個)
  12: 18:31:42:907  1001ms
   4: 18:31:42:918  1000ms
   8: 18:31:42:918  1000ms
  11: 18:31:42:918  1000ms
   9: 18:31:42:919  1000ms
   6: 18:31:42:921  1004ms (6個)
   4: 18:31:43:919  1004ms
   8: 18:31:43:919  1004ms
  11: 18:31:43:919  1004ms
   9: 18:31:43:919  1004ms
   6: 18:31:43:926  1004ms (5個)
   4: 18:31:44:942  1003ms
  11: 18:31:44:943  1003ms
   9: 18:31:44:942  1003ms
   6: 18:31:44:945  1004ms
   8: 18:31:44:944  1004ms
  12: 18:31:44:958  1000ms (6個)
  13: 18:31:45:933  1004ms
   8: 18:31:45:956  1004ms
   9: 18:31:45:956  1005ms
  11: 18:31:45:958  1003ms
   6: 18:31:45:957  1003ms
  12: 18:31:45:961  1003ms
   4: 18:31:45:985  1001ms (7個)
  13: 18:31:46:940  1002ms
  14: 18:31:46:961  1004ms
   6: 18:31:46:968  1003ms
  11: 18:31:46:968  1003ms
  12: 18:31:46:968  1003ms
   9: 18:31:46:968  1004ms
   4: 18:31:46:988  1001ms
   8: 18:31:46:992  1003ms (8個)
  15: 18:31:47:940  1002ms
  13: 18:31:47:944  1004ms
  14: 18:31:47:968  1004ms
   6: 18:31:47:977  1004ms
   9: 18:31:47:979  1004ms
  12: 18:31:47:980  1003ms
   4: 18:31:47:990  1004ms
   8: 18:31:47:996  1004ms
  11: 18:31:48:078  1002ms (9個)
  13: 18:31:48:950  1000ms
  14: 18:31:48:973  1002ms
   6: 18:31:48:982  1003ms
  12: 18:31:48:984  1002ms
   9: 18:31:48:984  1003ms
   4: 18:31:48:995  1001ms
   8: 18:31:49:001  1004ms
  15: 18:31:49:081  1003ms
  11: 18:31:49:081  1003ms (9個)
  13: 18:31:49:951  1003ms
  16: 18:31:49:953  1003ms
  14: 18:31:49:977  1004ms
   6: 18:31:49:985  1004ms
   9: 18:31:49:987  1004ms
  12: 18:31:49:987  1004ms
   4: 18:31:49:997  1000ms
   8: 18:31:50:006  1000ms
  15: 18:31:50:085  1003ms
  11: 18:31:50:085  1003ms (10個)
  16: 18:31:50:957  1001ms
  14: 18:31:50:981  1004ms
   6: 18:31:50:990  1004ms
   9: 18:31:50:992  1004ms
  12: 18:31:50:992  1004ms
   4: 18:31:50:998  1004ms
   8: 18:31:51:007  1005ms
  13: 18:31:51:015  1003ms
  15: 18:31:51:089  1000ms
  11: 18:31:51:089  1000ms (10個)
  16: 18:31:51:962  1004ms
  14: 18:31:51:987  1001ms
   6: 18:31:51:995  1003ms
  12: 18:31:51:997  1003ms
   9: 18:31:51:996  1003ms
   4: 18:31:52:003  1002ms
  13: 18:31:52:021  1001ms
   8: 18:31:52:021  1001ms
  11: 18:31:52:090  1001ms
  15: 18:31:52:090  1001ms (10個)
  16: 18:31:52:967  1005ms
  14: 18:31:52:989  1002ms
   6: 18:31:52:999  1004ms
   9: 18:31:53:000  1004ms
  12: 18:31:53:000  1004ms
   4: 18:31:53:006  1003ms
   8: 18:31:53:026  1000ms
  13: 18:31:53:026  1000ms
  11: 18:31:53:092  1003ms
  15: 18:31:53:092  1003ms
  17: 18:31:53:092  1003ms (11個)
  16: 18:31:53:972  1002ms
  14: 18:31:53:992  1000ms
   6: 18:31:54:003  1000ms (3個)

徐々に使えるスレッドが増えていっている、というか徐々にしかスレッド数が増えないのがわかる。

最低スレッド数を上げる

一番簡単な解決策としては即立ち上げられるスレッドの個数を増やせばよい。
そのためには、ThreadPool.SetMinThreadsメソッドを使う。
とりあえず以下のようにワーカースレッドの最低数を100にしてみる。

public static void Main(string[] args)
{
    ThreadPool.SetMinThreads(100, 4);
    CreateHostBuilder(args).Build().Run();
}

abコマンドを実行すると、

ab -n 100 -c 100 http://localhost:5000/test/sync
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient).....done


Server Software:        Kestrel
Server Hostname:        localhost
Server Port:            5000

Document Path:          /test/sync
Document Length:        4 bytes

Concurrency Level:      100
Time taken for tests:   2.082 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      13700 bytes
HTML transferred:       400 bytes
Requests per second:    48.04 [#/sec] (mean)
Time per request:       2081.569 [ms] (mean)
Time per request:       20.816 [ms] (mean, across all concurrent requests)
Transfer rate:          6.43 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0   11   3.0     12      13
Processing:  1020 1055   5.1   1056    1061
Waiting:     1007 1054   6.3   1055    1060
Total:       1020 1066   7.0   1068    1074

Percentage of the requests served within a certain time (ms)
  50%   1068
  66%   1069
  75%   1070
  80%   1070
  90%   1072
  95%   1073
  98%   1073
  99%   1074
 100%   1074 (longest request)

ほぼ1秒で終了した。このことからも、やはり同時アクセス時の応答が遅くなるのは、スレッドの立ち上げが待たされているためだということがわかった。とはいえ以下の注意書きにもあるように、むやみにスレッド数を増やすのもよくないかもしれない。

スレッドプールの最小値

アイドルスレッドの最小数は、ThreadPool.SetMinThreads メソッドを使用して増やすことができます。 ただし、これらの値を必要以上に大きくすると、パフォーマンスの問題が発生する可能性があります。 同時に開始するタスクの数が多すぎる場合は、すべてのタスクで処理速度が低下する可能性があります。 ほとんどの場合、スレッドを割り当てるためのスレッド プール独自のアルゴリズムを使用することでスレッドプールのパフォーマンスが向上します。

IO同期待ちの場合

IO待ちの場合も見てみよう。
先ほどの1秒待って応答するAPI(http://localhost:5001 で待ち受けさせる)に対してリクエストするAPIを以下のように書く。非同期メソッドGetAsyncをResultで同期的に待つようにする。

[HttpGet]
[Route("io")]
public string Io()
{
    var startTime = DateTimeOffset.Now;
    Stopwatch sw = new Stopwatch();
    sw.Start();
    var response = client.GetAsync("http://localhost:5001/test/sync").Result;
    sw.Stop();
    this._logger.LogInformation("{0,2}: {1:D2}:{2:D2}:{3:D2}:{4:D3} {5,5}ms",  Thread.CurrentThread.ManagedThreadId, startTime.Hour, startTime.Minute, startTime.Second, startTime.Millisecond, sw.ElapsedMilliseconds);
    return "iosy";
}

これをワーカースレッド4、IO待ちスレッド4で起動させて、同じくabで同時100アクセスするとどうなるか。

ab -n 100 -c 100 -s 300 http://localhost:5000/test/io
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient).....done


Server Software:        Kestrel
Server Hostname:        localhost
Server Port:            5000

Document Path:          /test/io
Document Length:        4 bytes

Concurrency Level:      100
Time taken for tests:   65.697 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      13700 bytes
HTML transferred:       400 bytes
Requests per second:    1.52 [#/sec] (mean)
Time per request:       65696.601 [ms] (mean)
Time per request:       656.966 [ms] (mean, across all concurrent requests)
Transfer rate:          0.20 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    5   1.5      6       7
Processing:  1021 63970 6359.5  64624   64674
Waiting:     1014 63970 6360.2  64623   64674
Total:       1022 63975 6360.0  64630   64681

Percentage of the requests served within a certain time (ms)
  50%  64630
  66%  64640
  75%  64642
  80%  64648
  90%  64662
  95%  64673
  98%  64675
  99%  64681
 100%  64681 (longest request)

めっちゃくちゃ遅くなった。abのデフォルトタイムアウト30秒では足りないのでタイムアウト時間を伸ばさないといけなかったほど。ログはこんな感じになる(開始時間順に並べ直している)

   6: 21:09:51:730  1080ms
  11: 21:09:54:925  1006ms
   4: 21:09:55:949 63195ms
   6: 21:09:55:949 63129ms
   8: 21:09:55:949 62054ms
   9: 21:09:55:949 63110ms
  11: 21:09:55:954 63080ms
  12: 21:09:57:038 62084ms
  13: 21:09:57:540 61490ms
  14: 21:09:58:050 61037ms
  15: 21:09:59:057 59987ms
  16: 21:10:00:068 58999ms
  17: 21:10:01:073 58031ms
  18: 21:10:01:581 57523ms
  19: 21:10:02:584 56543ms
  20: 21:10:03:589 55471ms
  21: 21:10:04:607 54465ms
  22: 21:10:05:610 53450ms
  23: 21:10:06:616 52488ms
  24: 21:10:07:613 51514ms
  25: 21:10:08:620 50446ms
  26: 21:10:09:127 49940ms
  27: 21:10:10:145 48959ms
  28: 21:10:11:151 47928ms
  29: 21:10:12:147 46873ms
  30: 21:10:13:151 45952ms
  31: 21:10:13:658 45388ms
  32: 21:10:14:662 44427ms
  33: 21:10:15:665 43401ms
  34: 21:10:16:170 42933ms
  35: 21:10:16:678 42382ms
  36: 21:10:17:184 41877ms
  37: 21:10:17:685 41405ms
  38: 21:10:18:200 40873ms
  39: 21:10:19:223 39843ms
  40: 21:10:20:260 38832ms
  41: 21:10:21:219 37814ms
  42: 21:10:21:722 37305ms
  43: 21:10:22:227 36798ms
  44: 21:10:22:729 36313ms
  45: 21:10:23:284 35757ms
  46: 21:10:24:291 34757ms
  47: 21:10:25:298 33782ms
  48: 21:10:26:288 32781ms
  49: 21:10:27:264 31802ms
  50: 21:10:28:267 30822ms
  51: 21:10:28:769 30322ms
  52: 21:10:29:271 29777ms
  53: 21:10:29:776 29286ms
  54: 21:10:30:282 28779ms
  55: 21:10:30:787 28279ms
  56: 21:10:31:292 27757ms
  57: 21:10:31:797 27272ms
  58: 21:10:32:302 26748ms
  59: 21:10:32:808 26290ms
  60: 21:10:33:321 25733ms
  61: 21:10:34:337 24696ms
  62: 21:10:35:329 23731ms
  63: 21:10:35:834 23264ms
  64: 21:10:36:337 22761ms
  65: 21:10:36:841 22256ms
  66: 21:10:37:346 21757ms
  67: 21:10:37:851 21213ms
  68: 21:10:38:356 20671ms
  69: 21:10:38:860 20202ms
  70: 21:10:39:361 19700ms
  71: 21:10:39:866 19208ms
  72: 21:10:40:370 18694ms
  73: 21:10:40:871 18244ms
  74: 21:10:41:376 17740ms
  75: 21:10:41:877 17238ms
  76: 21:10:42:879 16245ms
  77: 21:10:43:381 15650ms
  78: 21:10:43:885 15195ms
  79: 21:10:44:390 14671ms
  80: 21:10:44:894 14229ms
  81: 21:10:45:398 13730ms
  82: 21:10:45:903 13225ms
  83: 21:10:46:408 12634ms
  84: 21:10:46:913 12210ms
  85: 21:10:47:418 11649ms
  86: 21:10:47:920 11154ms
  87: 21:10:48:422 10635ms
  88: 21:10:48:933 10199ms
  89: 21:10:49:433  9699ms
  90: 21:10:49:938  9195ms
  91: 21:10:50:443  8689ms
  92: 21:10:50:948  8142ms
  93: 21:10:51:453  7640ms
  94: 21:10:51:956  7183ms
  95: 21:10:52:460  6644ms
  96: 21:10:52:965  6052ms
  97: 21:10:53:470  5670ms
  98: 21:10:53:972  5143ms
  99: 21:10:54:477  4592ms
 100: 21:10:54:982  4158ms
 101: 21:10:55:486  3654ms
 102: 21:10:55:990  3155ms
 103: 21:10:56:492  2541ms
 104: 21:10:56:997  2149ms
 105: 21:10:57:501  1644ms

スレッドが500msもしくは1秒ごとに1個生成されて、ほぼ100個のスレッドが生成され(正確には98個)、ほぼ同じ時間に終了している。http://localhost:5000/test/io へのアクセスは徐々に処理されているが、http://localhost:5001/test/sync にはほぼ同じ時間にアクセスが届いている。

IO待ちスレッドを100に増やしても同程度。ワーカースレッドを100にすると1秒になった。

アクセス時にスレッドプールのキューに100個溜まって、それを処理するスレッドが順次生成されているが、それが全部消化されるまではIO処理に入っていない感じになっている。正確にはよくわからないが、WaitやResultで待つ処理はワーカースレッドのキューに入り、それが割り当てられない限りは非同期部分の処理にも進めないのかもしれない。この場合も想定される同時リクエスト数分のワーカースレッド数を設定しないとさばけないだろう。結果としては最悪なので、非同期処理を同期的に待つ、というのはあまりやらない方がよさそう。

IO非同期待ちの場合

ということでawaitで非同期に待って処理させるようにする。以下のようになる。

[HttpGet]
[Route("asyncio")]
public async Task<string> AsyncIo()
{
    var startTime = DateTimeOffset.Now;
    Stopwatch sw = new Stopwatch();
    sw.Start();
    var response = await client.GetAsync("http://localhost:5001/test/sync");
    sw.Stop();
    this._logger.LogInformation("{0,2}: {1:D2}:{2:D2}:{3:D2}:{4:D3} {5,5}ms",  Thread.CurrentThread.ManagedThreadId, startTime.Hour, startTime.Minute, startTime.Second, startTime.Millisecond, sw.ElapsedMilliseconds);
    return "ioas";
}

コントローラのメソッドにasyncをつけ、戻り値の型をTaskとし、GetAsyncをawaitで待つ。

これまでと同様、ワーカースレッド4、IO待ちスレッド4として起動し、abで同時100アクセスした。

ab -n 100 -c 100 http://localhost:5000/test/asyncio
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient).....done


Server Software:        Kestrel
Server Hostname:        localhost
Server Port:            5000

Document Path:          /test/asyncio
Document Length:        4 bytes

Concurrency Level:      100
Time taken for tests:   2.206 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      13700 bytes
HTML transferred:       400 bytes
Requests per second:    45.32 [#/sec] (mean)
Time per request:       2206.289 [ms] (mean)
Time per request:       22.063 [ms] (mean, across all concurrent requests)
Transfer rate:          6.06 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    5   2.0      4       9
Processing:  1015 1149  26.1   1156    1165
Waiting:     1015 1148  26.6   1155    1164
Total:       1023 1153  25.5   1159    1169

Percentage of the requests served within a certain time (ms)
  50%   1159
  66%   1162
  75%   1164
  80%   1164
  90%   1166
  95%   1167
  98%   1168
  99%   1169
 100%   1169 (longest request)

スレッド数が少なくても、ほぼ1秒で処理できたことがわかる。ASP.NET Core的にはこれが正解なんだろうと思う。

IO待ちに関する記事を読むと、IOの結果を待っている間にスレッドは存在しないとのこと。処理がawait client.GetAsyncにまでたどり着けばその時点でスレッドは解放され、次のスレッドを生成することができる。(Resultで待つようなことをしているとスレッドは解放されないものと思われる)

そして非同期待ちが終わると、その時点で処理がスレッドに割り当てられて処理が再開されるが、この時もスレッドは使われていないのでスレッドプールにある少数のスレッドを割り当てることができる。APIアクセスやDBアクセスなどIO待ちがメインの処理の場合は、awaitで呼び出すと少数のスレッドで多数のリクエストをさばけるようになる。基本的にはこの形で作るべきと思われる。

おわりに

スレッド、スレッドプール、タスク、async/awaitなど、C#や.NET Coreの非同期処理の勉強になった。
async/awaitは一見何やってるかわからないけど、わかればすごく便利。使えるところでは積極的に使っていこう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#で3D形状データを表示・操作し、画像を保存する。

概要

 C#で3D形状、3Dモデルを簡単に表示・操作し、画像として保存するまでの良いテンプレートが見つかりませんでしたので、自分の行っている手法を紹介いたします。本記事では、3D表示にOpenGLのラッパーであるOpenTKを、画像データの保存にOpenCVのC#ラッパーのOpenCVSharpを、3D形状の読み込みにSurfaceAnalyzerを使います。完成イメージは以下のgifになります。はじめに三次元形状を読み込み、マウスで回転動作をし、画像として保存します。
 なお、今回用いたソースコードはgitにまとめています。
AGD_20200814_205618_1.gif

準備

 初めに、必要なライブラリのインストールを行います。

Windows Form Applicationの作成

 今回は、WindowsFormApplicationとして作成していきます。
image.png

OpenTK, OpenTK.GLControlのインストール

 OpenTKは、C#でOpenGLを扱うためのライブラリです。GLControlは、フォーム上にOpenGLの画面を表示するためのコントロールです。
image.png

OpenCVSharp4, OpenCVSharp4.runtime.winのインストール

 OpenCVSharp4は、C#でOpenCVを使うためのラッパーです。今回は、画像を保存するために使用するため、画像保存が不要でしたらインストール不要です。System.Drawing.Bitmapでも良いのですが、取得した画像後処理をかけることを考慮して汎用性のあるこちらを用います。画像をウィンドウで表示するためには、それぞれの環境にあったruntimeもインストールする必要があります。Windowsの場合は、OpenCVSharp.runtime.winをインストールしましょう。
image.png

Surface Analyzerのインストール

 Surface Analyzerは、C#でSTLデータを扱うためのライブラリです。詳細な説明は、こちらの記事をご覧ください。STLファイルを読み込むことができれば、他のライブラリでも大丈夫です。
image.png

 それでは、これからコーディングを始めます。本記事では、実際に形状を表示する"Viewerフォーム"と、その表示をコントロールする"Controlフォーム"に分けてコーディングしていきます。

Viewerフォームのコーディング

 

Viewerフォームの作成

 今回は、3D形状を表示する専用のフォームを作成し、Controlフォームから操作します。[プログラム名]>追加>フォーム(Windows Form)の順にクリックします。
image.png

ビューアの設定

 下記のようにGLControlを追加し、イベントを追加します。こちらのサイトを参考にしています。GLControlはデザイナ側でも追加可能ですが、なぜかうまくいかないことが多いので自分はこちらの方法を利用しています。追加したイベントは画面上をカメラ操作するためのものです。右クリック&ドラッグ回転し、ホイール回転で回転中心からの距離を制御します。

Viewer.cs_GLControl系
using System;
using System.Drawing;
using System.Windows.Forms;

// openTK
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;

// surfaceAnalyzer
using SurfaceAnalyzer;

namespace _3dview
{
    public partial class Viewer : Form
    {
        #region Camera__Field

        bool isCameraRotating;      //カメラが回転状態かどうか
        Vector2 current, previous;  //現在の点、前の点
        float zoom = 1.0f;                 //拡大度
        double rotateX = 1, rotateY = 0, rotateZ = 0;//カメラの回転による移動
        float theta = 0;
        float phi = 0;

        #endregion

        public Viewer()
        {
            InitializeComponent();

            AddglControl();
        }

        // glControlの追加
        GLControl glControl;
        private void AddglControl()
        {
            SuspendLayout();

            int width = this.Width;
            int height = this.Height;

            //GLControlの初期化
            glControl = new GLControl();

            glControl.Name = "SHAPE";
            glControl.Size = new Size(width, height);
            glControl.Location = new System.Drawing.Point(0, 0);
            glControl.SendToBack();

            //イベントハンドラ
            glControl.Load += new EventHandler(glControl_Load);
            glControl.Resize += new EventHandler(glControl_Resize);
            glControl.MouseDown += new System.Windows.Forms.MouseEventHandler(this._3DView_MouseDown);
            glControl.MouseMove += new System.Windows.Forms.MouseEventHandler(this._3DView_MouseMove);
            glControl.MouseUp += new System.Windows.Forms.MouseEventHandler(this._3DView_MouseUp);
            glControl.MouseWheel += new System.Windows.Forms.MouseEventHandler(this._3DView_MouseWheel);

            Controls.Add(glControl);

            ResumeLayout(false);

        }

        private void glControl_Load(object sender, EventArgs e)
        {
            GLControl s = (GLControl)sender;
            s.MakeCurrent();

            GL.ClearColor(Color4.White);
            GL.Enable(EnableCap.DepthTest);

            Update();
        }

        private void glControl_Resize(object sender, EventArgs e)
        {
            GL.Viewport(0, 0, glControl.Size.Width, glControl.Size.Height);
            GL.MatrixMode(MatrixMode.Projection);
            Matrix4 projection = Matrix4.CreatePerspectiveFieldOfView((float)Math.PI / 4,
                (float)glControl.Size.Width / (float)glControl.Size.Height, 1.0f, 256.0f);
            GL.LoadMatrix(ref projection);

            Update();
        }

        private void _3DView_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            // 右ボタンが押された場合
            if (e.Button == MouseButtons.Right)
            {
                isCameraRotating = true;
                current = new Vector2(e.X, e.Y);
            }
            Update();
        }


        private void _3DView_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            //右ボタンが押された場合
            if (e.Button == MouseButtons.Right)
            {
                isCameraRotating = false;
                previous = Vector2.Zero;
            }
            Update();
        }


        private void _3DView_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            // カメラが回転状態の場合
            if (isCameraRotating)
            {
                previous = current;
                current = new Vector2(e.X, e.Y);
                Vector2 delta = current - previous;
                delta /= (float)Math.Sqrt(this.Width * this.Width + this.Height * this.Height);
                float length = delta.Length;

                if (length > 0.0)
                {
                    theta += delta.X * 10;
                    phi += delta.Y * 10;
                    rotateX = Math.Cos(theta) * Math.Cos(phi);
                    rotateY = Math.Sin(phi);
                    rotateZ = Math.Sin(theta) * Math.Cos(phi);
                }

                Update();
            }
        }

        private void _3DView_MouseWheel(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            float delta = e.Delta;

            zoom *= (float)Math.Pow(1.001, delta);

            //拡大、縮小の制限
            if (zoom > 4.0f)
                zoom = 4.0f;
            if (zoom < 0.03f)
                zoom = 0.03f;

            Update();
        }
    }
}

3D形状のレンダリングメソッドの追加

 次に、3D形状の表示部を作成します。
 Update()メソッドは、画面の表示の更新があるたびに呼ばれるメソッドです。ここから毎回Render()メソッドを呼び出します。
 Render()メソッドは、画面の表示を変更するメソッドとなります。引数polygonは、SurfaceAnalyzerにより読み込んだ形状となります。このメソッドをControl.cs側から呼ぶことで画面表示を操作します。
 DrawPolygons()メソッドは読み込んだ形状のポリゴンを一つ一つ表示します。GL.Begin()とGL.End()で囲まれた間で色、法線、頂点を与えることで画面上に表示することができます。ここでは、法線の方向に応じて色を面の描画色を指定しています。
 N2TK()メソッドでは、System.NumericsとOpenTKのVector3ベクトルを変換します。

Viewer.cs_画面表示
        PolygonModel Polygon;
        public void Update()
        {
            if (Polygon == null) return;
            Render(Polygon);
        }
        public void Render(PolygonModel polygon)
        {
            Polygon = polygon;

            // バッファのクリア
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

            // カメラ設定
            Vector3 vec_rotate = new Vector3((float)rotateX, (float)rotateY, (float)rotateZ);
            Vector3 center = new Vector3(N2TK(Polygon.GravityPoint()));
            Vector3 eye = center + vec_rotate * center.LengthFast / zoom;
            Matrix4 modelView = Matrix4.LookAt(eye, center, Vector3.UnitY);

            // 表示設定
            GL.MatrixMode(MatrixMode.Modelview);
            GL.LoadMatrix(ref modelView);

            // 3D形状の表示
            DrawPolygons(polygon);

            // バッファの入れ替え
            glControl.SwapBuffers();
        }

        private void DrawPolygons(PolygonModel polygon)
        {
            if (polygon == null) return;

            //描画
            GL.Begin(PrimitiveType.Triangles);

            //三角形を描画
            for (int l = 0; l < polygon.Faces.Count; l++)
            {![Something went wrong]()

                var normal = polygon.Faces[l].Normal();
                GL.Color4(Math.Abs(normal.X), Math.Abs(normal.Y), Math.Abs(normal.Z), 0);
                GL.Normal3(N2TK(normal));
                GL.Vertex3(N2TK(polygon.Faces[l].Vertices[0].P));
                GL.Vertex3(N2TK(polygon.Faces[l].Vertices[2].P));
                GL.Vertex3(N2TK(polygon.Faces[l].Vertices[1].P));
            }
            GL.End();
        }

        // Numerics.Vector3をOpenTK.Vector3に変換します。
        private static OpenTK.Vector3 N2TK(System.Numerics.Vector3 vec3) => new Vector3(vec3.X, vec3.Z, vec3.Y);

画像化メソッドの追加

 OpenTKの表示内容を1ピクセルずつ読み取り、OpenCVSharpのMatに変換します。データを1ピクセルずつ読み込むと遅いため、メモリを直接コピーする手法を採用しています。Marshalを使用するために、usingの追加が必要です。

Viewer.cs_画像化
using System.Runtime.InteropServices;

        // 画像の保存
        public OpenCvSharp.Mat GetMat()
        {
            int width = glControl.Width;
            int height = glControl.Height;

            float[] floatArr = new float[width * height * 3];
            OpenCvSharp.Mat ret = new OpenCvSharp.Mat(height, width, OpenCvSharp.MatType.CV_32FC3);

            // dataBufferへの画像の読み込み
            IntPtr dataBuffer = Marshal.AllocHGlobal(width * height * 12);
            GL.ReadBuffer(ReadBufferMode.Front);
            GL.ReadPixels(0, 0, width, height, PixelFormat.Bgr, PixelType.Float, dataBuffer);

            // imgへの読み込み
            Marshal.Copy(dataBuffer, floatArr, 0, floatArr.Length);

            // opencvsharp.Matへの変換
            Marshal.Copy(floatArr, 0, ret.Data, floatArr.Length);

            // 破棄
            Marshal.FreeHGlobal(dataBuffer);

            return ret;
        }

 以上で、Viewerフォームのコーディングは終了です。

Controlフォームのコーディング

 続いて、Controlフォームのコーディングを行います。

ボタンの配置

 Controlフォーム上にボタンを追加し、それぞれ「ビューアの表示」、「形状の表示」、「保存」とします。
image.png

Viewerフォームの表示

 各ボタンをダブルクリックし、イベントを追加し以下のように記述します。
image.png

 初めに、「ビューアの表示」ボタンをクリックした際の動作を作成します。

Control.cs途中
using System;
using System.Windows.Forms;

namespace _3dview
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        Viewer viewer;
        private void button1_Click(object sender, EventArgs e)
        {
            viewer = new Viewer();
            viewer.Show();
        }
    }
}

3D形状の表示

 続いて形状を読み込み、表示します。

Control.cs
using System;
using System.Windows.Forms;

namespace _3dview
{
    public partial class Control : Form
    {
        public Control()
        {
            InitializeComponent();
        }

        Viewer viewer;
        private void button1_Click(object sender, EventArgs e)
        {
            viewer = new Viewer();
            viewer.Show();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            // 形状の読み込み
            var polygon = SurfaceAnalyzer.LoadData.LoadSTL(@"local\cube3_とんがり2.STL", true);

            // 形状のレンダリング
            viewer.Render(polygon);
        }

        private void button3_Click(object sender, EventArgs e)
        {

        }
    }
}

 実行し、「ビューアの表示」ボタンをクリックしビューアを表示します。そして「形状の表示」ボタンをクリックすると、形状が表示されます。右クリックで回転、ホイールで拡大・縮小が可能なです。
 スクリーンショット 2020-08-14 20.03.42.png

3D形状の画像化

 最後に、Viewerフォームの内容を保存します。

Control.cs_画像化および保存
using OpenCvSharp;
using System;
using System.Windows.Forms;

namespace _3dview
{
    public partial class Control : Form
    {
        public Control()
        {
            InitializeComponent();
        }

        Viewer viewer;
        private void button1_Click(object sender, EventArgs e)
        {
            viewer = new Viewer();
            viewer.Show();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            // 形状の読み込み
            var polygon = SurfaceAnalyzer.LoadData.LoadSTL(@"local\cube3_とんがり2.STL", true);

            // 形状のレンダリング
            viewer.Render(polygon);
        }

        private void button3_Click(object sender, EventArgs e)
        {
            // viewerの画像の取得
            using (Mat mat = viewer.GetMat())
            {
                // 画像の表示
                Cv2.ImShow("mat", mat);

                // 画像の保存
                Cv2.ImWrite(@"local\mat.jpg", mat * 256);
            }
        }
    }
}

 実行[F5]し、「ビューアの表示」、「形状の表示」、「保存」の順にボタンをクリックすると画像が表示され、そのまま保存されます。OpenGLとOpenCVで座標軸の基準が異なるため、上下が反転した状態となります。
スクリーンショット 2020-08-14 20.33.12.png

まとめ

 本記事では、C#でSTLデータを読み込んで、表示・操作し、画像として保存する方法を紹介しました。C#で3D形状を扱いたいときに役に立てると幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

挨拶を返すアプリを作ってみたをやり直した

はじめに

2017年頃にC#...というよりVisual Studioプログラミングのリハビリ目的で「挨拶を返すアプリ」を作ってみた...という記事でわからないまま放置してたところがあったので書き直し。

当時やりたかったこと

  • HelloApp.exeを引数なしで指定すると何も表示しない
  • HelloApp.exeに引を入れて実行すると「Hello. {引数に入れた名前}さん!」と表示する。

プロジェクト概要

1 2
プロジェクト テンプレート コンソールアプリ(.NET Framework)
プロジェクト名 HelloApp
フレームワーク .NET Framework 4.7.2

最初の試行

プログラムの挙動

  • helloman.exeを実行すると「Hello.太郎さん!」と挨拶する。
  • 引数で名前を指定すると「太郎」が「指定した名前」に代わって、挨拶する。

間違ったコード

program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Halloman
{
    class Program
    {
        static void Main(string[] args)
        {
            string yourname;
            if (args.Length > 0) //引数が指定されていればyournameに代入
            {
                yourname = args[0];
            }
            Hello(yourname);
        }

        // 挨拶を表示する処理
        private static void Hello(string name)
        {
            Console.WriteLine("Hello. {0}さん!", name);
        }
    }
}

つまづいたところ

Visual Studioからビルドエラーが出力。

image.png

image.png

未割当のローカル変数 yournameを処理できないよと言われたので、ビルド処理 = コンパイルをするための条件が整ってないんだなと誤解したまま、とりあえず未割当変数をなんとかすりゃいいかなとテキトーに進めてしまった。。

頂いたアドバイス

image.png

悩んでるところ(足りない知識)はそこじゃないよとアドバイスを頂くが、当時いろいろと限界を超えていた為「ふーんなるほど(よくわからん)」状態。
テキトー修正を施し、エラー回避して無理やり動くものにしてしまった。

テキトー修正

static void Main(string[] args)
{
  string yourname = "太郎";
  if (args.Length > 0) //引数が指定されていればyournameに代入
    {
      yourname = args[0];
    }
  Hello(yourname);
}

まぁ未割当変数のエラーが回避できるようになったのでデバッグビルド処理は進んだ。

やりおなし

プログラムの挙動を考え直した。

引数を指定していない場合、複数引数を指定していた場合などのケースが全然作られていなかったので処理を追加。

  • HelloApp.exeを引数なしで指定すると何も表示しない
  • HelloApp.exeに引数を1つ指定して実行すると「Hello. {引数に入れた名前}さん!」と表示する。
  • HelloApp.exeに複数の引数を指定して実行すると「Hello. {引数に入れた名前}さん!」を引数で指定した数の分だけ表示する。

1回目の改修

突っ込みどころはあるが、改修途中のテンポラリも記載しているため、ここについてはスルーして頂けるとありがたい。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace HelloApp
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length == 0)
            {
                ReturnError("挨拶する相手の名前がわかりません。");
            }

            if (args.Length > 0)
            {
                string yourname;
                for (int ti=0; ti<args.Length; ti++)
                {
                    yourname = args[ti];
                    SayHello(yourname);
                }
            }

        }

        // 挨拶を表示する処理
        private static void SayHello(string name)
        {
            Console.WriteLine("Hello. {0}さん!", name);
        }

        private static void ReturnError(string message)
        {
            Console.WriteLine(message);
        }
    }
}

2回目の改修

どうにも無駄が多いので処理の考え方を変えてみた。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace HelloApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // 挨拶を返す処理はMainに書かずに関数に書くようにした
            if (args.Length == 0)
            {
                Console.WriteLine("挨拶する相手の名前がわかりません。");
            } else
            {
                SayHello();
            }
        }

        // 挨拶を表示する処理
        private static void SayHello()
        {
            string[] yourname = System.Environment.GetCommandLineArgs();

            for (int ti = 1; ti < yourname.Length; ti++)
            {
                Console.WriteLine("Hello. {0}さん!", yourname[ti]);
            }
        }
    }
}

まとめ

今思い返すと反省点多いなぁ・

  • 挙動のパターンに漏れがないか?
  • 関数化するならそれなりに処理をまとめよう

お詫び

また、やり直す前の記事をうっかり更新してしまったのでスクショを持ち越す形にさせていただきました。
gentaroさんごめんなさい。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

BlazorでMVVMパターンを試す

はじめに

 Blazorでアプリケーションを開発していて、WPF開発の際に行われているようなMVVMパターンを適用できれば開発が楽になるのではないかと思い試しました。
 サンプルはBlazor Serverアプリとして作成しています。
 事前準備としてMVVMパターンの開発を補助してくれるライブラリであるReactivePropertyをnuget経由でインストールしています。

・環境
.Net Core 3.1
ReactiveProperty 7.2.0

 今回はテキストボックスに文字を入力すると文字数を表示してくれるアプリを作ります。

 striglength.gif

Viewの作成

StringLengthCounter.razor
@page "/stringLengthCounter"
@inject StringLengthCounterViewModel ViewModel

<h1>文字列カウンタ</h1>
<input id="text1" @bind="ViewModel.Text1.Value" @bind:event="oninput" />
<p>@ViewModel.Text2.Value</p>

 Viewに相当するRazor コンポーネントを作成します。文字入力用のテキストボックスと結果の文字数表示部分が存在しています。
 @injectでDIコンテナに登録してあるViewModelのインスタンスを取得します。Viewではコーディングは最小限にしてViewModelのパラメータをバインドすることのみに留めるという方針で作っています。

 Razorコンポーネントにおけるデータバインディングは以下のページを参考にしました。
 ASP.NET Core Blazor データ バインディング

ViewModelの作成

StringLengthCounterViewModel.cs
    /// <summary>
    /// 文字列カウンタのビューモデル
    /// </summary>
    public class StringLengthCounterViewModel
    {
        public ReactivePropertySlim<string> Text1 { get; set; } = new ReactivePropertySlim<string>();

        public ReadOnlyReactivePropertySlim<string> Text2 { get; set; }

        public StringLengthCounterViewModel(StringLengthCounterModel model)
        {
            this.Text1 = model.Text1;
            this.Text2 = model.Text2;
        }
    }

 Viewで扱う項目を持つViewModelを作成します。今回はModelの値をそのままプロパティに代入しているだけの単純なものになっています。
 ModelはコンストラクタインジェクションによってDIコンテナから取得されます。

Modelの作成

    /// <summary>
    /// 文字列カウンタのモデル
    /// </summary>
    public class StringLengthCounterModel
    {
        public ReactivePropertySlim<string> Text1 { get; set; } = new ReactivePropertySlim<string>();

        public ReadOnlyReactivePropertySlim<string> Text2 { get; set; }

        public StringLengthCounterModel()
        {
            this.Text2 =
                Text1
                .Select(x => string.IsNullOrEmpty(x) ? "0文字" : x.Length + "文字")
                .ToReadOnlyReactivePropertySlim();
        }
    }

 Text1をインプットに文字数を調べて結果をText2に出力するModelを作成します。

DIコンテナの登録

Startup.cs
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddServerSideBlazor();
            services.AddScoped<StringLengthCounterModel>();
            services.AddTransient<StringLengthCounterViewModel>();
        }

 作成したViewModelとModelをDIコンテナに登録します。既存のStartup.csにあるConfigureServicesメソッドに追記する形で行います。
 AddScopedAddTransientというメソッドが出てきていますがそれぞれでオブジェクトの寿命が異なります。またBlazor ServerとBlazor WebAssemblyで仕様が異なります。
 以上でテキストボックスに入力した文字数を表示してくれるアプリケーションを作ることができました。

 DIコンテナの詳細は公式サイトに記述してあります。
 ASP.NET Core Blazor 依存関係の挿入

補足:ViewModelからViewへの通知

 上記の要領で作成したアプリケーションのModelをDelayメソッドを使ってText2への通知を100ms遅延させるように変更を加えます。

        public StringLengthCounterModel()
        {
            this.Text2 =
                Text1
                .Select(x => string.IsNullOrEmpty(x) ? "0文字" : x.Length + "文字")
                .Delay(TimeSpan.FromMilliseconds(100))
                .ToReadOnlyReactivePropertySlim();
        }

 100ms遅延して結果が表示されるかと思いきや予想と違う挙動をします。正しく文字列をカウントしていません。

striglength2.gif

 更に先程の処理を書き換えます。

        public StringLengthCounterModel()
        {
            this.Text2 =
                Text1
                .Select(x => string.IsNullOrEmpty(x) ? "0文字" : x.Length + "文字")
                .Delay(TimeSpan.FromMilliseconds(100), Scheduler.Immediate)
                .ToReadOnlyReactivePropertySlim();
        }

 変更した箇所はDelayメソッドの第二引数です。これで予想していたとおり100ms遅延してText2の変更がViewに反映されるようになりました。今回はScheduler.Immediateを指定しましたがDelayメソッドのデフォルトのスケジューラはThreadPoolSchedulerです。どうやらModelでスレッドプールを利用するとViewに変更が自動的に反映されないようです。

 Scheduler.Immediateを指定しなくてもViewModelからの変更を通知する処理をViewに記載することで意図した動作をさせることができます。

@page "/stringLengthCounter"
@inject StringLengthCounterViewModel ViewModel

<h1>文字列カウンタ</h1>

<input id="text1" @bind="ViewModel.Text1.Value" @bind:event="oninput" />

<p>@ViewModel.Text2.Value</p>

@code {
    protected override void OnInitialized()
    {
        ViewModel.Text2.Subscribe(_ => this.InvokeAsync(() => this.StateHasChanged()));
    }
}

 SubscribeメソッドにText2で変更が行われたときの処理を記載します。StateHasChangedメソッドを呼び出すとコンポーネントが再レンダリングされます。これによりText2が変更されると再レンダリングが行われるようになります。InvokeAsyncメソッドはStateHasChangedメソッドをコンポーネントが動作しているスレッドから呼び出すために使用しています。コンポーネントが動作しているスレッドとは違うスレッドでStateHasChangedメソッドを呼び出すと実行時エラーになります。

さいごに

 Blazorを元に簡単なMVVMパターンのアプリケーションの作成を行いました。
 ViewModelからViewへの値の反映を行う場合に工夫する必要があることが分かりました。

今回動作させたソースコード
https://github.com/ttlatex/BlazorMvvmTiny

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# 画像表示

まえがき

Visual StudioでC#アプリケーションを作成するための備忘録

仕様


・ボタンのイベントで画像を選択し、PictureBoxに選択画像を表示する。
・選択画像とPictureBoxのサイズを自動判別し、PictureBox内に選択画像を全面表示する
・出力画像名を入力ファイル名から再生成する。

・ボタンのイベントで画像処理用のメモリを確保
・特定の画像処理を行う
・処理後の画像をPictureBoxに表示する

・ボタンのイベントで処理後の画像を保存する

ソース

        private void button1_Click(object sender, EventArgs e)
        {

            ///////////////////////////////////////////////////////
            /// 画像ファイルを選択
            /////////////////////////////////////////////////////// 
            //ファイルを開くダイアログボックスの作成  
            var ofd = new OpenFileDialog();

            //ファイルフィルタ  
            ofd.Filter = "Image File(*.bmp,*.jpg,*.png,*.tif)|*.bmp;*.jpg;*.png;*.tif|Bitmap(*.bmp)|*.bmp|Jpeg(*.jpg)|*.jpg|PNG(*.png)|*.png";

            //ダイアログの表示 (Cancelボタンがクリックされた場合は何もしない)
            if (ofd.ShowDialog() == DialogResult.Cancel)
            {
            }
            //画像ファイル名の取得
            str_img_full_path = ofd.FileName;
            //ディレクトリ名の取得
            str_img_dir = Path.GetDirectoryName(str_img_full_path);
            //ファイル名の取得(拡張子なし)
            str_img_file = Path.GetFileNameWithoutExtension(str_img_full_path);
            //ファイル名の取得(拡張子あり)
            label1.Text = Path.GetFileName(str_img_full_path);

            //出力ファイル名の生成
            str_out_file = str_img_file + str_out_suffix;
            label2.Text = str_out_file;
            //フルパスの生成
            str_out_full_path = str_img_dir + "\\" + str_out_file;


            ///////////////////////////////////////////////////////
            /// 画像をPictureBoxに表示する
            /// /////////////////////////////////////////////////////// 
            pictureBox1.Image = new Bitmap(pictureBox1.Width, pictureBox1.Height);

            //画像ファイルの読み込み
            isrc = new Bitmap(ofd.FileName);

            //縮小サイズの計算(最大サイズに合わせて縮小)
            double scale_x = ((double)isrc.Width / (double)pictureBox1.Width);
            double scale_y = ((double)isrc.Height / (double)pictureBox1.Height);
            double scale = (scale_x > scale_y) ? scale_x : scale_y;

            //リサイズ画像の作成
            Bitmap bmpResize = new Bitmap(isrc, (int)(isrc.Width / scale), (int)(isrc.Height / scale));

            var g = Graphics.FromImage(pictureBox1.Image);
            g.DrawImage(bmpResize, 0, 0, bmpResize.Width, bmpResize.Height);

        }

        private void button3_Click(object sender, EventArgs e)
        {
            ///////////////////////////////////////////////////////
            /// 画像処理用メモリを確保する
            ///////////////////////////////////////////////////////
            // Bitmapをロック
            var bmpData = isrc.LockBits(new Rectangle(0, 0, isrc.Width, isrc.Height),
                                        System.Drawing.Imaging.ImageLockMode.ReadWrite,
                                        isrc.PixelFormat
                );

            // メモリの幅のバイト数を取得
            var stride = Math.Abs(bmpData.Stride);
            // チャンネル数取得
            var channel = Bitmap.GetPixelFormatSize(isrc.PixelFormat) / 8;


            // 画像格納用配列
            var src_data = new byte[stride * bmpData.Height];
            var dst_data = new byte[stride * bmpData.Height];

            // Bitmapデータをsrc配列へコピー
            System.Runtime.InteropServices.Marshal.Copy(
                bmpData.Scan0,
                src_data,
                0,
                src_data.Length
                );

            //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            //ここにHLS用関数を追加する
            //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

            //Pixel->Beat変換

            int index;

            // 移動平均処理
            for (int j = 1; j < bmpData.Height - 1; j++)
            {
                for (int i = channel; i < (bmpData.Width - 1) * channel; i++)
                {
                    index = i + j * stride;
                    dst_data[index]
                        = (byte)((
                            src_data[index - channel - stride] + src_data[index - stride] + src_data[index + channel - stride]
                          + src_data[index - channel] + src_data[index] + src_data[index + channel]
                          + src_data[index - channel + stride] + src_data[index + stride] + src_data[index + channel + stride]
                          ) / 9);
                }
            }


            // 配列をBitmapデータへコピー
            System.Runtime.InteropServices.Marshal.Copy(
                dst_data,
                0,
                bmpData.Scan0,
                dst_data.Length
            );

            // アンロック
            isrc.UnlockBits(bmpData);

            ///////////////////////////////////////////////////////
            /// 処理画像をPictureBox2に表示する
            /// /////////////////////////////////////////////////////// 
            pictureBox2.Image = new Bitmap(pictureBox2.Width, pictureBox2.Height);


            //縮小サイズの計算(最大サイズに合わせて縮小)
            double scale_x = ((double)isrc.Width / (double)pictureBox1.Width);
            double scale_y = ((double)isrc.Height / (double)pictureBox1.Height);
            double scale = (scale_x > scale_y) ? scale_x : scale_y;

            //リサイズ画像の作成
            Bitmap bmpResize = new Bitmap(isrc, (int)(isrc.Width / scale), (int)(isrc.Height / scale));

            var g = Graphics.FromImage(pictureBox2.Image);
            g.DrawImage(bmpResize, 0, 0, bmpResize.Width, bmpResize.Height);


        }

        private void button4_Click(object sender, EventArgs e)
        {
            isrc.Save(str_out_full_path,System.Drawing.Imaging.ImageFormat.Bmp);
            label2.Text = "Complete Save Image";
        }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WPF 「MahApps.Metro」を使ってWPFアプリケーションをModernUIにしてみる(Ver2.00以降)

今作っているWPFアプリケーションにMahApps.Metroを入れるとひとまずモダンなUIになります。
さくっと入るので見た目ちょっと違ったアプリ作るのに便利です。
(VitualStudio2017でテストしました)

手順

  1. MahApps.Metroを入れる
  2. App.xamlを修正
  3. メインウィンドウのXAMLを修正
  4. メインウィンドウのコードビハインドファイルを修正

1.MahApps.Metroを入れる

  1. 「プロジェクトエクスプローラ」→「参照設定」を右クリックして「NuGetパッケージ管理」を開きます。
  2. ウィンドウ右上のオンラインの検索に「MahApps」と入れてやると出てきます。インストールしましょう。
  3. 今回入れたのはバージョン2.0.0.0です。

※NuGetがよくわかんない場合は「VisualStudio NuGet」あたりでググると出てきます。使いたいモジュールをダウンロード,インストールしてくれる便利なやつです。

2.App.xamlを修正

  • リソースディレクショナリを追加します。
xaml(追加前)
<Application x:Class="WpfApplicationTest.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">
    <Application.Resources>

    </Application.Resources>
</Application>
xaml(追加後)
<Application x:Class="WpfApplicationTest.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

3. メインウィンドウのXAMLを修正

  • 二箇所変更します。

1.ネームスペースの追加
WindowにMahApps.Metroを追加します。

xaml
 xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"

2.Windowクラスの変更
WindowControls:MetroWindow にします。

xaml(変更前)
<Window x:Class="WpfApplicationTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
        Title="MainWindow" Height="350" Width="525">
xaml(変更後)
<Controls:MetroWindow x:Class="WpfApplicationTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
        Title="MainWindow" Height="350" Width="525">

4. メインウィンドウのコードビハインドファイルを修正

  • xamlでクラスを変更したので、コード側のクラスも合わせます。
cpp(変更前)
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }

変更後

cpp(変更後)
    public partial class MainWindow : MahApps.Metro.Controls.MetroWindow
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }

はい、モダンになりました。青色のウィンドウになったと思います。

おまけ 色を変えたいとき

App.xaml
<Application x:Class="WpfApplicationTest.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
           ココ→ <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

「ココ」の行を変更すれば色が変わります。

<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/ベース.色.xaml" />
ベース
  • "Light" :白
  • "Dark" :黒

Red, Green, Blue, Purple, Orange, Lime, Emerald, Teal, Cyan, Cobalt, Indigo, Violet, Pink, Magenta, Crimson, Amber, Yellow, Brown, Olive, Steel, Mauve, Taupe, Sienna

黒ベースで赤にしたい場合
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Dark.Red.xaml" />
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Prism の ViewModelLocator を学ぶ

RRISM LIBRARY の Documentation から ViewModelLocator の箇所を学んでみます。

そのまんまの翻訳ですが、メモとして。
日本語がへんなところは英語に戻ってそれなりに理解。。。

The ViewModelLocator is used to wire the DataContext of a view to an instance of a ViewModel using a standard naming convention.

The Prism ViewModelLocator has an AutoWireViewModel attached property, that when set to true calls the AutoWireViewModelChanged method in the ViewModelLocationProvider class to resolve the ViewModel for the view, and then set the view’s data context to an instance of that ViewModel.

Add the AutoWireViewModel attached property to each View:
(Google翻訳)
ViewModelLocatorは、標準の命名規則を使用して、ビューのDataContextをViewModelのインスタンスにワイヤリングするために使用されます。

Prism ViewModelLocatorにはAutoWireViewModel添付プロパティがあり、trueに設定すると、ViewModelLocationProviderクラスのAutoWireViewModelChangedメソッドを呼び出してビューのViewModelを解決し、ビューのデータコンテキストをそのViewModelのインスタンスに設定します。

AutoWireViewModel添付プロパティを各ビューに追加します。

<Window x:Class="Demo.Views.MainWindow"
    ...
    xmlns:prism="http://prismlibrary.com/"
    prism:ViewModelLocator.AutoWireViewModel="True">

To locate a ViewModel, the ViewModelLocationProvider first attempts to resolve the ViewModel from any mappings that may have been registered by the ViewModelLocationProvider.Register method (See Custom ViewModel Registrations). If the ViewModel cannot be resolved using this approach, the ViewModelLocationProvider falls back to a convention-based approach to resolve the correct ViewModel type.

This convention assumes:

・that ViewModels are in the same assembly as the view types
・that ViewModels are in a .ViewModels child namespace
・that views are in a .Views child namespace
・that ViewModel names correspond with view names and end with "ViewModel."
(Google翻訳)
ViewModelを見つけるために、ViewModelLocationProviderはまず、ViewModelLocationProvider.Registerメソッドによって登録されている可能性のあるマッピングからViewModelを解決しようとします(カスタムViewModel登録を参照)。 このアプローチを使用してViewModelを解決できない場合、ViewModelLocationProviderは、正しいViewModelタイプを解決するために、コンベンションベースのアプローチにフォールバックします。

この規則は、以下を前提としています。

・ViewModelがビュータイプと同じアセンブリにあること
・ViewModelsが.ViewModels子名前空間にあること
・そのビューが.Views子名前空間にあること
・ViewModel名がビュー名に対応し、「ViewModel」で終わること。

NOTE
The ViewModelLocationProvider can be found in the Prism.Mvvm namespace in the Prism.Core NuGet package. The ViewModelLocator can be found in the Prism.Mvvm namespace in the platform specific packages (Prism.WPF, Prism.Forms) NuGet package.
(Google翻訳)
注意
ViewModelLocationProviderは、Prism.Core NuGetパッケージのPrism.Mvvm名前空間にあります。 ViewModelLocatorは、プラットフォーム固有のパッケージ(Prism.WPF、Prism.Forms)NuGetパッケージのPrism.Mvvm名前空間にあります。

NOTE
The ViewModelLocator is required, and automatically applied to every View, when developing with Xamarin.Forms as it is responsible for providing the correct instance of the INavigationService to the ViewModel. When developing a Xamarin.Forms app, the ViewModelLocator is opt-out only.
(Google翻訳)
注意
Xamarin.Formsを使用して開発する場合、ViewModelLocatorは必須であり、INavigationServiceの正しいインスタンスをViewModelに提供する必要があるため、すべてのビューに自動的に適用されます。 Xamarin.Formsアプリを開発する場合、ViewModelLocatorはオプトアウトのみです。

image.png

Change the Naming Convention

If your application does not follow the ViewModelLocator default naming convention, you can change the convention to meet the requirements of your application. The ViewModelLocationProvider class provides a static method called SetDefaultViewTypeToViewModelTypeResolver that can be used to provide your own convention for associating views to view models.

To change the ViewModelLocator naming convention, override the ConfigureViewModelLocator method in the App.xaml.cs class. Then provide your custom naming convention logic in the ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver method.
(Google翻訳)
アプリケーションがViewModelLocatorのデフォルトの命名規則に従っていない場合は、アプリケーションの要件を満たすように規則を変更できます。 ViewModelLocationProviderクラスは、ビューをビューモデルに関連付けるための独自の規則を提供するために使用できるSetDefaultViewTypeToViewModelTypeResolverと呼ばれる静的メソッドを提供します。

ViewModelLocatorの命名規則を変更するには、App.xaml.csクラスのConfigureViewModelLocatorメソッドをオーバーライドします。 次に、ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolverメソッドでカスタムの命名規則ロジックを提供します。

protected override void ConfigureViewModelLocator()
{
    base.ConfigureViewModelLocator();

    ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) =>
    {
        var viewName = viewType.FullName.Replace(".ViewModels.", ".CustomNamespace.");
        var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
        var viewModelName = $"{viewName}ViewModel, {viewAssemblyName}";
        return Type.GetType(viewModelName);
    });
}

image.png

Custom ViewModel Registrations

There may be instances where your app is following the ViewModelLocator default naming convention, but you have a number of ViewModels that do not follow the convention. Instead of trying to customize the naming convention logic to conditionally meet all your naming requirments, you can register a mapping for a ViewModel to a specific view directly with the ViewModelLocator by using the ViewModelLocationProvider.Register method.

The following examples show the various ways to create a mapping between a view called MainWindow and a ViewModel named CustomViewModel.
(Google翻訳)
アプリがViewModelLocatorのデフォルトの命名規則に従っている場合がありますが、規則に従っていないViewModelがいくつかあります。 すべての命名要件を条件付きで満たすように命名規則ロジックをカスタマイズする代わりに、ViewModelLocationProvider.Registerメソッドを使用して、ViewModelLocatorで特定のビューへのViewModelのマッピングを直接登録できます。

次の例は、MainWindowというビューとCustomViewModelという名前のViewModelの間のマッピングを作成するさまざまな方法を示しています。

Type / Type

ViewModelLocationProvider.Register(typeof(MainWindow).ToString(), typeof(CustomViewModel));

Type / Factory

ViewModelLocationProvider.Register(typeof(MainWindow).ToString(), () => Container.Resolve<CustomViewModel>());

Generic Factory

ViewModelLocationProvider.Register<MainWindow>(() => Container.Resolve<CustomViewModel>());

Generic Type

ViewModelLocationProvider.Register<MainWindow, CustomViewModel>();

NOTE
Registering your ViewModels directly with the ViewModelLocator is faster than relying on the default naming convention. This is because the naming convention requires the use of reflection, while a custom mapping provides the type directly to the ViewModelLocator.
(Google翻訳)
注意
ViewModelLocatorに直接ViewModelを登録する方が、デフォルトの命名規則に依存するよりも高速です。 これは、命名規則ではリフレクションを使用する必要があるためですが、カスタムマッピングでは、タイプがViewModelLocatorに直接提供されます。

IMPORTANT
The viewTypeName parameter must be the fully qualifyied name of the view's Type (Type.ToString()). Otherwise the mapping will fail.
(Google翻訳)
重要
viewTypeNameパラメータは、ビューのタイプの完全修飾名(Type.ToString())である必要があります。 そうでない場合、マッピングは失敗します。

image.png

Control how ViewModels are Resolved

By default, the ViewModelLocator will use the DI container you have chosen to create your Prism application to resolve ViewModels. However, if you ever have the need to customize how ViewModels are resolved or change the resolver altogether, you can achieve this by using the ViewModelLocationProvider.SetDefaultViewModelFactory method.

This example shows how you might change the container used for resolving the ViewModel instances.
(Google翻訳)
デフォルトでは、ViewModelLocatorは、選択したDIコンテナを使用して、Prismアプリケーションを作成し、ViewModelを解決します。 ただし、ViewModelの解決方法をカスタマイズしたり、リゾルバーを完全に変更したりする必要がある場合は、ViewModelLocationProvider.SetDefaultViewModelFactoryメソッドを使用してこれを実現できます。

この例は、ViewModelインスタンスの解決に使用されるコンテナーを変更する方法を示しています。

protected override void ConfigureViewModelLocator()
{
    base.ConfigureViewModelLocator();

    ViewModelLocationProvider.SetDefaultViewModelFactory(viewModelType) =>
    {
        return MyAwesomeNewContainer.Resolve(viewModelType);
    });
}

This is an example of how you might check the type of the view the ViewModel is being created for, and performing logic to control how the ViewModel is created.
(Google翻訳)
これは、ViewModelが作成されているビューのタイプを確認し、ViewModelの作成方法を制御するロジックを実行する方法の例です。

protected override void ConfigureViewModelLocator()
{
    base.ConfigureViewModelLocator();

    ViewModelLocationProvider.SetDefaultViewModelFactory((view, viewModelType) =>
    {
        switch (view)
        {
            case Window window:
                //your logic
                break;
            case UserControl userControl:
                //your logic
                break;
        }

        return MyAwesomeNewContainer.Resolve(someNewType);
    });
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Prism の Event Aggregator を学ぶ

RRISM LIBRARY の Documentation から Event Aggregator の箇所を学んでみます。

そのまんまの翻訳ですが、メモとして。
日本語がへんなところは英語に戻ってそれなりに理解。。。

The Prism Library provides an event mechanism that enables communications between loosely coupled components in the application. This mechanism, based on the event aggregator service, allows publishers and subscribers to communicate through events and still do not have a direct reference to each other.

The EventAggregator provides multicast publish/subscribe functionality. This means there can be multiple publishers that raise the same event and there can be multiple subscribers listening to the same event. Consider using the EventAggregator to publish an event across modules and when sending a message between business logic code, such as controllers and presenters.

Events created with the Prism Library are typed events. This means you can take advantage of compile-time type checking to detect errors before you run the application. In the Prism Library, the EventAggregator allows subscribers or publishers to locate a specific EventBase. The event aggregator also allows for multiple publishers and multiple subscribers, as shown in the following illustration.
(Google翻訳)
Prism Libraryは、アプリケーション内の疎結合コンポーネント間の通信を可能にするイベントメカニズムを提供します。イベントアグリゲーターサービスに基づくこのメカニズムにより、パブリッシャーとサブスクライバーはイベントを介して通信できますが、相互に直接参照することはできません。

EventAggregatorは、マルチキャストパブリッシュ/サブスクライブ機能を提供します。つまり、同じイベントを発生させる複数のパブリッシャーが存在し、同じイベントをリッスンする複数のサブスクライバーが存在する可能性があります。 EventAggregatorを使用してモジュール間でイベントを発行すること、およびコントローラーやプレゼンターなどのビジネスロジックコード間でメッセージを送信することを検討してください。

Prism Libraryで作成されたイベントは型付きイベントです。これは、アプリケーションを実行する前に、コンパイル時の型チェックを利用してエラーを検出できることを意味します。 Prism Libraryでは、EventAggregatorによってサブスクライバーまたはパブリッシャーが特定のEventBaseを見つけることができます。イベントアグリゲーターでは、次の図に示すように、複数のパブリッシャーとサブスクライバーも使用できます。
image.png

image.png

IEventAggregator

The EventAggregator class is offered as a service in the container and can be retrieved through the IEventAggregatorinterface. The event aggregator is responsible for locating or building events and for keeping a collection of the events in the system.
(Google翻訳)
EventAggregatorクラスはコンテナー内のサービスとして提供され、IEventAggregatorインターフェースを介して取得できます。 イベントアグリゲーターは、イベントの検索または構築、およびシステム内のイベントのコレクションの保持を担当します。

public interface IEventAggregator
{
    TEventType GetEvent<TEventType>() where TEventType : EventBase;
}

The EventAggregator constructs the event on its first access if it has not already been constructed. This relieves the publisher or subscriber from needing to determine whether the event is available.
(Google翻訳)
EventAggregatorは、まだ構築されていない場合、最初のアクセスでイベントを構築します。 これにより、パブリッシャーまたはサブスクライバーは、イベントが利用可能かどうかを判断する必要がなくなります。

PubSubEvent

The real work of connecting publishers and subscribers is done by the PubSubEvent class. This is the only implementation of the EventBase class that is included in the Prism Library. This class maintains the list of subscribers and handles event dispatching to the subscribers.

The PubSubEvent class is a generic class that requires the payload type to be defined as the generic type. This helps enforce, at compile time, that publishers and subscribers provide the correct methods for successful event connection. The following code shows a partial definition of the PubSubEvent class.
(Google翻訳)
パブリッシャーとサブスクライバーを接続する実際の作業は、PubSubEventクラスによって行われます。 これは、Prism Libraryに含まれているEventBaseクラスの唯一の実装です。 このクラスは、サブスクライバーのリストを維持し、サブスクライバーへのイベントディスパッチを処理します。

PubSubEventクラスは、ペイロードタイプをジェネリックタイプとして定義する必要があるジェネリッククラスです。 これにより、パブリッシャーとサブスクライバーがイベント接続を成功させるための適切なメソッドを提供するように、コンパイル時に実行できます。 次のコードは、PubSubEventクラスの部分的な定義を示しています。

NOTE
PubSubEvent can be found in the Prism.Events namespace which is located in the Prism.Core NuGet package.
(Google翻訳)
注意
PubSubEventは、Prism.Core NuGetパッケージにあるPrism.Events名前空間にあります。

Creating an Event

The PubSubEvent is intended to be the base class for an application's or module's specific events. TPayLoad is the type of the event's payload. The payload is the argument that will be passed to subscribers when the event is published.

For example, the following code shows the TickerSymbolSelectedEvent. The payload is a string containing the company symbol. Notice how the implementation for this class is empty.
(Google翻訳)
PubSubEvent は、アプリケーションまたはモジュールの特定のイベントの基本クラスになることを目的としています。 TPayLoadは、イベントのペイロードのタイプです。 ペイロードは、イベントが発行されたときにサブスクライバーに渡される引数です。

たとえば、次のコードはTickerSymbolSelectedEventを示しています。 ペイロードは、会社のシンボルを含む文字列です。 このクラスの実装が空であることに注意してください。

public class TickerSymbolSelectedEvent : PubSubEvent<string>{}

NOTE
In a composite application, the events are frequently shared between multiple modules, so they are defined in a common place. It is common practice to define these events in a shared assembly such as a "Core" or "Infrastructure" project.
(Google翻訳)
注意
複合アプリケーションでは、イベントは複数のモジュール間で頻繁に共有されるため、共通の場所で定義されます。 「コア」プロジェクトや「インフラストラクチャ」プロジェクトなどの共有アセンブリでこれらのイベントを定義することは一般的な方法です。

Publishing an Event

Publishers raise an event by retrieving the event from the EventAggregator and calling the Publish method. To access the EventAggregator, you can use dependency injection by adding a parameter of type IEventAggregator to the class constructor.
(Google翻訳)
パブリッシャーは、EventAggregatorからイベントを取得し、Publishメソッドを呼び出すことにより、イベントを発生させます。 EventAggregatorにアクセスするには、クラスコンストラクターにIEventAggregator型のパラメーターを追加することで、依存性注入を使用できます。

public class MainPageViewModel
{
    IEventAggregator _eventAggregator;
    public MainPageViewModel(IEventAggregator ea)
    {
        _eventAggregator = ea;
    }
}

The following code demonstrates publishing the TickerSymbolSelectedEvent.
(Google翻訳)
以下のコードは、TickerSymbolSelectedEventの公開を示しています。

_eventAggregator.GetEvent<TickerSymbolSelectedEvent>().Publish("STOCK0");

Subscribing to Events

Subscribers can enlist with an event using one of the Subscribe method overloads available on the PubSubEvent class.
(Google翻訳)
サブスクライバーは、PubSubEventクラスで利用可能なSubscribeメソッドオーバーロードの1つを使用してイベントに参加できます。

public class MainPageViewModel
{
    public MainPageViewModel(IEventAggregator ea)
    {
        ea.GetEvent<TickerSymbolSelectedEvent>().Subscribe(ShowNews);
    }

    void ShowNews(string companySymbol)
    {
        //implement logic
    }
}

There are several ways to subscribe to PubSubEvents. Use the following criteria to help determine which option best suits your needs:

・If you need to be able to update UI elements when an event is received, subscribe to receive the event on the UI thread.
・If you need to filter an event, provide a filter delegate when subscribing.
・If you have performance concerns with events, consider using strongly referenced delegates when subscribing and then manually unsubscribe from the PubSubEvent.
・If none of the preceding is applicable, use a default subscription.

The following sections describe these options.
(Google翻訳)
PubSubEventsをサブスクライブする方法はいくつかあります。 次の基準を使用して、ニーズに最適なオプションを決定してください。

・イベント受信時にUI要素を更新できるようにする必要がある場合は、UIスレッドでイベントを受信するようサブスクライブしてください。
・イベントをフィルタリングする必要がある場合は、サブスクライブ時にフィルターデリゲートを提供してください。
・イベントに関するパフォーマンスの問題がある場合は、サブスクライブするときに強く参照されるデリゲートを使用することを検討してから、PubSubEventから手動でサブスクライブを解除してください。
・上記に該当しない場合は、デフォルトのサブスクリプションをご利用ください。

次のセクションでは、これらのオプションについて説明します。

Subscribing on the UI Thread

Frequently, subscribers will need to update UI elements in response to events. In WPF, only a UI thread can update UI elements.

By default, the subscriber receives the event on the publisher's thread. If the publisher sends the event from the UI thread, the subscriber can update the UI. However, if the publisher's thread is a background thread, the subscriber may be unable to directly update UI elements. In this case, the subscriber would need to schedule the updates on the UI thread using the Dispatcher class.

The PubSubEvent provided with the Prism Library can assist by allowing the subscriber to automatically receive the event on the UI thread. The subscriber indicates this during subscription, as shown in the following code example.
(Google翻訳)
多くの場合、サブスクライバーはイベントに応答してUI要素を更新する必要があります。 WPFでは、UIスレッドのみがUI要素を更新できます。

デフォルトでは、サブスクライバーはパブリッシャーのスレッドでイベントを受け取ります。 パブリッシャーがUIスレッドからイベントを送信する場合、サブスクライバーはUIを更新できます。 ただし、パブリッシャーのスレッドがバックグラウンドスレッドの場合、サブスクライバーはUI要素を直接更新できない場合があります。 この場合、サブスクライバーはDispatcherクラスを使用してUIスレッドで更新をスケジュールする必要があります。

Prism Libraryで提供されるPubSubEventは、サブスクライバーがUIスレッドでイベントを自動的に受信できるようにすることで支援できます。 次のコード例に示すように、サブスクライバーはサブスクリプション中にこれを示します。

public class MainPageViewModel
{
    public MainPageViewModel(IEventAggregator ea)
    {
        ea.GetEvent<TickerSymbolSelectedEvent>().Subscribe(ShowNews, ThreadOption.UIThread);
    }

    void ShowNews(string companySymbol)
    {
        //implement logic
    }
}

The following options are available for ThreadOption:

・PublisherThread: Use this setting to receive the event on the publishers' thread. This is the default setting.
・BackgroundThread: Use this setting to asynchronously receive the event on a .NET Framework thread-pool thread.
・UIThread: Use this setting to receive the event on the UI thread.
(Google翻訳)
ThreadOptionには次のオプションがあります。

・PublisherThread:この設定を使用して、発行者のスレッドでイベントを受信します。 これがデフォルトの設定です。
・BackgroundThread:.NET Frameworkのスレッドプールスレッドで非同期にイベントを受信するには、この設定を使用します。
・UIThread:この設定を使用して、UIスレッドでイベントを受信します。

NOTE
In order for PubSubEvent to publish to subscribers on the UI thread, the EventAggregator must initially be constructed on the UI thread.
(Google翻訳)
注意
PubSubEventをUIスレッドのサブスクライバーに公開するには、最初にEventAggregatorをUIスレッドで構築する必要があります。

Subscription Filtering

Subscribers may not need to handle every instance of a published event. In these cases, the subscriber can use the filter parameter. The filter parameter is of type System.Predicate and is a delegate that gets executed when the event is published to determine if the payload of the published event matches a set of criteria required to have the subscriber callback invoked. If the payload does not meet the specified criteria, the subscriber callback is not executed.

Frequently, this filter is supplied as a lambda expression, as shown in the following code example.
(Google翻訳)
サブスクライバーは、発行されたイベントのすべてのインスタンスを処理する必要がない場合があります。 このような場合、サブスクライバーはフィルターパラメーターを使用できます。 フィルターパラメーターはSystem.Predicate 型で、イベントが発行されたときに実行され、発行されたイベントのペイロードがサブスクライバーコールバックを呼び出すために必要な一連の基準に一致するかどうかを判断するデリゲートです。 ペイロードが指定された基準を満たさない場合、サブスクライバーコールバックは実行されません。

次のコード例に示すように、このフィルターはラムダ式として提供されることがよくあります。

public class MainPageViewModel
{
    public MainPageViewModel(IEventAggregator ea)
    {
        TickerSymbolSelectedEvent tickerEvent = ea.GetEvent<TickerSymbolSelectedEvent>();
        tickerEvent.Subscribe(ShowNews, ThreadOption.UIThread, false, 
                              companySymbol => companySymbol == "STOCK0");
    }

    void ShowNews(string companySymbol)
    {
        //implement logic
    }
}

NOTE
The Subscribe method returns a subscription token of type Prism.Events.SubscriptionToken that can be used to remove a subscription to the event later. This token is particularly useful when you are using anonymous delegates or lambda expressions as the callback delegate or when you are subscribing the same event handler with different filters.
(Google翻訳)
注意
Subscribeメソッドは、Prism.Events.SubscriptionTokenタイプのサブスクリプショントークンを返します。これを使用して、後でイベントへのサブスクリプションを削除できます。 このトークンは、匿名デリゲートまたはラムダ式をコールバックデリゲートとして使用する場合、または同じイベントハンドラーを異なるフィルターでサブスクライブする場合に特に役立ちます。

NOTE
It is not recommended to modify the payload object from within a callback delegate because several threads could be accessing the payload object simultaneously. You could have the payload be immutable to avoid concurrency errors.
(Google翻訳)
注意
複数のスレッドがペイロードオブジェクトに同時にアクセスしている可能性があるため、コールバックデリゲート内からペイロードオブジェクトを変更することはお勧めしません。 同時実行エラーを回避するために、ペイロードを不変にすることができます。

Subscribing Using Strong References

If you are raising multiple events in a short period of time and have noticed performance concerns with them, you may need to subscribe with strong delegate references. If you do that, you will then need to manually unsubscribe from the event when disposing the subscriber.

By default, PubSubEvent maintains a weak delegate reference to the subscriber's handler and filter on subscription. This means the reference that PubSubEvent holds on to will not prevent garbage collection of the subscriber. Using a weak delegate reference relieves the subscriber from the need to unsubscribe and allows for proper garbage collection.

However, maintaining this weak delegate reference is slower than a corresponding strong reference. For most applications, this performance will not be noticeable, but if your application publishes a large number of events in a short period of time, you may need to use strong references with PubSubEvent. If you do use strong delegate references, your subscriber should unsubscribe to enable proper garbage collection of your subscribing object when it is no longer used.

To subscribe with a strong reference, use the keepSubscriberReferenceAlive parameter on the Subscribe method, as shown in the following code example.
(Google翻訳)
短期間に複数のイベントを発生させ、パフォーマンスの問題に気付いた場合は、強力なデリゲートリファレンスをサブスクライブする必要がある場合があります。その場合は、サブスクライバーを破棄するときに、イベントから手動でサブスクライブを解除する必要があります。

デフォルトでは、PubSubEventはサブスクライバーのハンドラーへの弱いデリゲート参照を維持し、サブスクリプションをフィルターします。つまり、PubSubEventが保持する参照は、サブスクライバーのガベージコレクションを妨げません。弱いデリゲート参照を使用すると、サブスクライバーがサブスクライブを解除する必要がなくなり、適切なガベージコレクションが可能になります。

ただし、この弱いデリゲート参照の維持は、対応する強い参照よりも遅くなります。ほとんどのアプリケーションでは、このパフォーマンスは目立ちませんが、アプリケーションが短期間に大量のイベントを発行する場合は、PubSubEventで強力な参照を使用する必要がある場合があります。強いデリゲート参照を使用する場合、サブスクライバーはサブスクライブを解除して、サブスクライブオブジェクトが使用されなくなったときに適切なガベージコレクションを有効にする必要があります。

強参照でサブスクライブするには、次のコード例に示すように、SubscribeメソッドでkeepSubscriberReferenceAliveパラメータを使用します。

public class MainPageViewModel
{
    public MainPageViewModel(IEventAggregator ea)
    {
        bool keepSubscriberReferenceAlive = true;
        TickerSymbolSelectedEvent tickerEvent = ea.GetEvent<TickerSymbolSelectedEvent>();
        tickerEvent.Subscribe(ShowNews, ThreadOption.UIThread, keepSubscriberReferenceAlive, 
                              companySymbol => companySymbol == "STOCK0");
    }

    void ShowNews(string companySymbol)
    {
        //implement logic
    }
}

The keepSubscriberReferenceAlive parameter is of type bool:

・When set to true, the event instance keeps a strong reference to the subscriber instance, thereby not allowing it to get garbage collected. For information about how to unsubscribe, see the section Unsubscribing from an Event later in this topic.
・When set to false (the default value when this parameter omitted), the event maintains a weak reference to the subscriber instance, thereby allowing the garbage collector to dispose the subscriber instance when there are no other references to it. When the subscriber instance gets collected, the event is automatically unsubscribed.
(Google翻訳)
keepSubscriberReferenceAliveパラメーターのタイプはboolです。

・trueに設定すると、イベントインスタンスはサブスクライバーインスタンスへの強い参照を維持するため、ガベージコレクションを実行できません。 サブスクライブを解除する方法については、このトピックの後半の「イベントからのサブスクライブ解除」セクションを参照してください。
・false(このパラメーター省略時のデフォルト値)に設定すると、イベントはサブスクライバーインスタンスへの弱い参照を維持するため、ガベージコレクターは他に参照がない場合にサブスクライバーインスタンスを破棄できます。 サブスクライバーインスタンスが収集されると、イベントは自動的にサブスクライブ解除されます。

Unsubscribing from an Event

If your subscriber no longer wants to receive events, you can unsubscribe by using your subscriber's handler or you can unsubscribe by using a subscription token.

The following code example shows how to directly unsubscribe to the handler.
(Google翻訳)
サブスクライバーがイベントを受信する必要がなくなった場合は、サブスクライバーのハンドラーを使用してサブスクライブを解除するか、サブスクリプショントークンを使用してサブスクライブを解除できます。

次のコード例は、ハンドラーを直接サブスクライブ解除する方法を示しています。

public class MainPageViewModel
{
    TickerSymbolSelectedEvent _event;
    public MainPageViewModel(IEventAggregator ea)
    {
        _event = ea.GetEvent<TickerSymbolSelectedEvent>();
        _event.Subscribe(ShowNews);
    }

    void Unsubscribe()
    {
        _event.Unsubscribe(ShowNews);
    }

    void ShowNews(string companySymbol)
    {
        //implement logic
    }
}

The following code example shows how to unsubscribe with a subscription token. The token is supplied as a return value from the Subscribe method.
(Google翻訳)
次のコード例は、サブスクリプショントークンを使用してサブスクライブを解除する方法を示しています。 トークンは、Subscribeメソッドからの戻り値として提供されます。

public class MainPageViewModel
{
    TickerSymbolSelectedEvent _event;
    SubscriptionToken _token;
    public MainPageViewModel(IEventAggregator ea)
    {
        _event = ea.GetEvent<TickerSymbolSelectedEvent>();
        _token = _event.Subscribe(ShowNews);
    }

    void Unsubscribe()
    {
        _event.Unsubscribe(_token);
    }

    void ShowNews(string companySymbol)
    {
        //implement logic
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Prism の Composite Commands を学ぶ

RRISM LIBRARY の Documentation から Composite Commands の箇所を学んでみます。

そのまんまの翻訳ですが、メモとして。
日本語がへんなところは英語に戻ってそれなりに理解。。。

In many cases, a command defined by a view model will be bound to controls in the associated view so that the user can directly invoke the command from within the view. However, in some cases, you may want to be able to invoke commands on one or more view models from a control in a parent view in the application's UI.

For example, if your application allows the user to edit multiple items at the same time, you may want to allow the user to save all the items using a single command represented by a button in the application's toolbar or ribbon. In this case, the Save All command will invoke each of the Save commands implemented by the view model instance for each item as shown in the following illustration.
(Google翻訳)
多くの場合、ビューモデルによって定義されたコマンドは、関連付けられたビューのコントロールにバインドされるため、ユーザーはビュー内から直接コマンドを呼び出すことができます。 ただし、場合によっては、アプリケーションのUIの親ビューにあるコントロールから1つ以上のビューモデルのコマンドを呼び出せるようにしたい場合があります。

たとえば、アプリケーションでユーザーが同時に複数のアイテムを編集できるようにする場合、ユーザーがアプリケーションのツールバーまたはリボンのボタンで表される単一のコマンドを使用してすべてのアイテムを保存できるようにすることができます。 この場合、次の図に示すように、[すべて保存]コマンドは、各アイテムのビューモデルインスタンスによって実装された各保存コマンドを呼び出します。

image.png

Prism supports this scenario through the CompositeCommand class.

The CompositeCommand class represents a command that is composed from multiple child commands. When the composite command is invoked, each of its child commands is invoked in turn. It is useful in situations where you need to represent a group of commands as a single command in the UI or where you want to invoke multiple commands to implement a logical command.

The CompositeCommand class maintains a list of child commands (DelegateCommand instances). The Execute method of the CompositeCommand class simply calls the Execute method on each of the child commands in turn. The CanExecute method similarly calls the CanExecute method of each child command, but if any of the child commands cannot be executed, the CanExecute method will return false. In other words, by default, a CompositeCommand can only be executed when all the child commands can be executed.
(Google翻訳)
Prismは、CompositeCommandクラスを通じてこのシナリオをサポートします。

CompositeCommandクラスは、複数の子コマンドから構成されるコマンドを表します。 複合コマンドが呼び出されると、その子コマンドのそれぞれが順番に呼び出されます。 これは、UIでコマンドのグループを単一のコマンドとして表す必要がある場合や、論理コマンドを実装するために複数のコマンドを呼び出す必要がある場合に役立ちます。

CompositeCommandクラスは、子コマンド(DelegateCommandインスタンス)のリストを保持します。 CompositeCommandクラスのExecuteメソッドは、各子コマンドのExecuteメソッドを順番に呼び出すだけです。 CanExecuteメソッドは、同様に各子コマンドのCanExecuteメソッドを呼び出しますが、実行できない子コマンドがある場合、CanExecuteメソッドはfalseを返します。 つまり、デフォルトでは、すべての子コマンドを実行できる場合にのみ、CompositeCommandを実行できます。

NOTE
CompositeCommand can be found in the Prism.Commands namespace which is located in the Prism.Core NuGet package.
(Google翻訳)
注意
CompositeCommandは、Prism.Core NuGetパッケージにあるPrism.Commands名前空間にあります。
image.png

Creating a Composite Command

To create a composite command, instantiate a CompositeCommand instance and then expose it as either an ICommand or ComponsiteCommand property.
(Google翻訳)
複合コマンドを作成するには、CompositeCommandインスタンスをインスタンス化してから、ICommandまたはComponsiteCommandプロパティのいずれかとして公開します。

    public class ApplicationCommands
    {
        private CompositeCommand _saveCommand = new CompositeCommand();
        public CompositeCommand SaveCommand
        {
            get { return _saveCommand; }
        }
    }

Making a CompositeCommand Globally Available

Typically, CompositeCommands are shared throughout an application and need to be made available globally. It's important that when you register a child command with a CompositeCommand that you are using the same instance of the CompositeCommand throughout the application. This requires the CompositeCommand to be defined as a singleton in your application. This can be done by either using dependency injection (DI), or by defining your CompositeCommand as a static class.
(Google翻訳)
通常、CompositeCommandsはアプリケーション全体で共有され、グローバルに使用できるようにする必要があります。 子コマンドをCompositeCommandに登録するときは、アプリケーション全体でCompositeCommandの同じインスタンスを使用していることが重要です。 これには、アプリケーションでCompositeCommandをシングルトンとして定義する必要があります。 これを行うには、依存性注入(DI)を使用するか、CompositeCommandを静的クラスとして定義します。

Using Dependency Injection

The first step in defining your CompositeCommands is to create an interface.
(Google翻訳)
CompositeCommandsを定義する最初のステップは、インターフェースを作成することです。

    public interface IApplicationCommands
    {
        CompositeCommand SaveCommand { get; }
    }

Next, create a class that implements the interface.
(Google翻訳)
次に、インターフェースを実装するクラスを作成します。

    public class ApplicationCommands : IApplicationCommands
    {
        private CompositeCommand _saveCommand = new CompositeCommand();
        public CompositeCommand SaveCommand
        {
            get { return _saveCommand; }
        }
    }

Once you have defined your ApplicationCommands class, you must register it as a singleton with the container.
(Google翻訳)
ApplicationCommandsクラスを定義したら、コンテナーにシングルトンとして登録する必要があります。

    public partial class App : PrismApplication
    {
        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {
            containerRegistry.RegisterSingleton<IApplicationCommands, ApplicationCommands>();
        }
    }

Next, ask for the IApplicationCommands interface in the ViewModel constructor. Once you have an instance of the ApplicationCommands class, can now register your DelegateCommands with the appropriate CompositeCommand.
(Google翻訳)
次に、ViewModelコンストラクターでIApplicationCommandsインターフェイスを要求します。 ApplicationCommandsクラスのインスタンスを取得したら、DelegateCommandsを適切なCompositeCommandに登録できます。

    public DelegateCommand UpdateCommand { get; private set; }

    public TabViewModel(IApplicationCommands applicationCommands)
    {
        UpdateCommand = new DelegateCommand(Update);
        applicationCommands.SaveCommand.RegisterCommand(UpdateCommand);
    }

Using a Static Class

Create a static class that will represent your CompositeCommands
(Google翻訳)
CompositeCommandsを表す静的クラスを作成します

public static class ApplicationCommands
{
    public static CompositeCommand SaveCommand = new CompositeCommand();
}

In your ViewModel, associate child commands to the static ApplicationCommands class.
(Google翻訳)
ViewModelで、子コマンドを静的ApplicationCommandsクラスに関連付けます。

    public DelegateCommand UpdateCommand { get; private set; }

    public TabViewModel()
    {
        UpdateCommand = new DelegateCommand(Update);
        ApplicationCommands.SaveCommand.RegisterCommand(UpdateCommand);
    }

NOTE
To increase the maintainability and testability of your code, it is recommended that you using the dependency injection approach.
(Google翻訳)
注意
コードの保守性とテスト容易性を高めるために、依存性注入アプローチを使用することをお勧めします。

Binding to a Globally Available Command

Once you have created your CompositeCommands, you must now bind them to UI elements in order to invoke the commands.
(Google翻訳)
CompositeCommandsを作成したら、コマンドを呼び出すために、それらをUI要素にバインドする必要があります。

Using Depency Injection

When using DI, you must expose the IApplicationCommands for binding to a View. In the ViewModel of the view, ask for the IApplicationCommands in the constructor and set a property of type IApplicationCommands to the instance.
(Google翻訳)
DIを使用する場合、ビューにバインドするためにIApplicationCommandsを公開する必要があります。 ビューのViewModelで、コンストラクターにIApplicationCommandsを要求し、IApplicationCommands型のプロパティをインスタンスに設定します。

    public class MainWindowViewModel : BindableBase
    {
        private IApplicationCommands _applicationCommands;
        public IApplicationCommands ApplicationCommands
        {
            get { return _applicationCommands; }
            set { SetProperty(ref _applicationCommands, value); }
        }

        public MainWindowViewModel(IApplicationCommands applicationCommands)
        {
            ApplicationCommands = applicationCommands;
        }
    }

In the view, bind the button to the ApplicationCommands.SaveCommand property. The SaveCommand is a property that is defined on the ApplicationCommands class.
(Google翻訳)
ビューで、ボタンをApplicationCommands.SaveCommandプロパティにバインドします。 SaveCommandは、ApplicationCommandsクラスで定義されるプロパティです。

<Button Content="Save" Command="{Binding ApplicationCommands.SaveCommand}"/>

Using a Static Class

If you are using the static class approach, the following code example shows how to bind a button to the static ApplicationCommands class in WPF.
(Google翻訳)
静的クラスアプローチを使用している場合、次のコード例は、ボタンをWPFの静的ApplicationCommandsクラスにバインドする方法を示しています。

<Button Content="Save" Command="{x:Static local:ApplicationCommands.SaveCommand}" />

Unregister a Command

As seen in the previous examples, child commands are registered using the CompositeCommand.RegisterCommand method. However, when you no longer wish to respond to a CompositeCommand or if you are destroying the View/ViewModel for garbage collection, you should unregister the child commands with the CompositeCommand.UnregisterCommand method.
(Google翻訳)
前の例で見たように、子コマンドはCompositeCommand.RegisterCommandメソッドを使用して登録されます。 ただし、CompositeCommandに応答する必要がなくなった場合、またはガベージコレクションのView / ViewModelを破棄する場合は、CompositeCommand.UnregisterCommandメソッドを使用して子コマンドの登録を解除する必要があります。

    public void Destroy()
    {
        _applicationCommands.UnregisterCommand(UpdateCommand);
    }

IMPORTANT
You MUST unregister your commands from a CompositeCommand when the View/ViewModel is no longer needed (ready for GC). Otherwise you will have introduced a memory leak.
(Google翻訳)
重要
View / ViewModelが不要になったとき(GCの準備ができているとき)、CompositeCommandからコマンドの登録を解除する必要があります。 そうしないと、メモリリークが発生します。

Executing Commands on Active Views

Composite commands at the parent view level will often be used to coordinate how commands at the child view level are invoked. In some cases, you will want the commands for all shown views to be executed, as in the Save All command example described earlier. In other cases, you will want the command to be executed only on the active view. In this case, the composite command will execute the child commands only on views that are deemed to be active; it will not execute the child commands on views that are not active. For example, you may want to implement a Zoom command on the application's toolbar that causes only the currently active item to be zoomed, as shown in the following diagram.
(Google翻訳)
親ビューレベルでの複合コマンドは、子ビューレベルでのコマンドの呼び出し方法を調整するためによく使用されます。 前述の「すべて保存」コマンドの例のように、表示されているすべてのビューのコマンドを実行したい場合があります。 それ以外の場合は、アクティブビューでのみコマンドを実行する必要があります。 この場合、複合コマンドは、アクティブであると見なされるビューでのみ子コマンドを実行します。 アクティブでないビューでは子コマンドを実行しません。 たとえば、次の図に示すように、アプリケーションのツールバーにズームコマンドを実装すると、現在アクティブなアイテムのみがズームされます。
image.png

To support this scenario, Prism provides the IActiveAware interface. The IActiveAware interface defines an IsActive property that returns true when the implementer is active, and an IsActiveChanged event that is raised whenever the active state is changed.

You can implement the IActiveAware interface on views or ViewModels. It is primarily used to track the active state of a view. Whether or not a view is active is determined by the views within the specific control. For the Tab control, there is an adapter that sets the view in the currently selected tab as active, for example.

The DelegateCommand class also implements the IActiveAware interface. The CompositeCommand can be configured to evaluate the active status of child DelegateCommands (in addition to the CanExecute status) by specifying true for the monitorCommandActivity parameter in the constructor. When this parameter is set to true, the CompositeCommand class will consider each child DelegateCommand's active status when determining the return value for the CanExecute method and when executing child commands within the Execute method.
(Google翻訳)
このシナリオをサポートするために、PrismはIActiveAwareインターフェースを提供しています。 IActiveAwareインターフェイスは、実装者がアクティブなときにtrueを返すIsActiveプロパティと、アクティブな状態が変更されるたびに発生するIsActiveChangedイベントを定義します。

IActiveAwareインターフェイスをビューまたはViewModelに実装できます。これは主に、ビューのアクティブな状態を追跡するために使用されます。ビューがアクティブかどうかは、特定のコントロール内のビューによって決まります。たとえば、タブコントロールには、現在選択されているタブのビューをアクティブに設定するアダプターがあります。

DelegateCommandクラスは、IActiveAwareインターフェイスも実装します。コンストラクターのmonitorCommandActivityパラメーターにtrueを指定することにより、CanExecuteステータスに加えて、子DelegateCommandsのアクティブステータスを評価するようにCompositeCommandを構成できます。このパラメーターがtrueに設定されている場合、CompositeCommandクラスは、CanExecuteメソッドの戻り値を決定するとき、およびExecuteメソッド内で子コマンドを実行するときに、各子DelegateCommandのアクティブステータスを考慮します。

    public class ApplicationCommands : IApplicationCommands
    {
        private CompositeCommand _saveCommand = new CompositeCommand(true);
        public CompositeCommand SaveCommand
        {
            get { return _saveCommand; }
        }
    }

When the monitorCommandActivity parameter is true, the CompositeCommand class exhibits the following behavior:

・CanExecute: Returns true only when all active commands can be executed. Child commands that are inactive will not be considered at all.
・Execute: Executes all active commands. Child commands that are inactive will not be considered at all.

By implementing the IActiveAware interface on your ViewModels, you will be notified when your view becomes active or inactive. When the view's active status changes, you can update the active status of the child commands. Then, when the user invokes the composite command, the command on the active child view will be invoked.
(Google翻訳)
monitorCommandActivityパラメータがtrueの場合、CompositeCommandクラスは次の動作を示します。

・CanExecute:アクティブなコマンドをすべて実行できる場合のみtrueを返します。 非アクティブな子コマンドはまったく考慮されません。
・実行:アクティブなコマンドをすべて実行します。 非アクティブな子コマンドはまったく考慮されません。

ViewModelにIActiveAwareインターフェースを実装することにより、ビューがアクティブまたは非アクティブになったときに通知されます。 ビューのアクティブステータスが変更されると、子コマンドのアクティブステータスを更新できます。 次に、ユーザーが複合コマンドを呼び出すと、アクティブな子ビューのコマンドが呼び出されます。

    public class TabViewModel : BindableBase, IActiveAware
    {
        private bool _isActive;
        public bool IsActive
        {
            get { return _isActive; }
            set
            {
                _isActive = value;
                OnIsActiveChanged();
            }
        }

        public event EventHandler IsActiveChanged;

        public DelegateCommand UpdateCommand { get; private set; }

        public TabViewModel(IApplicationCommands applicationCommands)
        {
            UpdateCommand = new DelegateCommand(Update);
            applicationCommands.SaveCommand.RegisterCommand(UpdateCommand);
        }

        private void Update()
        {
            //implement logic
        }

        private void OnIsActiveChanged()
        {
            UpdateCommand.IsActive = IsActive; //set the command as active
            IsActiveChanged?.Invoke(this, new EventArgs()); //invoke the event for all listeners
        }
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Prism の Commanding を学ぶ

RRISM LIBRARY の Documentation から Commanding の箇所を学んでみます。

そのまんまの翻訳ですが、メモとして。
日本語がへんなところは英語に戻ってそれなりに理解。。。

In addition to providing access to the data to be displayed or edited in the view, the ViewModel will likely define one or more actions or operations that can be performed by the user. Actions or operations that the user can perform through the UI are typically defined as commands. Commands provide a convenient way to represent actions or operations that can be easily bound to controls in the UI. They encapsulate the actual code that implements the action or operation and help to keep it decoupled from its actual visual representation in the view.
(Google翻訳)
ビューで表示または編集するデータへのアクセスを提供することに加えて、ViewModelは、ユーザーが実行できる1つ以上のアクションまたは操作を定義する可能性があります。 ユーザーがUIを介して実行できるアクションまたは操作は、通常、コマンドとして定義されます。 コマンドは、UIのコントロールに簡単にバインドできるアクションまたは操作を表す便利な方法を提供します。 これらは、アクションまたは操作を実装する実際のコードをカプセル化し、ビュー内の実際の視覚的表現から切り離しておくのに役立ちます。

Commands can be visually represented and invoked in many different ways by the user as they interact with the view. In most cases, they are invoked as a result of a mouse click, but they can also be invoked as a result of shortcut key presses, touch gestures, or any other input events. Controls in the view are data bound to the ViewModels's commands so that the user can invoke them using whatever input event or gesture the control defines. Interaction between the UI controls in the view and the command can be two-way. In this case, the command can be invoked as the user interacts with the UI, and the UI can be automatically enabled or disabled as the underlying command becomes enabled or disabled.
(Google翻訳)
コマンドは、ユーザーがビューを操作するときに、さまざまな方法で視覚的に表現して呼び出すことができます。 ほとんどの場合、マウスクリックの結果として呼び出されますが、ショートカットキーの押下、タッチジェスチャ、またはその他の入力イベントの結果としても呼び出されます。 ビュー内のコントロールは、ViewModelのコマンドにバインドされたデータであり、ユーザーは、コントロールが定義する入力イベントまたはジェスチャーを使用してそれらを呼び出すことができます。 ビューのUIコントロールとコマンドの間の相互作用は双方向です。 この場合、ユーザーがUIを操作するときにコマンドを呼び出すことができ、基になるコマンドが有効または無効になると、UIを自動的に有効または無効にすることができます。

The ViewModel can implement commands as a Command Object (an object that implements the ICommand interface). The view's interaction with the command can be defined declaratively without requiring complex event handling code in the view's code-behind file. For example, certain controls inherently support commands and provide a Command property that can be data bound to an ICommand object provided by the ViewModel. In other cases, a command behavior can be used to associate a control with a command method or command object provided by the ViewModel.
(Google翻訳)
ViewModelは、コマンドオブジェクト(ICommandインターフェイスを実装するオブジェクト)としてコマンドを実装できます。 ビューとコマンドの相互作用は、ビューの分離コードファイルに複雑なイベント処理コードを必要とせずに宣言的に定義できます。 たとえば、特定のコントロールは本質的にコマンドをサポートし、ViewModelによって提供されるICommandオブジェクトにデータバインドできるCommandプロパティを提供します。 他の場合では、コマンド動作を使用して、コントロールを、ViewModelによって提供されるコマンドメソッドまたはコマンドオブジェクトに関連付けることができます。

Implementing the ICommand interface is straightforward. Prism provides the DelegateCommand implementation of this interface that you can readily use in your applications.
(Google翻訳)
ICommandインターフェイスの実装は簡単です。 Prismは、アプリケーションですぐに使用できるこのインターフェイスのDelegateCommand実装を提供します。

NOTE
DelegateCommand can be found in the Prism.Commands namespace which is located in the Prism.Core NuGet package.
(Google翻訳)
注意
DelegateCommandは、Prism.Core NuGetパッケージにあるPrism.Commands名前空間にあります。

Creating a DelegateCommand

image.png

The Prism DelegateCommand class encapsulates two delegates that each reference a method implemented within your ViewModel class. It implements the ICommand interface's Execute and CanExecute methods by invoking these delegates. You specify the delegates to your ViewModel methods in the DelegateCommand class constructor. For example, the following code example shows how a DelegateCommand instance, which represents a Submit command, is constructed by specifying delegates to the OnSubmit and CanSubmit ViewModel methods. The command is then exposed to the view via a read-only property that returns a reference to the DelegateCommand.
(Google翻訳)
Prism DelegateCommandクラスは、それぞれがViewModelクラス内に実装されたメソッドを参照する2つのデリゲートをカプセル化します。 これらのデリゲートを呼び出すことにより、ICommandインターフェイスのExecuteメソッドとCanExecuteメソッドを実装します。 DelegateCommandクラスコンストラクターでViewModelメソッドへのデリゲートを指定します。 たとえば、次のコード例は、OnSubmitおよびCanSubmit ViewModelメソッドにデリゲートを指定することにより、Submitコマンドを表すDelegateCommandインスタンスがどのように構築されるかを示しています。 次に、コマンドは、DelegateCommandへの参照を返す読み取り専用プロパティを介してビューに公開されます。

public class ArticleViewModel
{
    public DelegateCommand SubmitCommand { get; private set; }

    public ArticleViewModel()
    {
        SubmitCommand = new DelegateCommand<object>(Submit, CanSubmit);
    }

    void Submit(object parameter)
    {
        //implement logic
    }

    bool CanSubmit(object parameter)
    {
        return true;
    }
}

When the Execute method is called on the DelegateCommand object, it simply forwards the call to the method in your ViewModel class via the delegate that you specified in the constructor. Similarly, when the CanExecute method is called, the corresponding method in your ViewModel class is called. The delegate to the CanExecute method in the constructor is optional. If a delegate is not specified, DelegateCommand will always return true for CanExecute.

The DelegateCommand class is a generic type. The type argument specifies the type of the command parameter passed to the Execute and CanExecute methods. In the preceding example, the command parameter is of type object. A non-generic version of the DelegateCommand class is also provided by Prism for use when a command parameter is not required, and is defined as follows:
(Google翻訳)
ExecuteメソッドがDelegateCommandオブジェクトで呼び出されると、コンストラクターで指定したデリゲートを介してViewModelクラスのメソッドへの呼び出しが転送されます。 同様に、CanExecuteメソッドが呼び出されると、ViewModelクラスの対応するメソッドが呼び出されます。 コンストラクターのCanExecuteメソッドへのデリゲートはオプションです。 デリゲートが指定されていない場合、DelegateCommandはCanExecuteに対して常にtrueを返します。

DelegateCommandクラスはジェネリック型です。 type引数は、ExecuteメソッドとCanExecuteメソッドに渡されるコマンドパラメータのタイプを指定します。 上記の例では、コマンドパラメータのタイプはオブジェクトです。 DelegateCommandクラスの非ジェネリックバージョンも、コマンドパラメーターが不要な場合に使用するためにPrismによって提供され、次のように定義されています。

public class ArticleViewModel
{
    public DelegateCommand SubmitCommand { get; private set; }

    public ArticleViewModel()
    {
        SubmitCommand = new DelegateCommand(Submit, CanSubmit);
    }

    void Submit()
    {
        //implement logic
    }

    bool CanSubmit()
    {
        return true;
    }
}

NOTE
The DelegateCommand deliberately prevents the use of value types (int, double, bool, etc). Because ICommand takes an object, having a value type for T would cause unexpected behavior when CanExecute(null) is called during XAML initialization for command bindings. Using default(T) was considered and rejected as a solution because the implementor would not be able to distinguish between a valid and defaulted values. If you wish to use a value type as a parameter, you must make it nullable by using DelegateCommand> or the shorthand ? syntax (DelegateCommand).
(Google翻訳)
注意
DelegateCommandは、意図的に値型(int、double、boolなど)の使用を禁止します。 ICommandはオブジェクトを取得するため、Tの値型を持つと、コマンドバインディングのXAML初期化中にCanExecute(null)が呼び出されたときに予期しない動作が発生します。 実装者が有効な値とデフォルト値を区別できないため、default(T)の使用はソリューションとして検討され、拒否されました。 値タイプをパラメーターとして使用する場合は、DelegateCommand >または省略形を使用して、値タイプをNULL可能にする必要があります。 構文(DelegateCommand )。

Invoking DelegateCommands from the View

There are a number of ways in which a control in the view can be associated with a command object provided by the ViewModel. Certain WPF, Xamarin.Forms, and UWP controls can be easily data bound to a command object through the Command property.
(Google翻訳)
ビューのコントロールを、ViewModelによって提供されるコマンドオブジェクトに関連付ける方法はいくつかあります。 特定のWPF、Xamarin.Forms、およびUWPコントロールは、Commandプロパティを使用して、コマンドオブジェクトに簡単にデータバインドできます。

<Button Command="{Binding SubmitCommand}" CommandParameter="OrderId"/>

A command parameter can also be optionally defined using the CommandParameter property. The type of the expected argument is specified in the DelegateCommand generic declaration. The control will automatically invoke the target command when the user interacts with that control, and the command parameter, if provided, will be passed as the argument to the command's Execute method. In the preceding example, the button will automatically invoke the SubmitCommand when it is clicked. Additionally, if a CanExecute delegate is specified, the button will be automatically disabled if CanExecute returns false, and it will be enabled if it returns true.
(Google翻訳)
コマンドパラメータは、CommandParameterプロパティを使用してオプションで定義することもできます。 予期される引数のタイプは、DelegateCommand ジェネリック宣言で指定されています。 ユーザーがそのコントロールを操作すると、コントロールは自動的にターゲットコマンドを呼び出し、コマンドパラメーターが指定されている場合は、引数としてコマンドのExecuteメソッドに渡されます。 前の例では、ボタンをクリックすると、SubmitCommandが自動的に呼び出されます。 さらに、CanExecuteデリゲートが指定されている場合、CanExecuteがfalseを返すとボタンは自動的に無効になり、trueを返すとボタンは有効になります。

Raising Change Notifications

The ViewModel often needs to indicate a change in the command's CanExecute status so that any controls in the UI that are bound to the command will update their enabled status to reflect the availability of the bound command. The DelegateCommand provides several ways to send these notifications to the UI.
(Google翻訳)
多くの場合、ViewModelはコマンドのCanExecuteステータスの変更を示す必要があります。これにより、コマンドにバインドされているUIのコントロールは、バインドされたコマンドの可用性を反映するために、有効なステータスを更新します。 DelegateCommandは、これらの通知をUIに送信するいくつかの方法を提供します。

RaiseCanExecuteChanged

Use the RaiseCanExecuteChanged method whenever you need to manually update the state of the bound UI elements. For example, when the IsEnabled property values changes, we are calling RaiseCanExecuteChanged in the setter of the property to notify the UI of state changes.
(Google翻訳)
バインドされたUI要素の状態を手動で更新する必要がある場合は常に、RaiseCanExecuteChangedメソッドを使用します。 たとえば、IsEnabledプロパティの値が変更されると、プロパティのセッターでRaiseCanExecuteChangedを呼び出して、UIに状態の変更を通知します。

        private bool _isEnabled;
        public bool IsEnabled
        {
            get { return _isEnabled; }
            set
            {
                SetProperty(ref _isEnabled, value);
                SubmitCommand.RaiseCanExecuteChanged();
            }
        }

ObservesProperty

In cases where the command should send notifications when a property value changes, you can use the ObservesProperty method. When using the ObservesProperty method, whenever the value of the supplied property changes, the DelegateCommand will automatically call RaiseCanExecuteChanged to notify the UI of state changes.
(Google翻訳)
プロパティ値が変更されたときにコマンドが通知を送信する必要がある場合は、ObservesPropertyメソッドを使用できます。 ObservesPropertyメソッドを使用する場合、提供されたプロパティの値が変更されると、DelegateCommandは自動的にRaiseCanExecuteChangedを呼び出して、状態の変更をUIに通知します。

public class ArticleViewModel : BindableBase
{
    private bool _isEnabled;
    public bool IsEnabled
    {
        get { return _isEnabled; }
        set { SetProperty(ref _isEnabled, value); }
    }

    public DelegateCommand SubmitCommand { get; private set; }

    public ArticleViewModel()
    {
        SubmitCommand = new DelegateCommand(Submit, CanSubmit).ObservesProperty(() => IsEnabled);
    }

    void Submit()
    {
        //implement logic
    }

    bool CanSubmit()
    {
        return IsEnabled;
    }
}

NOTE
You can chain-register multiple properties for observation when using the ObservesProperty method. Example: ObservesProperty(() => IsEnabled).ObservesProperty(() => CanSave).
(Google翻訳)
注意
ObservesPropertyメソッドを使用すると、複数のプロパティをチェーン登録して監視できます。 例:ObservesProperty(()=> IsEnabled).ObservesProperty(()=> CanSave)。

ObservesCanExecute

If your CanExecute is the result of a simple Boolean property, you can eliminate the need to declare a CanExecute delegate, and use the ObservesCanExecute method instead. ObservesCanExecute will not only send notifications to the UI when the registered property value changes but it will also use that same property as the actual CanExecute delegate.
(Google翻訳)
CanExecuteが単純なブール型プロパティの結果である場合、CanExecuteデリゲートを宣言する必要をなくし、代わりにObservesCanExecuteメソッドを使用できます。 ObservesCanExecuteは、登録されたプロパティ値が変更されたときにUIに通知を送信するだけでなく、実際のCanExecuteデリゲートと同じプロパティを使用します。

public class ArticleViewModel : BindableBase
{
    private bool _isEnabled;
    public bool IsEnabled
    {
        get { return _isEnabled; }
        set { SetProperty(ref _isEnabled, value); }
    }

    public DelegateCommand SubmitCommand { get; private set; }

    public ArticleViewModel()
    {
        SubmitCommand = new DelegateCommand(Submit).ObservesCanExecute(() => IsEnabled);
    }

    void Submit()
    {
        //implement logic
    }
}

WARNING
Do not attempt to chain-register ObservesCanExecute methods. Only one property can be observed for the CanExcute delegate.
(Google翻訳)
警告
ObservesCanExecuteメソッドをチェーン登録しないでください。 CanExcuteデリゲートで監視できるプロパティは1つだけです。

Implementing a Task-Based DelegateCommand

In today's world of async/await, calling asynchronous methods inside of the Execute delegate is a very common requirement. Everyone's first instinct is that they need an AsyncCommand, but that assumption is wrong. ICommand by nature is synchronous, and the Execute and CanExecute delegates should be considered events. This means that async void is a perfectly valid syntax to use for commands. There are two approaches to using async methods with DelegateCommand.
(Google翻訳)
今日の非同期/待機の世界では、実行デリゲート内で非同期メソッドを呼び出すことは非常に一般的な要件です。 全員の最初の本能は、AsyncCommandが必要であるということですが、その仮定は間違っています。 ICommandは本質的に同期であり、実行デリゲートとCanExecuteデリゲートはイベントと見なされます。 つまり、async voidはコマンドに使用するのに完全に有効な構文です。 DelegateCommandで非同期メソッドを使用するには、2つの方法があります。

Option 1:

public class ArticleViewModel
{
    public DelegateCommand SubmitCommand { get; private set; }

    public ArticleViewModel()
    {
        SubmitCommand = new DelegateCommand(Submit);
    }

    async void Submit()
    {
        await SomeAsyncMethod();
    }
}

Option 2:

public class ArticleViewModel
{
    public DelegateCommand SubmitCommand { get; private set; }

    public ArticleViewModel()
    {
        SubmitCommand = new DelegateCommand(async ()=> await Submit());
    }

    Task Submit()
    {
        return SomeAsyncMethod();
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#で和暦表示

よく忘れるので調べたついでに備忘録.

やること

  1. System.Globalization.CultureInfoja-JPを指定
  2. DateTimeFormat.CalendarJapaneseCalendarを設定
  3. DateTimeに「1.」のCultureInfoを設定
  4. 文字列とする際に書式とCultureInfoを設定
var ci1 = new CultureInfo("ja-JP")
{
    DateTimeFormat = { Calendar = new JapaneseCalendar() }
};            
Console.WriteLine(new DateTime(1926, 12, 25).ToString("ggyy年MM月dd日", ci1));
Console.WriteLine(new DateTime(2018, 9, 1).ToString("ggyy年MM月dd日", ci1));
Console.WriteLine(DateTime.Now.ToString("ggyy年MM月dd日", ci1));

こちらの方が良いと思う.

var ci2 = new CultureInfo("ja-JP");
ci2.DateTimeFormat.Calendar = new JapaneseCalendar();
Console.WriteLine(new DateTime(1926, 12, 25).ToString("ggyy年MM月dd日", ci2));
Console.WriteLine(new DateTime(2018, 9, 1).ToString("ggyy年MM月dd日", ci2));
Console.WriteLine(DateTime.Now.ToString("ggyy年MM月dd日", ci2));

出力結果

昭和元年12月25日
平成30年09月01日
令和02年08月14日

参考

なんやかんやでまだMSのリファレンスに慣れていない自分がいる...

https://docs.microsoft.com/ja-jp/dotnet/api/system.globalization.cultureinfo?view=netcore-3.1

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む