- 投稿日:2020-08-14T22:27:24+09:00
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は一見何やってるかわからないけど、わかればすごく便利。使えるところでは積極的に使っていこう。
- 投稿日:2020-08-14T22:27:24+09:00
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は一見何やってるかわからないけど、わかればすごく便利。使えるところでは積極的に使っていこう。
- 投稿日:2020-08-14T22:02:29+09:00
C#で3D形状データを表示・操作し、画像を保存する。
概要
C#で3D形状、3Dモデルを簡単に表示・操作し、画像として保存するまでの良いテンプレートが見つかりませんでしたので、自分の行っている手法を紹介いたします。本記事では、3D表示にOpenGLのラッパーであるOpenTKを、画像データの保存にOpenCVのC#ラッパーのOpenCVSharpを、3D形状の読み込みにSurfaceAnalyzerを使います。完成イメージは以下のgifになります。はじめに三次元形状を読み込み、マウスで回転動作をし、画像として保存します。
なお、今回用いたソースコードはgitにまとめています。
準備
初めに、必要なライブラリのインストールを行います。
Windows Form Applicationの作成
今回は、WindowsFormApplicationとして作成していきます。
OpenTK, OpenTK.GLControlのインストール
OpenTKは、C#でOpenGLを扱うためのライブラリです。GLControlは、フォーム上にOpenGLの画面を表示するためのコントロールです。
OpenCVSharp4, OpenCVSharp4.runtime.winのインストール
OpenCVSharp4は、C#でOpenCVを使うためのラッパーです。今回は、画像を保存するために使用するため、画像保存が不要でしたらインストール不要です。System.Drawing.Bitmapでも良いのですが、取得した画像後処理をかけることを考慮して汎用性のあるこちらを用います。画像をウィンドウで表示するためには、それぞれの環境にあったruntimeもインストールする必要があります。Windowsの場合は、OpenCVSharp.runtime.winをインストールしましょう。
Surface Analyzerのインストール
Surface Analyzerは、C#でSTLデータを扱うためのライブラリです。詳細な説明は、こちらの記事をご覧ください。STLファイルを読み込むことができれば、他のライブラリでも大丈夫です。
それでは、これからコーディングを始めます。本記事では、実際に形状を表示する"Viewerフォーム"と、その表示をコントロールする"Controlフォーム"に分けてコーディングしていきます。
Viewerフォームのコーディング
Viewerフォームの作成
今回は、3D形状を表示する専用のフォームを作成し、Controlフォームから操作します。[プログラム名]>追加>フォーム(Windows Form)の順にクリックします。
ビューアの設定
下記のように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フォーム上にボタンを追加し、それぞれ「ビューアの表示」、「形状の表示」、「保存」とします。
Viewerフォームの表示
各ボタンをダブルクリックし、イベントを追加し以下のように記述します。
初めに、「ビューアの表示」ボタンをクリックした際の動作を作成します。
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.csusing 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) { } } }実行し、「ビューアの表示」ボタンをクリックしビューアを表示します。そして「形状の表示」ボタンをクリックすると、形状が表示されます。右クリックで回転、ホイールで拡大・縮小が可能なです。
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で座標軸の基準が異なるため、上下が反転した状態となります。
まとめ
本記事では、C#でSTLデータを読み込んで、表示・操作し、画像として保存する方法を紹介しました。C#で3D形状を扱いたいときに役に立てると幸いです。
- 投稿日:2020-08-14T21:24:23+09:00
挨拶を返すアプリを作ってみたをやり直した
はじめに
2017年頃にC#...というよりVisual Studioプログラミングのリハビリ目的で「挨拶を返すアプリ」を作ってみた...という記事でわからないまま放置してたところがあったので書き直し。
当時やりたかったこと
- HelloApp.exeを引数なしで指定すると何も表示しない
- HelloApp.exeに引を入れて実行すると「Hello. {引数に入れた名前}さん!」と表示する。
プロジェクト概要
1 2 プロジェクト テンプレート コンソールアプリ(.NET Framework) プロジェクト名 HelloApp フレームワーク .NET Framework 4.7.2 最初の試行
プログラムの挙動
- helloman.exeを実行すると「Hello.太郎さん!」と挨拶する。
- 引数で名前を指定すると「太郎」が「指定した名前」に代わって、挨拶する。
間違ったコード
program.csusing 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からビルドエラーが出力。
未割当のローカル変数 yournameを処理できないよと言われたので、ビルド処理 = コンパイルをするための条件が整ってないんだなと誤解したまま、とりあえず未割当変数をなんとかすりゃいいかなとテキトーに進めてしまった。。
頂いたアドバイス
悩んでるところ(足りない知識)はそこじゃないよとアドバイスを頂くが、当時いろいろと限界を超えていた為「ふーんなるほど(よくわからん)」状態。
テキトー修正を施し、エラー回避して無理やり動くものにしてしまった。テキトー修正
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さんごめんなさい。参考
- コマンドライン引数(C#プログラミングガイド) https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/main-and-command-args/command-line-arguments
- Environment.GetCommandLineArgs メソッド https://docs.microsoft.com/ja-jp/dotnet/api/system.environment.getcommandlineargs?view=netcore-3.1
- 投稿日:2020-08-14T20:57:55+09:00
BlazorでMVVMパターンを試す
はじめに
Blazorでアプリケーションを開発していて、WPF開発の際に行われているようなMVVMパターンを適用できれば開発が楽になるのではないかと思い試しました。
サンプルはBlazor Serverアプリとして作成しています。
事前準備としてMVVMパターンの開発を補助してくれるライブラリであるReactivePropertyをnuget経由でインストールしています。・環境
.Net Core 3.1
ReactiveProperty 7.2.0今回はテキストボックスに文字を入力すると文字数を表示してくれるアプリを作ります。
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.cspublic void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddServerSideBlazor(); services.AddScoped<StringLengthCounterModel>(); services.AddTransient<StringLengthCounterViewModel>(); }作成したViewModelとModelをDIコンテナに登録します。既存のStartup.csにあるConfigureServicesメソッドに追記する形で行います。
AddScoped
AddTransient
というメソッドが出てきていますがそれぞれでオブジェクトの寿命が異なります。また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遅延して結果が表示されるかと思いきや予想と違う挙動をします。正しく文字列をカウントしていません。
更に先程の処理を書き換えます。
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
- 投稿日:2020-08-14T18:19:54+09:00
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"; }
- 投稿日:2020-08-14T16:36:06+09:00
WPF 「MahApps.Metro」を使ってWPFアプリケーションをModernUIにしてみる(Ver2.00以降)
今作っているWPFアプリケーションにMahApps.Metroを入れるとひとまずモダンなUIになります。
さくっと入るので見た目ちょっと違ったアプリ作るのに便利です。
(VitualStudio2017でテストしました)手順
- MahApps.Metroを入れる
- App.xamlを修正
- メインウィンドウのXAMLを修正
- メインウィンドウのコードビハインドファイルを修正
1.MahApps.Metroを入れる
- 「プロジェクトエクスプローラ」→「参照設定」を右クリックして「NuGetパッケージ管理」を開きます。
- ウィンドウ右上のオンラインの検索に「MahApps」と入れてやると出てきます。インストールしましょう。
- 今回入れたのはバージョン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を追加します。xamlxmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"2.Windowクラスの変更
Window → Controls: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" />
- 投稿日:2020-08-14T06:47:51+09:00
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はオプトアウトのみです。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); }); }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())である必要があります。 そうでない場合、マッピングは失敗します。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); }); }
- 投稿日:2020-08-14T06:47:33+09:00
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を見つけることができます。イベントアグリゲーターでは、次の図に示すように、複数のパブリッシャーとサブスクライバーも使用できます。
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 } }
- 投稿日:2020-08-14T06:47:14+09:00
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つ以上のビューモデルのコマンドを呼び出せるようにしたい場合があります。たとえば、アプリケーションでユーザーが同時に複数のアイテムを編集できるようにする場合、ユーザーがアプリケーションのツールバーまたはリボンのボタンで表される単一のコマンドを使用してすべてのアイテムを保存できるようにすることができます。 この場合、次の図に示すように、[すべて保存]コマンドは、各アイテムのビューモデルインスタンスによって実装された各保存コマンドを呼び出します。
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名前空間にあります。
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翻訳)
親ビューレベルでの複合コマンドは、子ビューレベルでのコマンドの呼び出し方法を調整するためによく使用されます。 前述の「すべて保存」コマンドの例のように、表示されているすべてのビューのコマンドを実行したい場合があります。 それ以外の場合は、アクティブビューでのみコマンドを実行する必要があります。 この場合、複合コマンドは、アクティブであると見なされるビューでのみ子コマンドを実行します。 アクティブでないビューでは子コマンドを実行しません。 たとえば、次の図に示すように、アプリケーションのツールバーにズームコマンドを実装すると、現在アクティブなアイテムのみがズームされます。
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 } }
- 投稿日:2020-08-14T06:46:24+09:00
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
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(); } }
- 投稿日:2020-08-14T00:32:38+09:00
C#で和暦表示
よく忘れるので調べたついでに備忘録.
やること
System.Globalization.CultureInfo
にja-JP
を指定DateTimeFormat.Calendar
にJapaneseCalendar
を設定DateTime
に「1.」のCultureInfo
を設定- 文字列とする際に書式と
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