- 投稿日:2020-11-28T23:31:26+09:00
Blazorアプリケーションをリファクタリングしようとして失敗しているお話
概要
- コードをリファクタリングしてみたら意図通りに動作できなかった記録 (現在進行形)
宣伝
C#とBlazorでホロライブファン向けの動画ビューワー『ホロビューワー』を開発しました
何した
- 『ホロビューワー』のコードをリファクタリングしてた。
- 下記のように同じようなコードが何度も現れるのをなんとかしたかった。
リリース時 (コードの一部を抜粋)
画面分割モードに応じて、GridとGridCellを使用してアプリケーションのUIを生成する処理@switch (ApplicationScreenMode.CurrentScreenMode) { case ScreenMode.Single: <Grid HorizontalOptions="LayoutOptions.FillAndExpand" VerticalOptions="LayoutOptions.FillAndExpand"> <StackLayout> <WebViewToolbar @ref="WebViewToolbar1" /> <BlazorWebView @ref="BlazorWebView1" VerticalOptions="LayoutOptions.FillAndExpand"> <HoloViewer.WebUI.App /> </BlazorWebView> </StackLayout> </Grid> break; case ScreenMode.SplitHorizontal2: <Grid RowDefinitions="*, *" HorizontalOptions="LayoutOptions.FillAndExpand" VerticalOptions="LayoutOptions.FillAndExpand"> <GridCell Row="0"> <StackLayout> <WebViewToolbar @ref="WebViewToolbar1" /> <BlazorWebView @ref="BlazorWebView1" VerticalOptions="LayoutOptions.FillAndExpand"> <HoloViewer.WebUI.App /> </BlazorWebView> </StackLayout> </GridCell> <GridCell Row="1"> <StackLayout> <WebViewToolbar @ref="WebViewToolbar2" /> <BlazorWebView @ref="BlazorWebView2" VerticalOptions="LayoutOptions.FillAndExpand"> <HoloViewer.WebUI.App /> </BlazorWebView> </StackLayout> </GridCell> </Grid> break; // 以下同じようなコードが続いていたGridCellの設定はまとめられることに気がつく。 (最初と同じ辺りの処理を抜粋)
case ScreenMode.Single: <Grid HorizontalOptions="LayoutOptions.FillAndExpand" VerticalOptions="LayoutOptions.FillAndExpand"> <StackLayout> <WebViewToolbar @ref="WebViewToolbars" /> <BlazorWebView @ref="BlazorWebViews" VerticalOptions="LayoutOptions.FillAndExpand"> <HoloViewer.WebUI.App /> </BlazorWebView> </StackLayout> </Grid> break; case ScreenMode.SplitHorizontal2: <Grid RowDefinitions="*, *" HorizontalOptions="LayoutOptions.FillAndExpand" VerticalOptions="LayoutOptions.FillAndExpand"> @for (int i = 0; i < 2; i++) { <GridCell Row="@i"> <StackLayout> <WebViewToolbar @ref="WebViewToolbars" /> <BlazorWebView @ref="BlazorWebViews" VerticalOptions="LayoutOptions.FillAndExpand"> <HoloViewer.WebUI.App /> </BlazorWebView> </StackLayout> </GridCell> } </Grid> break;ちょっとマシになった。 (ここまでは正常に動作していた)
Gridの設定もバラバラに行うのではなく一箇所にまとめられることに気がつく。(同じ辺りのコードを抜粋。ここから失敗コース)
<Grid @ref="grid" RowDefinitions="@GridSettings[ApplicationScreenMode.CurrentScreenMode].rowDefinitions" ColumnDefinitions="@GridSettings[ApplicationScreenMode.CurrentScreenMode].columnDefinitions" HorizontalOptions="LayoutOptions.FillAndExpand" VerticalOptions="LayoutOptions.FillAndExpand"> @for (int i = 0; i < GridSettings[ApplicationScreenMode.CurrentScreenMode].cellCount; i++) { <GridCell Row="@GridSettings[ApplicationScreenMode.CurrentScreenMode].rowIndies[i]" Column="@GridSettings[ApplicationScreenMode.CurrentScreenMode].columnIndies[i]" ColumnSpan="@GridSettings[ApplicationScreenMode.CurrentScreenMode].columnSpanCounts[i]"> <StackLayout> <WebViewToolbar @ref="WebViewToolbars" /> <BlazorWebView @ref="BlazorWebViews" VerticalOptions="LayoutOptions.FillAndExpand"> <HoloViewer.WebUI.App /> </BlazorWebView> </StackLayout> </GridCell> } </Grid> @code { public class GridSetting { public readonly string rowDefinitions; public readonly string columnDefinitions; public readonly int cellCount; public readonly int[] rowIndies; public readonly int[] columnIndies; public readonly int[] columnSpanCounts; public GridSetting (string rowDefinitions, string columnDefinitions, int cellCount, int[] rowIndies, int[] columnIndies, int[] columnSpanCounts) { this.rowDefinitions = rowDefinitions; this.columnDefinitions = columnDefinitions; this.cellCount = cellCount; this.rowIndies = rowIndies; this.columnIndies = columnIndies; this.columnSpanCounts = columnSpanCounts; } } public static readonly Dictionary<ScreenMode, GridSetting> GridSettings = new Dictionary<ScreenMode, GridSetting>() { { ScreenMode.Single, new GridSetting("*", "*", 1, new int[]{ 0 }, new int[]{ 0 }, new int[]{ 1 }) }, { ScreenMode.SplitHorizontal2, new GridSetting("*, *", "*", 2, new int[]{ 0, 1 }, new int[]{ 0, 0 }, new int[]{ 1, 1 }) }, { ScreenMode.SplitVertical2, new GridSetting("*", "*, *", 2, new int[]{ 0, 0 }, new int[]{ 0, 1 }, new int[]{ 1, 1 }) }, { ScreenMode.SplitHorizontal3, new GridSetting("*, *, *", "*", 3, new int[]{ 0, 1, 2 }, new int[]{ 0, 0, 0 }, new int[]{ 1, 1, 1 }) }, { ScreenMode.SplitVertical3, new GridSetting("*", "*, *, *", 3, new int[]{ 0, 0, 0 }, new int[]{ 0, 1, 2 }, new int[]{ 1, 1, 1 }) }, { ScreenMode.SplitCustom3_1, new GridSetting("*, *", "*, *", 3, new int[]{ 0, 1, 1 }, new int[]{ 0, 0, 1 }, new int[]{ 2, 1, 1 }) }, { ScreenMode.SplitCustom3_2, new GridSetting("*, *", "*, *", 3, new int[]{ 0, 0, 1 }, new int[]{ 0, 1, 0 }, new int[]{ 1, 1, 2 }) }, { ScreenMode.SplitHorizontal4, new GridSetting("*, *, *, *", "*", 4, new int[]{ 0, 1, 2, 3 }, new int[]{ 0, 0, 0, 0 }, new int[]{ 1, 1, 1, 1 }) }, { ScreenMode.SplitVertical4, new GridSetting("*", "*, *, * ,*", 4, new int[]{ 0, 0, 0, 0 }, new int[]{ 0, 1, 2, 3 }, new int[]{ 1, 1, 1, 1 }) }, { ScreenMode.SplitCustom4, new GridSetting("*, *", "*, *", 4, new int[]{ 0, 0, 1, 1 }, new int[]{ 0, 1, 0, 1 }, new int[]{ 1, 1, 1, 1 }) }, };いざビルドして動かしてみると、以前と動作が違う。
例えば画面分割モードを横2分割の後に縦2分割を選択すると、以下の画像のように直前の変更が残ってしまうようになった。やってみたけど効果がなかったこと
- Gridプロパティを覗いてみると、RowDefinitionsとColumnDefinitionsが反映されているのにChildrenが変更されずに残ったままになっていたので、Grid.NativeControl.Children.Clear()でコントロールを削除してみた。
- 画面分割モードを変更してもページが再生成されず、ページが表示されなかった。
- ページが表示されないとダメなのでこの方法はボツ
- Xamarin関連のパッケージをプレビュー版も含めてアップデートした。
- 2020/11/28時点での最新版にアップデートしても変化がなかった。
- こちらも効果がなかったのでボツ
結論
- コードをまとめるのは諦める。
- 一旦コードはこのままにしてフレームワークなどの更新を待ってみる。
- Blazorへの理解を深めて対応できる問題なのかもう少し調査したい。
- 投稿日:2020-11-28T22:18:29+09:00
Unityで可読性の高いアプリケーション開発をする方法
はじめに
Unityは自由度が高いため、人によってヒエラルキー構成であったり、スクリプトの実装方法などに大きなばらつきが生じます。
これにより、誰かが作ったUnityプロジェクトを見る際に、処理のフローが掴みづらく、この機能はなんで動いているんだろう?といった疑問を持った経験がある方は少なくないかと思います。
この問題を解決すべく、どのようなフローで処理が行われているかを把握しやすくした、普段私が実際に行っているUnityでのアプリケーション開発手法についてご紹介します。まず結論
- エントリーポイントを定義する
- ヒエラルキー構成を階層化し責務を明示化する
- エントリーポイントを定義したMainスクリプトで処理をハンドリングする
なぜわかりにくいのか?
Unityの処理のフローが分かりづらい要因として、エントリーポイントが明示的にないことが挙げられます。
例えば、至るスクリプトでStart()が記述されている場合、Start()の実行順序はランダムであるため、どのスクリプトのStart()から呼ばれるかは保証されません。
そのため、スクリプトからはどういう処理フローになっているかを読み解くことは非常に困難です。解決策
そこで、エントリーポイントとなるStart()を一つのスクリプトのみで記述し、その他のスクリプトではInitialize()といったメソッドを用意して、Start()からInitialize()を呼びます。
これをすることにより、Start()は一つしか存在しないため、そこから処理をたどっていくことが可能となります。※GameObjectのライフサイクルの問題などで、処理上どうしてもStart()が複数必要になるケースは棚上げします。
実装例
実際どのように実装するか一例をご紹介します。
開発環境
- Windows 10 Version 20H2
- Unity 2020.1.14f1
- UniTask Ver.2.0.37
ヒエラルキー構成
Mainと命名したGameObjectをルートに作成し、その配下にGameObjectを役割ごとに階層構造に配置します。個人的には以下のような構成にするのが分かりやすくておすすめです。
ここで少し話が逸れますが、細かくプレファブ化をすることで、チームで一つのUnityプロジェクトをGitを使って開発する際に、同時に同じプレファブをいじらないようにすることで競合が起こりにくくすることができます。
Mainスクリプトの作成
Main.csを作成し、そこでエントリーポイントとなるStart()を記述します。Start()の中でその他のスクリプトのInitialize()を呼び、初期化処理を行います。また、それぞれの機能を実装したスクリプトのメソッドをMain.csから呼ぶことで、処理のフローの管理もしやすくなります。
これにより、Main.csを見れば大体の処理のフローを掴めるようにすることができました。
(今回の例では、カスタマイズ性が高いので、UniTaskでUpdate処理を置き換えています。)Main.csusing Cysharp.Threading.Tasks; using System.Threading; using UnityEngine; public class Main : MonoBehaviour { [SerializeField] private PlayerController m_PlayerController = null; [SerializeField] private AudioManager m_AudioManager = null; [SerializeField] private DataManager m_DataManager = null; [SerializeField] private InputManager m_InputManager = null; [SerializeField] private UIManager m_UIManager = null; [SerializeField] private XRManager m_XRManager = null; /// <summary> /// エントリーポイントです。 /// </summary> private async UniTask Start() { await InitializeAsync(); UpdateLoop(this.GetCancellationTokenOnDestroy()).Forget(); } /// <summary> /// 初期化処理を実行します。 /// </summary> private async UniTask InitializeAsync() { m_PlayerController.Initialize(); m_AudioManager.Initialize(); m_DataManager.Initialize(); m_InputManager.Initialize(); m_UIManager.Initialize(); m_XRManager.Initialize(); await UniTask.Yield(); } /// <summary> /// Update処理を実行します。 /// </summary> private async UniTaskVoid UpdateLoop(CancellationToken cancellationToken) { while (true) { // Updateで実行する処理をここに記述します。 await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken); } } }まとめ
Mainスクリプトを読めば大体の処理のフローが分かるという安心感は、実装者側も読む側もハッピーにしてくれます。Unityは作るアプリケーションによって、適した構成も変わるので一概には言えませんが、処理のフローを分かりやすく作りたいと思っている方は参考にしてみてください。
すべてのUnity使いに幸あれ。
- 投稿日:2020-11-28T18:42:13+09:00
.NET5の簡単なまとめ .NET5リリース記念オンラインイベントに参加しました
はじめに
Announcing .NET 5.0 | .NET Blogにあるように、.NET5が正式にリリースされました。
オンラインイベントに参加して.NET5面白いと思ったので、簡単にまとめてみました。
詳しくは公開資料を見た方が良いです!
【オンライン/開催日決定】.NET 5 リリース記念 C# Tokyo イベント - connpass.NET5 について
- .NET Core + .NET Framework + XAMARIN/MONO
.NET5でもWPF, Windows Formsでの開発は引き続き可能。
.NET Framework 4.8が最終メジャーリリース。それ以降はサポートメインに。
共通言語ランタイムが異なる。
.NET Framework: CLR 4.0
.NET 5: CLR 5.0
=>C++アプリから呼び出す必要がある場合は.NET Frameworkに残す必要あり。
- GA(Current)なので、LTSと異なりサポート期間は短い
GA: GA/LTSがリリースされてから3ヶ月
LST: 最低3年
- C#9 F#5へアップデート
C# 9.0については以下を参照。
What's new in C# 9.0 - C# Guide | Microsoft Docs
C# 小ネタ:.NET 5.0 で使える C# 9.0 で気に入ってる機能紹介 - Qiita
=>Recode typesでimmutableクラスを明示しやすいのはいいかも。
- null許容参照型のアノテーションを.NETライブラリに導入。
null許容参照型のアノテーションstring?
でnullを許容する型だと示す。
- TFMがプラットフォーム共通とOS固有と大きく2つになった
>ターゲット フレームワーク モニカー (Target Framework Moniker)。
.NET アプリまたはライブラリのターゲット フレームワークを指定するための標準化されたトークン形式のこと。
.NET 用語集 | Microsoft Docs
- パフォーマンス改善
- ARM64対応
- Windows 10 APIが使いやすくなった
カメラ、センサー、Windows ML、BLEなどが簡単に使える。
.NET 5からはTFMの指定のみで呼び出し可能。
DesktopのアプリやライブラリはUWPのAPIを簡単に参照できるようになった。
その他今後注目したいライブラリやツール
Windows UI Library 3.0 (Preview)
Windows 10のネイティブUIがOS組み込みからOSS+Nugetパッケージにする。
https://github.com/microsoft/microsoft-ui-xamlProject Tye
.NETアプリケーションのKubernetesへのデプロイを自動化などマイクロサービス開発が楽になるツール。Preview前。
https://devblogs.microsoft.com/aspnet/introducing-project-tye/まとめ
.NET Core と .NET Framework, XMARINが一つになって共通のAPIになって試しやすくなって来るのかなと感じました。
Windows UI Library 3.0は、UIの部品化するということで、Atomic Designの考えを取り入れて部品として使いまわせるようになるのかなと思いましたがどうなんでしょう。
ちょっと試したくなったら以下を参考にDocker環境作ってみようかなと思いました!
Docker 上でお手軽にC#9(.NET 5)を試す - Qiita
- 投稿日:2020-11-28T15:42:25+09:00
C#のDelegate、Action、Funcの動き
概要
DelegateCommand
の引数として使用するAction
についてよくわかっていなかったため、掘り下げようと思ったが、関係する大元のDelegate
があまりにも多岐にわたりすぎていてとりあえず以下の3つの動きを確認したときの備忘録です。
- Delgateクラス
- Acitonデリゲート
- Funcデリゲート
Delegate
デリゲートを表します。デリゲートとは、静的メソッドを参照するデータ構造、またはクラス インスタンスおよびクラスのインスタンス メソッドを参照するデータ構造です。
Delegateクラスの派生としてAction、Funcそれぞれのデリゲートが存在する
まず
Delegateクラス
があって、その派生にDelegate
から派生したActionデリゲート
、Funcデリゲート
がある。
この名前から想定されるようにActionとFuncの違いはTResult
の有無であり、以下のように使い分ける
デリゲート 用途 Action UIの操作(アクション) Func コールバックなどの関数 以下略以降、T1~T16の最大16個の引数を受け取れるデリゲートが定義されている
- Delegate - Action - Action<T>, - Action<T1, T2> - 以下略 - Func<TResut> - Func<T1, TResult> - 以下略Actionデリゲート
値を返さないメソッド
をカプセル化します。Funcデリゲート
TResult パラメーターで指定された型の値を返すメソッド
をカプセル化します。試したコード
class Program { static void Main(string[] args) { Console.WriteLine("----- Main - Start -----"); DelegateTarget((string msg) => Console.WriteLine($"{msg} Callback")); ActionTarget((string msg) => Console.WriteLine($"{msg} Callback")); FuncTarget((string msg) => { Console.WriteLine($"{msg} Callback"); return true; }); Console.WriteLine("----- Main - End -----"); } delegate void Callback(string message); static void DelegateTarget(Callback callback) { Console.WriteLine("***** Delegate - Start *****"); callback("Delegate"); Console.WriteLine("***** Delegate - End *****"); } // 戻り値の設定はできない static void ActionTarget(Action<string> action) { Console.WriteLine("***** Action - Start *****"); action("Action"); Console.WriteLine("***** Action - End *****"); } // 戻りの型引数は必須 static void FuncTarget(Func<string, bool> func) { Console.WriteLine("***** Func - Start *****"); func("Func"); Console.WriteLine("***** func - End *****"); } }結果
PS> dotnet run ----- Main - Start ----- ***** Delegate - Start ***** Delegate Callback ***** Delegate - End ***** ***** Action - Start ***** Action Callback ***** Action - End ***** ***** Func - Start ***** Func Callback ***** func - End ***** ----- Main - End -----
- 投稿日:2020-11-28T15:38:46+09:00
[C#の基礎] デザインパターン/テンプレートパターン
処理の流れは同じだが、場合によって処理の中身を変更したい場合に使えるパターン。
基本的な処理の流れの定義
public abstract class AbstractFoo{ public virtual void Process(){ Initialize(); ProcessMain(); Terminate(); } public abstract void Initialize(); public abstract void ProcessMain(); public abstract void Terminate(); }抽象クラスにしているのはこのクラス単体でインスタンス生成できなくするため。
実際の処理の中身を書く
public class FooImplementationA : AbstractFoo{ public void Initialize(){ // do something } public void ProcessMain(){ // do something } public void Terminate(){ // do something } } public class FooImplementationB : AbstractFoo{ public void Initialize(){ // do something another type } public void ProcessMain(){ // do something another type } public void Terminate(){ // do something another type } }利用
単純にインスタンス生成して使う場合
var fooA = new FooImplementationA (); fooA.Process(); var fooB = new FooImplementationB (); fooB.Process();ファクトリー使うならこう
class FooFactory{ public Foo GetFoo(string type){ if(type == "A") return new FooImplementationA (); if(type == "B") return new FooImplementationB (); } } var factory = new FooFactory(); var foo = factory.GetFoo("A"); foo.Process();DI使うならこう
var builder = new ContainerBuilder(); builder.RegisterType<FooImplementationA>() .As<Foo> .InstancePerLifetimeScope() ; var container = builder.Build(); var foo = container.Resolve<Foo>(); foo.Process();構成によって FooImplementationA か FooImplementationB かのどちらかを登録するようにすれば、実行時に構成ごとに使う実装を切り替えられる。
- 投稿日:2020-11-28T15:38:46+09:00
[C#の基礎] GoFデザインパターン/テンプレートパターン
処理の流れは同じだが、場合によって処理の中身を変更したい場合に使えるパターン。
基本的な処理の流れの定義
public abstract class AbstractFoo{ public virtual void Process(){ Initialize(); ProcessMain(); Terminate(); } public abstract void Initialize(); public abstract void ProcessMain(); public abstract void Terminate(); }抽象クラスにしているのはこのクラス単体でインスタンス生成できなくするため。
実際の処理の中身を書く
public class FooImplementationA : AbstractFoo{ public void Initialize(){ // do something } public void ProcessMain(){ // do something } public void Terminate(){ // do something } } public class FooImplementationB : AbstractFoo{ public void Initialize(){ // do something another type } public void ProcessMain(){ // do something another type } public void Terminate(){ // do something another type } }利用
単純にインスタンス生成して使う場合
var fooA = new FooImplementationA (); fooA.Process(); var fooB = new FooImplementationB (); fooB.Process();ファクトリー使うならこう
class FooFactory{ public Foo GetFoo(string type){ if(type == "A") return new FooImplementationA (); if(type == "B") return new FooImplementationB (); } } var factory = new FooFactory(); var foo = factory.GetFoo("A"); foo.Process();DI使うならこう
var builder = new ContainerBuilder(); builder.RegisterType<FooImplementationA>() .As<Foo> .InstancePerLifetimeScope() ; var container = builder.Build(); var foo = container.Resolve<Foo>(); foo.Process();構成によって FooImplementationA か FooImplementationB かのどちらかを登録するようにすれば、実行時に構成ごとに使う実装を切り替えられる。
- 投稿日:2020-11-28T13:03:42+09:00
C#でHTTP POSTに複数コンテンツを含める
この記事で解説すること
C#での複数コンテンツのPost方法を実例付きで解説する。
(複数コンテンツというのは、例えば、画像データとその名前の2つなどの事です。)複数コンテンツのHTTP POSTの実現方法
以下を使うと複数コンテンツのPOSTが行えます。
- System.Net.Http.HttpClientのPostAsync(Uri, HttpContent)メソッド
- あるいは、PostAsync(uri, HttpContent, fileName)メソッド
- System.Net.Http.MultipartFormDataContect(HttpContentの派生クラス)
要は、MultipartFromDataContenctにデータを追加していき、HttpClientのPostAsyncメソッドの引数として渡すだけです。(MultipartFormDataContentはHttpContentクラスを継承している)
実践-条件
それではやり方の一例を見ていきます。
今回は、以下のコンテンツをPostで送りたいとします。Postで送るもの
- 画像データと、その名前を一度にPostする
- 画像データ(image):バイナリデータ
- データ名(name):"hogehoge"
Post結果の確認方法
今回の例では、Post結果の確認にhttpbinを使わせてもらいます。
httpbinは、指定のURLにHTTPリクエストを送ると、送信されたパラメータなどの情報をレスポンスとして返してくれるWebサービス&ソフトウェアです。以下のJsonがPostのResponseとして返ってくれば成功
今回の場合は、http://httpbin.org/post にPOSTリクエストを送り、以下のようなJSONがReponseとして返ってくれば成功です。
確認するポイントは2つです。
- "files"内の"image"に指定したデータ(今回だと画像をバイト配列として送るのでバイト配列)が入っているかどうか
- "form"内の"name"に指定した名前(hogehoge)が入っているどうか
{ "args": {}, "data": "", "files": { "image": "(~byteの羅列がここに入る。長いので省略~)" }, "form": { "name": "hogehoge" }, "headers": { "Content-Length": "13515", "Content-Type": "multipart/form-data; boundary="5cc407cc-3e4e-4d4a-8f67-fd2f29436423"", "Host": "httpbin.org", "X-Amzn-Trace-Id": "Root=1-5fc1ad93-3233ba1c3f705dd867fdf9c9" }, "json": null, "origin": "126.99.210.176", "url": "http://httpbin.org/post" }実行環境
- Windows10
- C#8.0 (.NET Core3.1)
実践
本題のコードが以下です。
// MultipartFormDataContentのインスタンスをつくる。 using MultipartFormDataContent multiContent = new MultipartFormDataContent(); // コンテンツを、それぞれ専用の形式でインスタンス化する。 // imageBytes変数には本当は画像をバイナリ化したものが代入されている想定。 // ここでは簡単のため適当なバイナリを入れておく。 using ByteArrayContent imageContent = new ByteArrayContent(new byte[4] { 0, 1, 2, 3 }); using StringContent nameContent = new StringContent("hogehoge"); // 専用の形式にしたコンテンツを、MultipartFormDataContentにAddしていく。 multiContent.Add(imageContent, "image", "imageData"); multiContent.Add(nameContent, "name"); // HttpClientでPostする。 // (本当はHttpClientは都度インスタンスを生成するのではなく、アプリケーション内で使いまわしたほうがよい。) using HttpClient client = new HttpClient(); var uri = new Uri("http://httpbin.org/post"); var msg = await client.PostAsync(uri, multiContent ); // Responseの表示 var responseContent = await msg.Content.ReadAsStringAsync(); Console.WriteLine($"{responseContent}");コードについて解説していきます。
- MultipartFormDataContentのインスタンスをつくる
// MultipartFormDataContentのインスタンスをつくる。 using MultipartFormDataContent multiContent = new MultipartFormDataContent();ここは特に解説の必要もないと思います。
MultipartFormDataContentの派生大元であるHttpContentクラスはIDisposableを継承しているので、usingをつけて勝手に開放してくれるようにしておきます。
(この記事の趣旨とは関係ないですが、C#8.0から、using変数宣言により、その変数のスコープに基づいて自動開放してくれるようになりました。usingステートメントによる多重{}はコードが見にくくなるので、ここではusing変数宣言を使っています。)
- コンテンツをそれぞれ専用の形式でインスタンス化する
// コンテンツを、それぞれ専用の形式でインスタンス化する。 // imageBytes変数には本当は画像をバイナリ化したものが代入されている想定。 // ここでは簡単のため適当なバイナリを入れておく。 using ByteArrayContent imageContent = new ByteArrayContent(new byte[4] { 0, 1, 2, 3 }); using StringContent nameContent = new StringContent("hogehoge");MultipartFormDataContentに追加していく(もといHttpClientのコンテントとして扱う)ためには、各種コンテンツを専用の形式にしていく必要あります。
専用の形式とは、System.Net.Http.HttpClientクラスの派生クラスのことです。ここでは、画像データ(それを事前にバイナリ化したもの)と文字列をそれぞれHttpContent化します。画像データはByteArrayContentクラス、文字列はStringContentクラスでHttpContent化します。(ここでは結果が見やすいよう画像データには適当なバイナリをいれてます。)
この部分は送りたいデータの形式に合わせて変えます。(他にはStreamContentクラスや、JsonContentクラスがあります。詳細はHttpContentクラスのドキュメントを読んでください。)
ちなみに、MultipartFormDataContentクラスももちろんHttpContentクラスの派生クラスです。(正確にはMultipartFormDataContentクラス → MultipartContentクラス → HttpContentクラスという継承関係)
- 専用の形式にしたコンテンツをMultipartFormDataContentにAddしていく
// 専用の形式にしたコンテンツを、MultipartFormDataContentにAddしていく。 multiContent.Add(imageContent, "image", "imageData"); multiContent.Add(nameContent, "name");上で専用の形式(HttpContentクラスの派生クラス)にしたデータを追加していくだけです。
MultipartFormDataContent.Add(HttpContent, string)メソッドを使って追加していきます。ここで、画像データと文字列データでは、引数の異なるAdd()メソッドを使っています。
MultipartFormDataContent.Add()メソッドには、引数のとり方が3種類あります。
- HttpContentのみ
- HttpContentとString(HTTPコンテンツの名前)
- HttpContentとStringとString(HTTPコンテンツの名前とコレクションに追加する HTTP コンテンツのファイル名)
1, 2番目の方法だとコンテンツがRequestの"form"に含まれ、3番目の方法だとコンテンツがRequestの"files"に含まれます。
今回は、画像データは"files"に含まれて欲しいので3番目、文字列データは"forms"に含まれて欲しいので2番目のメソッドを使います。なお、3番目の方法の場合は、引数の3つ目に文字列が必要ですがこれは今回の場合何でもよいです。
(ちなみに1番目の方法を使った場合は、コンテンツはform"に含まれ、そのkeyにあたるものがnullとなります。)
- POSTする
// HttpClientでPostする。 // (本当はHttpClientは都度インスタンスを生成するのではなく、アプリケーション内で使いまわしたほうがよい。) using HttpClient client = new HttpClient(); var uri = new Uri("http://httpbin.org/post"); var msg = await client.PostAsync(uri, multiContent ); // Responseの表示 var responseContent = await msg.Content.ReadAsStringAsync(); Console.WriteLine($"{responseContent}");通常通りHttpClientのPostAsync()メソッドを使ってPostするだけです。
(上のコードではPostとそのResponseの表示をしています。)結果
コードを実行すると以下がResponseとして返ってきました。
大丈夫そうですね。
- "files"内の"image"に指定したデータが入っている("\u0000\u0001\u0002\u0003"は1, 2, 3, 4をそれぞれ16進数表記にしたもの)
- "form"内の"name"に指定した名前(hogehoge)が入っている
{ "args": {}, "data": "", "files": { "image": "\u0000\u0001\u0002\u0003" }, "form": { "name": "hogehoge" }, "headers": { "Content-Length": "13515", "Content-Type": "multipart/form-data; boundary="5cc407cc-3e4e-4d4a-8f67-fd2f29436423"", "Host": "httpbin.org", "X-Amzn-Trace-Id": "Root=1-5fc1ad93-3233ba1c3f705dd867fdf9c9" }, "json": null, "origin": "126.99.210.176", "url": "http://httpbin.org/post" }まとめ
この記事では、C#での複数コンテンツのPost方法を実例付きで解説しました。
簡単にやり方をまとめると以下です。
- MultipartFormDataContentのインスタンスをつくる
- コンテンツをそれぞれ専用の形式でインスタンス化する
- 専用の形式にしたコンテンツをMultipartFormDataContentにAddしていく
- 追加したい形式によって複数あるAddメソッドを使い分ける
- POSTする
リンク
- 投稿日:2020-11-28T10:24:19+09:00
C#でKey(変数)が定まっていないJsonをParseする
この記事で解説すること
Keyが定まっていないメンバーを含むJsonをParse(Desrialize)する方法
やりたいこと
Json形式はAPIのResponseなどとしてよく使われています。
Jsonの最小要素はKeyとValueのペアですが、このKey側があらかじめ定まっていない場合(静的でない)は、情報の取り出しに少し工夫がいります。
(C#での話です。Pythonだと"request"モジュールで自動的にParseしてくれたりします。)
(ちなみに、あらかじめKeyが定まっている場合はC#標準のDataContractJsonSerializerとかを使えば簡単にParseできます。)// Jsonの最小要素 // { "key" : value }の形で表される // この"key"の部分(ここでいう"name")が常に固定でない場合のParseをしたい。 { "name": "Tanaka" }Keyが定まっていないJsonの実例
題材がないと説明しにくいので、Keyが定まってないJsonの例を一つ書きます。
これは、SlackのWorkspaceに登録されている絵文字一覧を取得するAPI のResponseとして渡されるJsonです。最初にある"ok":trueは、Requestが上手くできたかの値なのでここでは関係ありません。
問題は次の"emoji"に対応する項目です。
"bowtie", "squirrel".. というように、登録されている絵文字の名前がKeyとして並んでいます。しかし、登録されている絵文字はあらかじめ決まっていない(Slackではユーザーが新しく絵文字を追加さることができる)です。つまり、"emoji"の中身の情報を取り出そうとすると、Keyが定まっていないJsonをParseする必要があります。
// ok, emojiがkeyとなるのは確定している。 { "ok": true, "emoji": { // emojiの各項目は、Key=絵文字の名前: Value=絵文字画像のurl(またはalias)、という形式。 // Key=絵文字の名前であり、それはあらかじめ定まっていないためParseに工夫が必要。 // また、emojiの項目数は固定ではない。 "bowtie": "https://my.slack.com/emoji/bowtie/46ec6f2bb0.png", "squirrel": "https://my.slack.com/emoji/squirrel/f35f40c0e0.png", // … (以下登録されている絵文字が続く) } }この記事では、このJsonを例にとって、Parse方法を見ていきます。
環境
- Windows10
- VisualStudio2019 Community
- C#8.0 (.NET Core3.1)
- Newtonsoft.Json(Json.NET) v12.0.3
今回は、Newtonsoft.Json(Json.NET)を使ってParseを行います。Newtonsoft.JsonはNugetからインストールします。
(C#標準でもJsonを扱うためのクラス群が用意されているのですが、予めKey(文字列)が定まっていないJsonのParse方法はなさそうでした。やり方知っている方いれば教えてください。)事前知識
本題に入る前に、Newtonsoft.Jsonの基本事項を確認しておきます。
Newtonsoft.Jsonを使ってJsonをParseする場合は、対象のJsonを、Jvalue, JObject, JArrayの3つの型に変換していく必要があります。Jvalue, JObject, JArrayの3つは、Parse対象のJsonの構成によって、適切に使い分ける必要があります。
(※Jsonの構成によっては、明示的にJValue, JObject, JArrayなどに変換しなくても、自動的に変換してくれるメソッドが用意されています。)JValue, JObject, JArrayについて、以下に説明と簡単な例を示します。
- JValue型 : プリミティブ型を表す。(文字列、数値など)
- JObject型 : 単純なKeyとValueの羅列を表す。
- JArray型 : 配列。いくつかのJObjectをひとまとめにしたものを表す。
- JToken型:JValue, JObject, JArrayのベースクラス。
(ちなみに、今回のParseで使うのはJObjectだけです。)// {}のまとまりがJObject (Taroや24はJValue) { "name" : "Taro", "age" : 24 } // []のまとまりがJArray [ { "name" : "Taro" }, { "age" : 24 }, ... ]本題
まずParseしたいJsonの構成を見て、使う型を選びます。
下記のJsonなら以下のようにParseできそうです。(今回はJObject型のみ使用)
- 全体をJObject型で受け取る。(全体 = 一番外側の"ok"や"emoji"を含む{}のこと)
- 1の中から"emoji"のValue("emoji" : 以降の{}の中身全体)をJObject型で受け取る
- 2の中から各項目をKeyValuePairとして取り出す
{ "ok": true, "emoji": { // emojiの各項目は、Key=絵文字の名前: Value=絵文字画像のurl(またはalias)、という形式。 // Key=絵文字の名前が不定のため特別にParseする必要がある。 // また、emojiの項目数は固定ではない。 "bowtie": "https://my.slack.com/emoji/bowtie/46ec6f2bb0.png", "squirrel": "https://my.slack.com/emoji/squirrel/f35f40c0e0.png", … } }実際に、1~3までの流れをコードで書くとこうです。
string jsonString = (↑に示したJsonが代入されているとする) // 1. 全体をJObject型で受け取る。 // jsonStringはResponseなどで受け取った、json構造のstring型変数とする。 // string --> JObjectの変換はJObject.Parse()で行う。 JObject jsonObject = JObject.Parse(jsonString); // 2. 1の中から"emoji"のValueをJObject型で受け取る。 // ["(KeyName)"]で特定のKeyのValueを、JObjectから取り出せる。 // ここでは取り出すValueもJObject型のため、JObject型の変数に代入してやる。明示的なCastが必要。 JObject emojis = (JObject)jsonObject["emoji"]; // 3. 2の中から各項目をKeyValuePairとして取り出す // JObject型はIEnumerableを継承しており、 // GetEnumerator()でKeyValuePair<string, JToken?>を返す。 // 上の例では、KeyValuePairのKeyが絵文字の名前、Valueが絵文字のURIとなる。 foreach (var emoji in emojis) { var name = emoji.Key; // KeyValuePairのValueはJToken?型のため、stringに変換する。 var uri = new Uri(emoji.Value.ToString()); Console.WriteLine($"EmojiName : {name}, EmojiUri : {uri}"); } // ============================ // Output : // EmojiName : bowtie, EmojiUri : https://my.slack.com/emoji/bowtie/46ec6f2bb0.png // EmojiName : squirrel, EmojiUri : https://my.slack.com/emoji/squirrel/f35f40c0e0.png // ...コード中にここまでで解説していないものがいくつかあるので、補足していきます。
- JObject.Parse()
JObject jsonObject = JObject.Parse(jsonString);引数に与えられたstring型をJObject型に変換するメソッドです。
コード中では、まずこのメソッドを使って、Responseなどで受け取ったstring型をJObjectに変換しています。なお、引数に渡すstring型はJson形式になっている必要があります。(Json形式になっていない場合は、Newtonsoft.Json.JsonReaderExceptionがthrowされます。)
- Keyを指定したValueの取り出し
JObject emojis = (JObject)jsonObject["emoji"];JObject型から特定のKeyに対応するValueを取り出したい場合は、Dictionary型などと同様に、["(Key)"]の形で指定してやると取り出すことができます。
この際、取り出したValueを適切な型にCastしてやる必要があります。(正確にいうと、この時点ではCastしなくても大丈夫です。Castしない場合はJToken型となります。)例ではJObject型にCastしていますが、JArray型にCast使用とすると例外(System.InvalidCastException)がthrowされます。
- JObjectからの個別要素の取り出し
// emojisがJObject // emojisの実態は、 // "bowtie": "https://my.slack.com/emoji/bowtie/46ec6f2bb0.png" // "squirrel": "https://my.slack.com/emoji/squirrel/f35f40c0e0.png", ... // というKeyとValueの羅列 foreach (var emoji in emojis) { .... }ここで、変数emojisはJObject型であり、その実態はKeyとValueの羅列です。
JObject型はIEnumerableを継承しており、foreachで扱えます。
このとき、各要素(GetEnumerator()の戻り値)は、KeyValuePairとなります。(↑の例だと、Keyのstringが絵文字の名前、Valueが絵文字のuriとなります)なお、KeyValuePairのValueは、JToken型となるので利用するにはCastが必要となります。
まとめ
この記事では、Keyの名前が静的でないJsonのParse(Deserialize)方法を書きました。
簡潔にまとめると以下です。
- ParseしたいJsonの構成を見て、使う型(JObject, JArray, JValueなど)を選ぶ。
- string型をJObject.Parseで変換し、Newtonsoft.jsonで扱えるようにする。
- Json全体から、必要な部分を、選んだ型に変換しつつ取り出していく。
- Keyの名前が静的でない部分は、JObjectからKeyValuePairを受け取ることにより、取り出す。
各種リンク
- 投稿日:2020-11-28T00:04:35+09:00
[C#] Mutexでリソースの排他制御をする
もくじ
https://qiita.com/tera1707/items/4fda73d86eded283ec4fmutex関連
https://qiita.com/tera1707/items/4fda73d86eded283ec4f#cc-mutex%E9%96%A2%E9%80%A3やりたいこと
C#のアプリとC++のアプリの2つのアプリから一つのリソース(例えばファイル)に読み書きするときに、同時に読み書きしてしまうといろいろ都合が悪いので、同時にアクセスしないように排他制御をしたい。
やり方
Mutex
を使う。基本的には、
- 別のアプリと同じ名前の名前付きMutexを作る。
- そのMutexを所有権を要求(WaitOne)して、
- 使える状態(シグナル状態)であれば、そのまま自分が所有して、使いたいリソースを使う。
- 他ですでに所有されている状態(非シグナル状態)であれば、他がmutex開放して使える状態になるまで待つ。
- 使いたいリソースを使い終わったら、所有していたmutexを開放(Release)する。
という流れでmutexを使う。
C#のサンプルコード
下記が、WPFでmutexを使うサンプルコード。別途C++版も作成して両方からmutexを使う実験をする。
MainWindow.xaml<Window x:Class="WpfApp48.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfApp48" mc:Ignorable="d" Title="MainWindow" Height="900" Width="800" Loaded="Window_Loaded" Name="root"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="5*"/> <RowDefinition Height="1*"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <StackPanel Grid.Row="1" Grid.Column="0"> <CheckBox Name="cbSecurity" Content="フルアクセスあり"/> <Button Content="Mutex作成" Click="Button_Click" Height="120"/> </StackPanel> <Button Grid.Row="1" Grid.Column="1" Content="MutexをWaitOne" Click="Button_Click_1"/> <Button Grid.Row="1" Grid.Column="2" Content="MutexをRelease" Click="Button_Click_2"/> <ListBox Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" ItemsSource="{Binding Logs, ElementName=root}"/> </Grid> </Window>MainWindow.xaml.csusing System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Security.AccessControl; using System.Security.Principal; using System.Threading; using System.Windows; namespace WpfApp48 { public partial class MainWindow : Window, INotifyPropertyChanged { #region INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName)=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); #endregion #region LogFramework public ObservableCollection<string> Logs { get; set; } = new ObservableCollection<string>(); public void AddLog(string log) { DateTime now = DateTime.Now; Logs.Add(now.ToString("hh:mm:ss.fff ") + log); OnPropertyChanged(nameof(Logs)); } #endregion // Mutexの名前 // 「Global\\」をつけると、自分以外のUserとも共有できるMutexになる // ただし、Create時に振るアクセスできるようにしておかないと、別Userが // Create時にアクセス拒否例外になる string mutexName = "Global\\MyMutex"; Mutex mutex; public MainWindow() => InitializeComponent(); private void Window_Loaded(object sender, RoutedEventArgs e) { } // Mutex作成 private void Button_Click(object sender, RoutedEventArgs e) { if (mutex == null) { try { if (cbSecurity.IsChecked != false) { var mutexSecurity = new MutexSecurity(); mutexSecurity.AddAccessRule( new MutexAccessRule( new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.Synchronize | MutexRights.Modify, AccessControlType.Allow ) ); mutex = new Mutex(false, mutexName, out _, mutexSecurity); AddLog("MyMutex作成OK(フルアクセス)"); } else { mutex = new Mutex(false, mutexName, out _, null); AddLog("MyMutex作成OK(通常アクセス)"); } } catch (Exception ex) { AddLog(ex.Message); } } else { AddLog("MyMutexすでに作成済み"); } } // チェック private void Button_Click_1(object sender, RoutedEventArgs e) { bool signal = false; if (mutex == null) { AddLog("MyMutex未作成"); return; } else { AddLog("MyMutex WaitOne()実行"); } try { // mutexの所有権を要求する // C++のCreateMutex()とOpenMutex()を一緒にやる感じ signal = mutex.WaitOne(5000); } catch (AbandonedMutexException ex) { // 相手がmutexを解放する前に終了してしまった場合 signal = true; AddLog(ex.Message); } if (signal) { AddLog("MyMutex作成完了"); } else { AddLog("MyMutexタイムアウト"); mutex = null; } } // 解放 private void Button_Click_2(object sender, RoutedEventArgs e) { if (mutex != null) { AddLog("Mutex解放します"); mutex.ReleaseMutex(); mutex.Close(); mutex = null; } else { AddLog("MutexすでにReleaseしてます"); } } } }実装・実験するうえで引っかかったこと
今回は、「別々のユーザーで動いているC#とC++のアプリで、リソースを排他制御したい」という要件があった。
(具体的には、C++はサービスとして動いていて、C#はGUIをもつWPFアプリとして動く。)
そういう条件でmutexを作ったときに困ったのが下記のような点。普通の名前のmutexにすると、他のユーザーがすでにつけているmutexの名前と同じ名前のmutexを作る/所有する、ということができてしまう
つまり、そのままだと全然排他できてない、ということ。
(※同じユーザーが起動したものであれば、別のアプリであっても普通の名前で排他できていた)対処方法としては、名前の前に
Global\
をつけて「グローバルミューテックスにする」ということ。string mutexName = "Global\\MyMutex";グローバルミューテックスにすれば、別のユーザーであっても同じ名前を付けていれば、排他制御に使える。
その次に引っかかったのが、
別のユーザーが作ったグローバルミューテックスと同じ名前のmutexを作ろうとすると、「アクセスが拒否されました」という例外になる
別ユーザーが作成したグローバルミューテックスは、そのままではアクセスできない様子。
対処方法としては、「グローバルミューテックスをフルアクセスできるようにして作成する」ということ。var mutexSecurity = new MutexSecurity(); mutexSecurity.AddAccessRule( new MutexAccessRule( new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.Synchronize | MutexRights.Modify, AccessControlType.Allow ) ); mutex = new Mutex(false, mutexName, out _, mutexSecurity);どのユーザーであってもアクセスできるようにしてやることで、グローバルミューテックスを通して複数ユーザーで排他できるようになった。
所有権を要求(WaitOne)をしてないのにReleaseすると例外になる
通常、mutexを作って所有権を要求(WaitOne)して、処理が終わったらRelease、という流れになるが、
mutexを作るだけ作ってWaitOneせずにReleaseしたら、下記のような例外になる。
非同期ブロックから呼び出す
って何?と思うが、ともかくそういう例外になる。
対処としては、単純にWaitOneしてないものはReleaseしないようにする。
(これ起きるのはC#版だけかも?)以上
上記を気にして作成すれば、C#版とC++版でmutexを通じて排他制御ができた。
参考
Mutexクラス(MSDocs)
https://docs.microsoft.com/ja-jp/dotnet/api/system.threading.mutex?view=net-5.0