- 投稿日:2020-09-26T22:22:35+09:00
D言語からC++/CLIを経由して.NET APIを利用する(旧暦、和暦処理)
はじめに
D言語から
.NET API
を利用したい場合に、どうすればよいかを考えてみました。
何かの参考になればと思います。やりたいこと
前回の記事で、D言語でのカレンダー表示プログラムを実装しました。
引き続き、旧暦カレンダーを実装してみたいと思います。
.NET API
には、JapaneseCalendarクラスやJapaneseLunisolarCalendarクラスが用意されており、これらを使えば、旧暦や和暦の計算を一から実装せずに済みそうです。D言語から.NET API呼び出しの実現方法
私が調べた限り、
C++/CLIラッピング
を経由する方法が簡単だと思いました。
今回はこの方法を採用します。※参考までに、記事を書くために調べた情報リンクを最後に掲載します。
C#
でDLLを作成し、D言語から呼び出す方法です。前準備1(C#での実装)
D言語で実装する前に、
C#
で旧暦や和暦を取得する処理を実装しました。
実行時にパラメータなしだと今日の日付の旧暦や和暦、年月日パラメータを指定するとその日付の旧暦や和暦を表示します。JCalendar.csusing System; using System.Globalization; public class JCalendar { static string[] sEra = // 元号 { "", "明治", "大正", "昭和", "平成", "令和" }; static string[] sRokuyo = // 六曜 { "大安", "赤口", "先勝", "友引", "先負", "仏滅" }; static string[] sKanshi = // 天干 { "", "甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸" }; static string[] sChishi = // 地支 { "", "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥" }; public static void Main(string[] args) { DateTime newDate; if ( args.Length >= 3 ){ newDate = new DateTime( int.Parse(args[0]), int.Parse(args[1]), int.Parse(args[2])); } else { newDate = DateTime.Now; } Console.WriteLine("西暦 : {0:d}", newDate); if ( newDate >= new DateTime(1868, 9, 8) ){ printJapaneseCalendar(newDate); } if ( newDate >= new DateTime(1960, 1, 28) && newDate <= new DateTime(2050, 1, 22) ){ printJapaneseLunisolarCalendar(newDate); } } static void printJapaneseCalendar(DateTime newDate) { JapaneseCalendar jc = new JapaneseCalendar(); int era = jc.GetEra(newDate); int year = jc.GetYear(newDate); int month = jc.GetMonth(newDate); int day = jc.GetDayOfMonth(newDate); DateTime jDate = new DateTime(year, month, day); Console.WriteLine("和暦 : {0} {1:d}", sEra[era], jDate); } static void printJapaneseLunisolarCalendar(DateTime newDate) { JapaneseLunisolarCalendar jlc = new JapaneseLunisolarCalendar(); int era = jlc.GetEra(newDate); int year = jlc.GetYear(newDate); int month = jlc.GetMonth(newDate); int day = jlc.GetDayOfMonth(newDate); //閏月を取得 string sLeap = ""; if ( year > 0 ){ int leapMonth = jlc.GetLeapMonth(year, era); if ( month == leapMonth ){ sLeap = "(閏月)"; } //閏月含む場合の月を補正 if ( (leapMonth > 0) && (month >= leapMonth) ){ month = month - 1; //旧暦月の補正 } } // 干支(天干、地支) int sy = jlc.GetSexagenaryYear(newDate); int tk = jlc.GetCelestialStem(sy); int ts = jlc.GetTerrestrialBranch(sy); // 六曜(大安・赤口・先勝・友引・先負・仏滅) // (月 + 日) % 6 の余り int rokuyo = (month + day) % 6; Console.WriteLine("旧暦 : {0} {1:d4}/{2:d2}/{3:d2} {4}", sEra[era], year, month, day, sLeap); Console.WriteLine("干支 : {0}{1}", sKanshi[tk], sChishi[ts]); Console.WriteLine("六曜 : {0}", sRokuyo[rokuyo]); } }
Window10
には、C#コンパイラ(csc.exe)
が標準インストールされています。
どのバージョンがどのフォルダにインストールされているかは、それぞれの環境に依存します。
私の環境でのコンパイル、実行例を例示します。
旧暦には、うるう月(閏月)
というものが存在します。GetLeapMonth
を使って、その年のうるう月
を算出し、月を補正します。
実行結果では、月を補正して正しく表示できています。※旧暦カレンダーで検証
干支(天干、地支)は、その年の干支を取得しています。月や日にも干支が存在するそうです。実行結果d:\Dev>C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe JCalendar.cs Microsoft (R) Visual C# Compiler version 4.8.3752.0 for C# 5 Copyright (C) Microsoft Corporation. All rights reserved. This compiler is provided as part of the Microsoft (R) .NET Framework, but only supports language versions up to C# 5, which is no longer the latest version. For compilers that support newer versions of the C# programming language, see http://go.microsoft.com/fwlink/?LinkID=533240 d:\Dev>JCalendar 西暦 : 2020/09/26 和暦 : 令和 0002/09/26 旧暦 : 令和 0002/08/10 干支 : 庚子 六曜 : 大安 d:\Dev>JCalendar 2020 5 23 西暦 : 2020/05/23 和暦 : 令和 0002/05/23 旧暦 : 令和 0002/04/01 (閏月) 干支 : 庚子 六曜 : 仏滅前準備2(C++/CLIでの実装)
次に、
C++/CLI
で旧暦や和暦を取得する処理を実装しました。
C++/CLI
に関する日本語での情報は、それほど多くない印象なので、参考になればと思います。
前準備1のC#
のソースコードを移植したイメージです。
実行時にパラメータなしだと今日の日付の旧暦や和暦、年月日パラメータを指定するとその日付の旧暦や和暦を表示します。
C++/CLI
特有のものとして、演算子 (^)
があります。ハンドル宣言子( ^ は "hat") は、オブジェクトがアクセス不可能であるとシステムが判断したときに、宣言されたオブジェクトが自動的に削除されることを意味する型指定子をに変更します。
オブジェクト演算子 (^) へのハンドル (C++/CLI および C++/CX)より引用
また、
.NET API
のクラス生成では、gcnew
を使います。マネージド型 (参照型または値型) のメモリは gcnew によって割り当てられ、ガベージ コレクションによって解放されます。
ref new、gcnew (C++/CLI および C++/CX)より引用
JCal.cppusing namespace System; using namespace System::Globalization; #include <string> String^ getEraStr(int era) { // 元号 array<String^>^ sEra = gcnew array<String^> { "", "明治", "大正", "昭和", "平成", "令和" }; return ( sEra[era] ); } String^ getRokuyoStr(int rokuyo) { // 六曜 array<String^>^ sRokuyo = { "大安", "赤口", "先勝", "友引", "先負", "仏滅" }; return ( sRokuyo[rokuyo] ); } String^ getKanshiStr(int kanshi) { // 天干 array<String^>^ sKanshi = { "", "甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸" }; return ( sKanshi[kanshi] ); } String^ getChishiStr(int chishi) { // 地支 array<String^>^sChishi = { "", "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥" }; return ( sChishi[chishi] ); } void printJapaneseCalendar(DateTime newDate) { JapaneseCalendar^ jc = gcnew JapaneseCalendar(); int era = jc->GetEra(newDate); int year = jc->GetYear(newDate); int month = jc->GetMonth(newDate); int day = jc->GetDayOfMonth(newDate); DateTime jDate = DateTime::DateTime(year, month, day); Console::WriteLine("和暦 : {0} {1:d}", getEraStr(era), jDate); } void printJapaneseLunisolarCalendar(DateTime newDate) { JapaneseLunisolarCalendar^ jlc = gcnew JapaneseLunisolarCalendar(); int era = jlc->GetEra(newDate); int year = jlc->GetYear(newDate); int month = jlc->GetMonth(newDate); int day = jlc->GetDayOfMonth(newDate); //閏月を取得 String^ sLeap = ""; if ( year > 0 ){ int leapMonth = jlc->GetLeapMonth(year, era); if ( month == leapMonth ){ sLeap = "(閏月)"; } //閏月含む場合の月を補正 if ( (leapMonth > 0) && (month >= leapMonth) ){ month = month - 1; //旧暦月の補正 } } // 干支(天干、地支) int sy = jlc->GetSexagenaryYear(newDate); int tk = jlc->GetCelestialStem(sy); int ts = jlc->GetTerrestrialBranch(sy); // 六曜(大安・赤口・先勝・友引・先負・仏滅) // (月 + 日) % 6 の余り int rokuyo = (month + day) % 6; Console::WriteLine("旧暦 : {0} {1:d4}/{2:d2}/{3:d2} {4}", getEraStr(era), year, month, day, sLeap); Console::WriteLine("干支 : {0}{1}", getKanshiStr(tk), getChishiStr(ts)); Console::WriteLine("六曜 : {0}", getRokuyoStr(rokuyo)); } int main(int argc, char* argv[]) { DateTime newDate = DateTime::Now; if ( argc > 3 ){ newDate = DateTime::DateTime( atoi(argv[1]), atoi(argv[2]), atoi(argv[3])); } Console::WriteLine("西暦 : {0:d}", newDate); if ( newDate >= DateTime::DateTime(1868, 9, 8) ){ printJapaneseCalendar(newDate); } if ( newDate >= DateTime::DateTime(1960, 1, 28) && newDate <= DateTime::DateTime(2050, 1, 22) ){ printJapaneseLunisolarCalendar(newDate); } }私の環境には、Visual C++ ビルドツール 2019がインストールされています。
※参考:Visual C++ ビルドツール 2019 インストール手順VS2019用 x64 Native Tools コマンドプロンプトを起動します。
共通言語ランタイム (CLR) 機能を使用するために、コンパイルオプション/clr
をつけてコンパイルします。旧暦としては、令和は3月27日から始まるようです。※旧暦カレンダーで検証
実行結果d:\Dev>cl /clr JCal.cpp Microsoft(R) C/C++ Optimizing Compiler Version 19.24.28314 Microsoft (R) .NET Framework の場合 バージョン 4.08.4220.0 Copyright (C) Microsoft Corporation. All rights reserved. JCal.cpp Microsoft (R) Incremental Linker Version 14.24.28314.0 Copyright (C) Microsoft Corporation. All rights reserved. /out:JCal.exe JCal.obj d:\Dev>JCal.exe 2019 4 30 西暦 : 2019/04/30 和暦 : 平成 0031/04/30 旧暦 : 平成 0031/03/26 干支 : 己亥 六曜 : 仏滅 d:\Dev>JCal.exe 2019 5 1 西暦 : 2019/05/01 和暦 : 令和 0001/05/01 旧暦 : 令和 0001/03/27 干支 : 己亥 六曜 : 大安D言語、C++/CLIラッピングのソースコード
前準備が長くなりましたが、ここからが本題です。
.NET API
を呼び出すためのC++/CLI
ラッピング処理の実装例です。
旧暦、和暦の取得結果をstruct JCAL
にセットするシンプルな関数です。
D言語から呼び出せるように、extern "C" __declspec(dllexport)
属性を付与しています。JCalDll.cppusing namespace System; using namespace System::Globalization; typedef struct { int era; int year; int month; int day; int leapMonth; int zodiac; int kanshi; int chishi; int rokuyo; } JCAL; #define VC_DLL_EXPORTS extern "C" __declspec(dllexport) VC_DLL_EXPORTS void __cdecl getJapaneseCalendar(int year, int month, int day, JCAL &jcal) { DateTime newDate = DateTime::DateTime(year, month, day); JapaneseCalendar^ jc = gcnew JapaneseCalendar(); jcal.era = jc->GetEra(newDate); jcal.year = jc->GetYear(newDate); jcal.month = jc->GetMonth(newDate); jcal.day = jc->GetDayOfMonth(newDate); } VC_DLL_EXPORTS void __cdecl getJapaneseLunisolarCalendar(int year, int month, int day, JCAL &jcal) { DateTime newDate = DateTime::DateTime(year, month, day); JapaneseLunisolarCalendar^ jlc = gcnew JapaneseLunisolarCalendar(); jcal.era = jlc->GetEra(newDate); jcal.year = jlc->GetYear(newDate); jcal.month = jlc->GetMonth(newDate); jcal.day = jlc->GetDayOfMonth(newDate); jcal.leapMonth = 0; //閏月を取得 if ( jcal.year > 0 ){ int leapMonth = jlc->GetLeapMonth(jcal.year, jcal.era); if ( jcal.month == leapMonth ){ jcal.leapMonth = 1; } //閏月含む場合の月を補正 if ( (leapMonth > 0) && (jcal.month >= leapMonth) ){ jcal.month = jcal.month - 1; //旧暦月の補正 } } // 干支(天干、地支) jcal.zodiac = jlc->GetSexagenaryYear(newDate); jcal.kanshi = jlc->GetCelestialStem(jcal.zodiac); jcal.chishi = jlc->GetTerrestrialBranch(jcal.zodiac); // 六曜(大安・赤口・先勝・友引・先負・仏滅) // (月 + 日) % 6 の余り jcal.rokuyo = (jcal.month + jcal.day) % 6; }次にD言語で
C++/CLI
ラッピングを呼び出し、旧暦、和暦を表示するプログラムの実装例です。
取得したい情報をstructで定義、メモリを確保しC++/CLI
に渡しています。
pragma(lib, "JCalDll")
とextern (Windows)~
を書くだけで、C++/CLI
側の関数を呼び出せるので、思っていたより簡単に実装できました。oldcal.dimport std.algorithm; import std.conv; import std.datetime; import std.format; import std.range; import std.stdio; import core.sys.windows.windows; struct JCAL { int era; int year; int month; int day; int leapMonth; int zodiac; int kanshi; int chishi; int rokuyo; } pragma(lib, "JCalDll"); extern (Windows) nothrow @nogc { void getJapaneseCalendar(int, int, int, ref JCAL); void getJapaneseLunisolarCalendar(int, int, int, ref JCAL); } string[] sEra = [ "", "明治", "大正", "昭和", "平成", "令和" ]; string[] sKanshi = [ "", "甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸" ]; string[] sChishi = [ "", "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥" ]; string[] sRokuyo = [ "大", "赤", "勝", "友", "負", "仏" ]; // [ "大安", "赤口", "先勝", "友引", "先負", "仏滅" ]; void main(string[] args) { Date dt = Clock.currTime().to!Date; if ( args.length > 2 ){ dt = Date(args[1].to!int, args[2].to!int, 1); } else { dt = Date(dt.year, dt.month, 1); } writef("\n%4d年 %2d月", dt.year, dt.month); JCAL jcal; string line1 = " |".cycle.take(dt.dayOfWeek * 6).to!string; string line2 = " |".cycle.take(dt.dayOfWeek * 7).to!string; with ( jcal ){ getJapaneseCalendar(dt.year, dt.month, 1, jcal); writef("(%s %2d年) ", sEra[era], year); getJapaneseLunisolarCalendar(dt.year, dt.month, 1, jcal); writefln("[%s%s]", sKanshi[kanshi], sChishi[chishi]); writefln("%-( %s |%)", ["日", "月", "火", "水", "木", "金", "土" ]); for ( int d = 1; d <= dt.daysInMonth; d++ ){ getJapaneseLunisolarCalendar(dt.year, dt.month, d, jcal); line1 ~= format("%2d %s |", d, sRokuyo[rokuyo]); line2 ~= format("%2d/%2d%s|", month, day,(leapMonth == 1) ? "*" : " "); } } int num = (cast(int)line2.length / (7 * 7)) + 1; string line0 = "-".cycle.take(num * 7 * 7).to!string; roundRobin(line0.chunks(7 * 7), line1.chunks(6 * 7), line2.chunks(7 * 7)) .each!(s => writefln("%s", s)); }D言語ソースコードの補足説明
実行時にパラメータなしだと今日の年月で処理します。
args
パラメータで年月を指定することも可能です。
string line1
には、全角文字が各日付ごとに1文字含まれます。このため1日の表示幅が6
文字となります。
新暦の日付で2文字、半角スペース1文字、六曜表示または全角スペースで1文字、半角スペース1文字、|
の1文字の順で合計6
文字です。
string line2
はすべて半角で、1日の表示幅が7
文字となります。
旧暦の年月で5文字、半角スペースまたはうるう月は*
で1文字、|
の1文字の順で合計7
文字です。
string line0
は、横線-
です。
line0 line1 line2
を7日ごとに区切って改行するためにchunks
、交互に表示するためにroundRobin
を使います。
chunks
roundRobinコンパイル
VS2019用 x64 Native Tools コマンドプロンプトを起動します。
C++/CLI
コンパイル時に、オプション/clr
と/LD
を付けます。JCalDll.lib
とJCalDll.dll
が生成されます。
JCalDll.lib
は、D言語ソースのコンパイル時に必要です。JCalDll.dll
は、oldcal.exe
実行時に必要です。
dmd
でのコンパイル時は、64bitコード生成のためにオプション-m64
をつけます。
ldc2
をインストールしていれば、ldc2 oldcal.d
でもOKです。コンパイルd:\Dev>cl /clr /LD JCalDll.cpp Microsoft(R) C/C++ Optimizing Compiler Version 19.24.28314 Microsoft (R) .NET Framework の場合 バージョン 4.08.4220.0 Copyright (C) Microsoft Corporation. All rights reserved. JCalDll.cpp Microsoft (R) Incremental Linker Version 14.24.28314.0 Copyright (C) Microsoft Corporation. All rights reserved. /out:JCalDll.dll /dll /implib:JCalDll.lib JCalDll.obj ライブラリ JCalDll.lib とオブジェクト JCalDll.exp を作成中 d:\Dev>dmd -m64 oldcal.d実行結果
表示される日本語の文字コードは
UTF-8
なので、先にchcp 65001
を実行してください。
Shift-JIS
で日本語出力したい場合は、以前紹介した通り、文字コード変換処理をソースコードに加える必要があります。
紹介情報1
紹介情報2私の環境のコマンドプロンプトでは、Cicaフォントを使っています。
このため、実行結果では全角スペースが四角の枠で表示されています。これで、自分の誕生月の旧暦カレンダーも見られそうです。
ただし、JapaneseLunisolarCalendarクラスのGetLeapMonth
の仕様で、西暦1960年1月27(旧暦の1959年)以前のうるう月を取得しようとするとException
が発生するため、西暦1960年2月以降の旧暦カレンダーしか表示できません。上限は西暦2049年12月です。
今回使用した参考情報
JapaneseCalendarクラス
JapaneseLunisolarCalendarクラス
オブジェクト演算子 (^) へのハンドル (C++/CLI および C++/CX)
ref new、gcnew (C++/CLI および C++/CX)C#のメソッドをC++から呼ぶ方法
C#をC++/CLIでラップしてC++アプリから呼ぶ六曜・月齢・旧暦カレンダー
年・月・日の干支
六曜はどのように決まるのか?六曜の計算方法は?
c#で六曜の計算今回使用していない情報だけど参考になりそうな情報
C++/CLIラッピング入門
C++のモジュールからC#のDLLを呼び出してみる
C++からC# DLLを直接利用する方法
- 投稿日:2020-09-26T20:11:43+09:00
C#でMySQLを使う - 3.INSERT追加・UPDATE更新・DELETE削除してみる
前提と準備
C#の記事
- C#でMySQLを使う - 1.開発環境インストール
- C#でMySQLを使う - 2.SELECT・画面表示
- C#でMySQLを使う - 3.追加更新と削除【この記事】
前回はVisual Studio 2019にMySQL Connectors .NETを用いて、とりあえずMySQLに接続して、SELECTを用いてデータの内容を画面に表示しました。今回はDBのデータそのものを変更を施します(˶ ・ᴗ・ )੭
環境
- OS:Windows 10 Pro
- 開発環境:Visual Studio 2019 (MySQL Connectors .NET利用)
- データベース:MySQL 5.7
MySQLサーバー
テスト用ユーザー:test (パスワード:test1)
データベース:manutest
テーブル名:testtb
id name memo 型 INT VARCHAR(64) VARCHAR(256) 必須属性 PRIMARY KEY NOT NULL 初期値NULL その他属性 AUTO_INCREMENT - - 前提
前回のように、Visual Studio 2019とMySQLをインストールし、かつMySQL Connectors .NETがインストールされていること(Visual Studioは自動でMySQL Connectorsのライブラリを認識してくれる)
作業手順
Visual Studio 2019でのコーディング
コンポーネント・コントロールの配置
今回はボタンを押下→MySQLでデータを拾ってくる→画面に表示
これに加えて、名前とメモを入力し、追加や更新などができるようにフォームを追加
右上のリストボックスを選択すると該当するIDが画面表示されるので、選択している際に該当IDの更新や削除ができるようになりますが、細かいUI制御までは触れませんWindowsのC#フォームのプロジェクトを新規作成し、Form1.csデフォルトで開きました
- 左上:データグリッド dataGridView1
- 右上:リストボックス2つ listBox1・listBox2
- 選択するとテキストボックス「idNum」に該当するIDを表示する
- 左下:SQLを読み込むボタン button1
- 右中央:名前入力 textBoxName・メモ入力 textBoxMemo
- 右下:選択中のID idNum・新規追加SQL実行 buttonAdd・更新SQL実行 buttonEdit・削除SQL実行 buttonDel
これらを貼り付けます。
データグリッドとリストボックスはSQLのデータを表示させるために配置しましたMySQL Connector .NETの参照を追加
MySQLインストール時と同じように、参照を追加していきます。
「アセンブリ」の中に実際自動で認識してくれるので、参照マネージャーで、右上の検索画面に「mysql」を入力すると、↑の画面のように「MySql.Data」が何行も出てくるので、とりあえず1つだけ、どれでもいいので選択して(行左にマウスを当てると出てくるチェックボックスをON)「OK」を選択すると…
「MySql.Data」が追加されました(*˘꒳˘*)
この中にMySQLを扱うC#オブジェクトが入っているのです。ボタンクリック時のソースを記述する
今回も簡単のため、SQLの実行はボタンクリックで読み込むだけで、Form1.csのクリックイベント動作のみをコーディングしました。本来は規模が大きくなることがほとんどなので、formのボタンクリックではなく、独立したクラスに分けるケースがほとんどですが。。。
Form1.cs サンプルコード
Form1.csusing System; using System.Collections.Generic; using System.Data; using System.Windows.Forms; using MySql.Data.MySqlClient; namespace MySqlFormsTest { public partial class Form1 : Form { // 取得したデータのID一覧(フォーム内部変数) private List<int> idNums; // 選択中のID private int selId; // MySQL接続情報 private string connStr = "server=127.0.0.1;user id=test;password=test1;database=manutest"; public Form1() { InitializeComponent(); } /** * 接続ボタン(MySQLサーバーに接続し、一覧データを取得する) * */ private void button1_Click(object sender, EventArgs e) { MySqlConnection conn = new MySqlConnection(this.connStr); // 画面と内部変数を初期化 listBox1.Items.Clear(); listBox2.Items.Clear(); this.idNums = new List<int>(); selId = -1; idNum.Text = ""; buttonEdit.Enabled = false; buttonDel.Enabled = false; try { // 接続を開く conn.Open(); // データを取得するテーブル DataTable tbl = new DataTable(); // SQLを実行する MySqlDataAdapter dataAdp = new MySqlDataAdapter("SELECT id, name, memo FROM testtb", conn); dataAdp.Fill(tbl); // dataGridViewに表示させる dataGridView1.DataSource = tbl; // 実行結果を1行ずつ参照する場合 for (int i = 0; i < tbl.Rows.Count; i++) { DataRow row = tbl.Rows[i]; // データ行 // 右のリストボックスにアイテムを追加 listBox1.Items.Add(row[1]); listBox2.Items.Add(row[2]); // 同時にIDを内部変数に追加 this.idNums.Add((int)row[0]); } // 接続を閉じる conn.Close(); } catch (MySqlException mse) { MessageBox.Show(mse.Message, "データ取得エラー", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /** * リストボックスの行を選択 */ private void listBox1_SelectedIndexChanged(object sender, EventArgs e) { this.listBox_SelectedIndexChanged(sender, e); // 選択したIDを画面表示 if(listBox1.SelectedIndex != -1) { this.selId = idNums[listBox1.SelectedIndex]; idNum.Text = idNums[listBox1.SelectedIndex].ToString(); } else { this.selId = -1; idNum.Text = ""; } } private void listBox2_SelectedIndexChanged(object sender, EventArgs e) { this.listBox_SelectedIndexChanged(sender, e); // 選択したIDを画面表示 if (listBox2.SelectedIndex != -1) { this.selId = idNums[listBox2.SelectedIndex]; idNum.Text = idNums[listBox2.SelectedIndex].ToString(); } else { this.selId = -1; idNum.Text = ""; } } private void listBox_SelectedIndexChanged(object sender, EventArgs e) { // どちらかを選択していないと追加と削除が使えない if(listBox1.SelectedIndex != -1 || listBox2.SelectedIndex != -1) { buttonEdit.Enabled = true; buttonDel.Enabled = true; } else { buttonEdit.Enabled = false; buttonDel.Enabled = false; } } /** * 新規追加ボタンを選択 */ private void buttonAdd_Click(object sender, EventArgs e) { MySqlConnection conn = new MySqlConnection(this.connStr); MySqlTransaction trans = null; // 実行トランザクション // 新規追加のSQLコマンド string sqlCmd = @"INSERT INTO testtb (name, memo) VALUES (@name, @memo)"; // 追加クエリの開始 MySqlCommand cmd = new MySqlCommand(sqlCmd, conn); try { // ステークホルダーのセット cmd.Parameters.AddWithValue("name", textBoxName.Text); cmd.Parameters.AddWithValue("memo", textBoxMemo.Text); cmd.Connection.Open(); // 接続を開く // トランザクション監視開始 trans = cmd.Connection.BeginTransaction(IsolationLevel.ReadCommitted); // SQL実行 cmd.ExecuteNonQuery(); // DBをコミット trans.Commit(); } catch (MySqlException mse) { trans.Rollback(); // 例外発生時はロールバック MessageBox.Show(mse.Message, "データ追加エラー", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { // 接続はクローズする cmd.Connection.Close(); } } /** * 編集ボタンを選択 */ private void buttonEdit_Click(object sender, EventArgs e) { MySqlConnection conn = new MySqlConnection(this.connStr); MySqlTransaction trans = null; // 実行トランザクション // 編集のSQLコマンド string sqlCmd = @"UPDATE testtb SET name = @name, memo = @memo WHERE id = @id"; // 編集クエリの開始 MySqlCommand cmd = new MySqlCommand(sqlCmd, conn); try { // 選択中のIDを用いて、ステークホルダーのセット cmd.Parameters.AddWithValue("id", this.selId); cmd.Parameters.AddWithValue("name", textBoxName.Text); cmd.Parameters.AddWithValue("memo", textBoxMemo.Text); cmd.Connection.Open(); // 接続を開く // トランザクション監視開始 trans = cmd.Connection.BeginTransaction(IsolationLevel.ReadCommitted); // SQL実行 cmd.ExecuteNonQuery(); // DBをコミット trans.Commit(); } catch (MySqlException mse) { trans.Rollback(); // 例外発生時はロールバック MessageBox.Show(mse.Message, "データ更新エラー", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { // 接続はクローズする cmd.Connection.Close(); } } /** * 削除ボタンを選択 */ private void buttonDel_Click(object sender, EventArgs e) { MySqlConnection conn = new MySqlConnection(this.connStr); MySqlTransaction trans = null; // 実行トランザクション // 削除のSQLコマンド string sqlCmd = @"DELETE FROM testtb WHERE id = @id"; // 削除クエリの開始 MySqlCommand cmd = new MySqlCommand(sqlCmd, conn); try { // 選択中のIDを用いて、ステークホルダーのセット cmd.Parameters.AddWithValue("id", this.selId); cmd.Connection.Open(); // 接続を開く // トランザクション監視開始 trans = cmd.Connection.BeginTransaction(IsolationLevel.ReadCommitted); // SQL実行 cmd.ExecuteNonQuery(); // DBをコミット trans.Commit(); } catch (MySqlException mse) { trans.Rollback(); // 例外発生時はロールバック MessageBox.Show(mse.Message, "データ削除エラー", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { // 接続はクローズする cmd.Connection.Close(); } } } }まずフォームのC#ソースの内部変数として、MySQLのデータでどのIDを取得したかと、右上のリストボックスの項目に対応するIDはどれを選択しているのかを把握するため、Form1にprivate変数を追加しました
// 取得したデータのID一覧(フォーム内部変数) private List<int> idNums; // 選択中のID private int selId;読み込むSQLを実行するボタンで「SELECT id, name, memo~」をbutton1_Click()内で実行していると思いますが、単に右上のリストボックスにピックアップするだけでなく、対応するIDを内部変数idNumsで受け持つコードも実装しました
// 実行結果を1行ずつ参照する場合 for (int i = 0; i < tbl.Rows.Count; i++) { DataRow row = tbl.Rows[i]; // データ行 // 右のリストボックスにアイテムを追加 listBox1.Items.Add(row[1]); listBox2.Items.Add(row[2]); // 同時にIDを内部変数に追加 this.idNums.Add((int)row[0]); }row[0]が取得したデータのID番号ですが、そのままListにAdd()を実行するとObjectを暗黙的に変換できないエラーとなるため、(int)でキャストしました。
listBox1.SelectedIndex listBox2.SelectedIndexlistBox1_SelectedIndexChanged()などで使っていますが、リストボックスの選択したインデックス番号(0で始まるもの)はこれを使っています(未選択の場合は-1)。選択するとidNumの選択中IDのフィールドに表示する仕組みを入れたんです。選択中のIDは内部変数に入れています。
this.selId = idNums[listBox1.SelectedIndex];INSERTやUPDATEなどDBの内容を更新するSQLは、MySqlCommandを使ったんですが、こっちのほうがプレースホルダーが使いやすいので、SELECT以外はMySqlCommandを使いました。
string sqlCmd = @"INSERT INTO testtb (name, memo) VALUES (@name, @memo)"; MySqlCommand cmd = new MySqlCommand(sqlCmd, conn);そして更新の際はトランザクションを使っています。例外が発生したときはいつもロールバックするようにしていますが、正直タイミングは考えるのは大変なので、今回は簡単で例外キャッチのみで;;
try { trans = cmd.Connection.BeginTransaction(IsolationLevel.ReadCommitted); cmd.ExecuteNonQuery(); trans.Commit(); } catch (MySqlException mse) { trans.Rollback(); // 例外発生時はロールバック }実行結果
今回は追加などのボタンに再読み込み機能がないので、追加などのボタンを押した後は「読み込み」ボタンを押して再表示させています(面倒ですが…)
最初に下の画像のようなデータがあるとして、IDが6のデータを更新すると…
「更新」→「読み込み」で、更新に成功しました(˶ ・ᴗ・ )੭次はIDが5の「Testing」というデータを選択して消してみます
再度読み込みボタンを押して、これも削除成功しました!!最後に追加してみます
見事成功しました(˶ ・ᴗ・ )੭!!次回
Visual StudioでMySQLのデータがNULLの場合にハマったことがあるので、そっちについて追ってみます
参考文献
関連文献
- C#のMySQLでトランザクション使用する / C#でのトランザクションの実装について
- Symfoware Server アプリケーション開発ガイド(埋込みSQL編) - FUJITSU - / SQLのトランザクションとは何かの理解
- 【MySQL 5.1向けのもの】MySqlTransaction の使用 / C#でのトランザクション実装例
- IsolationLevel 列挙型 / C#でのトランザクションレベル - .NET API
- 投稿日:2020-09-26T18:12:35+09:00
Azure Static Web Apps + Communication Services + Blazor + Functions してみた
とりまやってみましょう。
Azure Communication Services は Azure 上に必須なので作ります。Token の払い出しを行うサーバーサイドの作成
これは Functions で行います。こちらにある通り。
Azure Functions を使用して、信頼できる認証サービスを構築する
今回は以下のような感じでいこうかなと思います。URL のクエリに id という名前のパラメーターがあるときは、それをユーザー ID としてトークンを取得。id 指定がない場合はユーザーを作成してトークンを取得といった感じにします。
id は、適当な値を設定しても Azure Communication Services ではじかれるので、初回は id 無しで呼び出して 2 回目以降は id 指定で行く感じです。戻ってくる JSON はこんな感じで
戻り値{ "token": "xxxxx", "expiresOn": "2020-09-27T04:36:01.1941722+00:00", "userId": "xxxxxxxxxx" }Azure Functions のプロジェクトを作って
local.settings.json
に Azure Communication Services の接続文字列を追加します。local.settings.json{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "UseDevelopmentStorage=true", "FUNCTIONS_WORKER_RUNTIME": "dotnet", "AzureCommunicationServices": "endpoint=https://xxxx.communication.azure.com/;accesskey=xxxxxxxxx" } }
Microsoft.Azure.Functions.Extensions
とAzure.Communication.Administration
(preview) パッケージを追加して以下のようにStartup.cs
を作ります。Startup.csusing Azure.Communication.Administration; using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using SimpleChat.Server; using System; [assembly: FunctionsStartup(typeof(Startup))] namespace SimpleChat.Server { public class Startup : FunctionsStartup { public override void Configure(IFunctionsHostBuilder builder) { builder.Services.AddTransient(provider => { var c = provider.GetRequiredService<IConfiguration>(); return new CommunicationIdentityClient(c.GetValue<string>("AzureCommunicationServices")); }); } } }では、Token を作成する関数を作ります。HttpTrigger の関数で CommunicationIdentityClient を使ってトークンを生成して返します。まずは、戻り値のクラスを定義して…
GetTokenResponse.csusing System; namespace SimpleChat.Core { public class GetTokenResponse { public string Token { get; set; } public DateTimeOffset ExpiresOn { get; set; } public string UserId { get; set; } } }関数本体を実装します。
GetToken.csusing System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Azure.Communication.Administration; using SimpleChat.Core; using Azure.Communication; namespace SimpleChat.Server { public class GetToken { private readonly CommunicationIdentityClient _communicationIdentityClient; public GetToken(CommunicationIdentityClient communicationIdentityClient) { _communicationIdentityClient = communicationIdentityClient ?? throw new ArgumentNullException(nameof(communicationIdentityClient)); } [FunctionName(nameof(GetToken))] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req, ILogger log) { var id = req.Query["id"].ToString(); var user = string.IsNullOrWhiteSpace(id) ? (await _communicationIdentityClient.CreateUserAsync()).Value : new CommunicationUser(id); try { var tokenResponse = await _communicationIdentityClient.IssueTokenAsync(user, new[] { CommunicationTokenScope.Chat }); return new OkObjectResult(new GetTokenResponse { UserId = tokenResponse.Value.User.Id, Token = tokenResponse.Value.Token, ExpiresOn = tokenResponse.Value.ExpiresOn, }); } catch (Exception ex) { log.LogError(ex, "IssureTokenAsync"); return new BadRequestResult(); } } } }次に人が集まって会話するスレッドを作成する関数を追加します。このスレッド ID が一緒じゃないと一人チャットになるので、スレッド ID は皆で共有できないといけません。
実は先ほど作ったユーザーの ID も、システムで管理してる(もしくは外部の ID プロバイダーの)ユーザーとの紐づけの管理とかは自分で作りこまないといけません。今回はユーザー管理は毎回ユーザーを作るという手抜き実装ですがスレッド ID は共有しないとみんなでチャット出来ないので頑張って作ります。ただ、 Cosmos DB とかに保存する処理とかを作るのはだるいのでメモリ上に保持するようにします。なので、Azure Functions がスケール アウトやスケール インすると不都合が起きますが、ちゃんと DB を使えばうまくいくはずです。
今回はグローバルで 1 つのスレッドという感じになるべくなるように実装していきます。
Server のプロジェクトに
Azure.Communication.Chat
パッケージを追加して GetToken メソッドでスレッドが無ければ作るように変更します。
local.settings.json
にスレッド ID を作るために必要なエンドポイントの設定も追加します。local.settings.json{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "UseDevelopmentStorage=true", "FUNCTIONS_WORKER_RUNTIME": "dotnet", "AzureCommunicationServices": "endpoint=https://xxxxxx.communication.azure.com/;accesskey=xxxxx...", "AzureCommunicationServicesEndpoint": "https://xxxxxx.communication.azure.com/" } }そして、GetToken.cs にスレッドも作る処理を追加しておきます。
GetToken.csusing System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Azure.Communication.Administration; using SimpleChat.Core; using Azure.Communication; using Azure.Communication.Chat; using Microsoft.Extensions.Configuration; using Azure.Communication.Identity; namespace SimpleChat.Server { public class GetToken { // 危険!こんなことは本番でやったらだめ!!絶対!!だけど、とりあえずのお試しなので static 変数にスレッドクライアントを保持します private static ChatThreadClient ChatThreadClient { get; set; } // ここにエンドポイントの値を構成情報から設定する private readonly string _endpoint; private readonly CommunicationIdentityClient _communicationIdentityClient; public GetToken(CommunicationIdentityClient communicationIdentityClient, IConfiguration configuration) { _communicationIdentityClient = communicationIdentityClient ?? throw new ArgumentNullException(nameof(communicationIdentityClient)); _endpoint = configuration.GetValue<string>("AzureCommunicationServicesEndpoint"); } [FunctionName(nameof(GetToken))] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req, ILogger log) { var id = req.Query["id"].ToString(); var user = string.IsNullOrWhiteSpace(id) ? (await _communicationIdentityClient.CreateUserAsync()).Value : new CommunicationUser(id); try { var tokenResponse = await _communicationIdentityClient.IssueTokenAsync(user, new[] { CommunicationTokenScope.Chat }); // スレッド ID がなかったら作る。同時アクセスがあると死ぬロジックなので本番でマネしないでね! var chat = new ChatClient( new Uri(_endpoint), new CommunicationUserCredential(tokenResponse.Value.Token)); if (ChatThreadClient == null) { // Thread が無い場合 ChatThreadClient = await chat.CreateChatThreadAsync( "talk", new[] { new ChatThreadMember(user) }); } else { // Thread がある場合は参加しておく await ChatThreadClient.AddMembersAsync(new[] { new ChatThreadMember(user) }); } return new OkObjectResult(new GetTokenResponse { UserId = tokenResponse.Value.User.Id, Token = tokenResponse.Value.Token, ExpiresOn = tokenResponse.Value.ExpiresOn, ThreadId = ChatThreadClient.Id, // ThreadId も返す }); } catch (Exception ex) { log.LogError(ex, "IssureTokenAsync"); return new BadRequestResult(); } } } }こうすると GetToken 関数の結果に
threadId
が追加されます。GetTokenの結果{ "token": "xxx.....", "expiresOn": "2020-09-27T06:04:49.1221965+00:00", "userId": "xxxxx...", "threadId": "xxxx...." }最後にローカルデバッグ時用の CORS の設定を追加します。
local.settings.json
に CORS の設定を以下のように追加しておきます。local.settings.json{ "IsEncrypted": false, "Values": { ... 省略 ... }, "Host": { "CORS": "*" } }クライアント側の実装
次にクライアント側です。Blazor のプロジェクトを作りましょう。そして
Azure.Communication.Chat
(preview) パッケージを追加します。とりあえずローカル実行時は、Azure Functions のローカルサーバーを見に行くようにします。Program.cs を以下のようにします。
Program.csusing Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.DependencyInjection; using System; using System.Net.Http; using System.Threading.Tasks; namespace SimpleChat.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("app"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration["BaseAddress"] ?? builder.HostEnvironment.BaseAddress) }); await builder.Build().RunAsync(); } } }wwwroot フォルダーの下に
appsettings.Development.json
という名前で以下のファイルを置いたら構成は完了です。appsettings.Development.json{ "BaseAddress": "http://localhost:7071/" }チャットにつなごう
ではつないでいきます。繋ぐためにはエンドポイントが必要なのでクライアントの構成ファイルに書いておくようにします。
appsettings.Development.json
に以下のように設定を追加します。appsettings.Development.json{ "BaseAddress": "http://localhost:7071/", "CommunicationServiceSettings": "https://xxxxxx.communication.azure.com/" }追加した設定を保持するためのクラスを定義します。
CommunicationServiceSettings.csnamespace SimpleChat.Client { public class CommunicationServiceSettings { public string Endpoint { get; set; } } }そして
Program.cs
に、設定をクラスに読み込んでおく処理を追加します。Program.csusing Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using SimpleChat.Client.Services; using System; using System.Diagnostics; using System.Net.Http; using System.Threading.Tasks; namespace SimpleChat.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("app"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration["BaseAddress"] ?? builder.HostEnvironment.BaseAddress) }); // 設定を読み込み builder.Services.AddSingleton(sp => { // Configure メソッド使いたかったけど何回試しても動かなかったので泣く泣く… var c = sp.GetRequiredService<IConfiguration>(); return new CommunicationServiceSettings { Endpoint = c[nameof(CommunicationServiceSettings)] }; }); builder.Services.AddScoped<CommunicationService>(); await builder.Build().RunAsync(); } } }そして、チャットのやり取りをするクラスを定義します。ここらへんは ChatClient や ChatThreadClient を使ってサクッと作れます。
CommunicationService.csusing Azure.Communication.Chat; using Azure.Communication.Identity; using Microsoft.Extensions.Options; using SimpleChat.Core; using System; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; namespace SimpleChat.Client.Services { public class CommunicationService { private static string ThreadId => "hello-world-sample"; private readonly CommunicationServiceSettings _communicationServiceSettings; private readonly HttpClient _http; private GetTokenResponse _getTokenResponse; private ChatClient _chatClient; private ChatThreadClient _chatThreadClient; public CommunicationService(IOptions<CommunicationServiceSettings> settings, HttpClient http) { _communicationServiceSettings = settings.Value; _http = http; } public bool IsJoined => _chatThreadClient != null; // チャットに参加 public async ValueTask JoinToChatAsync() { var res = await _http.GetStringAsync("/api/GetToken"); _getTokenResponse = JsonSerializer.Deserialize<GetTokenResponse>(res); _chatClient = new ChatClient( new Uri(_communicationServiceSettings.Endpoint), new CommunicationUserCredential(_getTokenResponse.Token)); _chatThreadClient = _chatClient.GetChatThreadClient(_getTokenResponse.ThreadId); } // チャットにメッセージを送信 public async ValueTask SendMessageAsync(string name, string message) { await _chatThreadClient.SendMessageAsync(message, senderDisplayName: name); } // チャットのメッセージを取得 public IAsyncEnumerable<ChatMessage> GetMessagesAsync() => _chatThreadClient.GetMessagesAsync(); } }
Program.cs
にコンテナに登録する処理も追加しておきましょう。Program.csbuilder.Services.AddTransient<CommunicationService>();仕上げに
Index.razor
です。async foreach
で脳死で読み込めばいい感じにメッセージがくるたびに読み込んでくれるかと思ったら、ダメだったので泣く泣くメッセージの重複管理を自前でやってます。これチャットが長くなると毎回データとってくるのつらいのでは…。そのうち API がいい感じになるのかな??Index.razor@page "/" @using SimpleChat.Client.Services @using System.Diagnostics @using Azure.Communication.Chat @inject CommunicationService communicationService @inject Microsoft.Extensions.Configuration.IConfiguration configuration; <h1>Simple chat</h1> @if (communicationService.IsJoined) { <div> ユーザー名:@displayName </div> <div> <input type="text" @bind-value="message" /> <button @onclick="SendButton_Click" disabled="@(string.IsNullOrEmpty(message))">Send</button> <hr /> <ul> @foreach (var message in messages) { <li @key="message.Id">@message.SenderDisplayName : @message.Content</li> } </ul> </div> } else { <button @onclick="JoinButton_Click" disabled="@(string.IsNullOrEmpty(displayName))">Join</button> <input type="text" @bind-value="displayName" /> } @code { private readonly HashSet<string> messageIds = new HashSet<string>(); private readonly List<ChatMessage> messages = new List<ChatMessage>(); private string displayName; private string message; private async void JoinButton_Click() { await communicationService.JoinToChatAsync(); StateHasChanged(); _ = StartReadMessagesAsync(); } private async ValueTask StartReadMessagesAsync() { while (true) { await foreach (var message in communicationService.GetMessagesAsync()) { Debug.WriteLine($"{message.Type}: {message.SenderDisplayName} > {message.Content}"); if (message.Type == "Text") { Debug.WriteLine("Text!!"); if (!messageIds.Contains(message.Id)) { messages.Add(message); messageIds.Add(message.Id); } } } StateHasChanged(); await Task.Delay(3000); } } private async void SendButton_Click() { await communicationService.SendMessageAsync(displayName, message); message = ""; StateHasChanged(); } }ローカルで動かしてみましょう。起動直後
名前を入力したところ(複数ブラウザで試してみてる)
メッセージうってみた
Static Web Apps へデプロイ
GitHub にリポジトリを作ってお約束の手順でデプロイしてみました。今回はこんな感じのレイアウトなので
それにあわせて、こんな感じで作りました
デプロイが完了したら構成に Azure Communication Services 関連の設定を追加しておきます。
クライアント側にも
appsettings.Development.json
しかなかったのでappsettings.json
を足しておきます。Azure Communication Services 関連の設定だけ引っ越ししましょう。以下のようにappsettings.json{ "CommunicationServiceSettings": "https://xxxxxxx.communication.azure.com/" }Static Web Apps にデプロイしてもとりあえずは動きました。
まとめ
ということで Azure Communication Services で一番とっつきやすいチャット機能を Blazor + Functions で試してみました。デプロイ先は Static Web Apps です。
試してみた感じ、Azure Communication Services は ID は発行してくれるけど、それが実際に誰なのかということは自分で管理しないといけないなという感じでした。あと、ちゃんと作るときは Azure AD や Azure AD B2C か、それに準ずる何かしらでログイン機能も実装しておかないとフリーダムなチャットになりそうだなと思いました。
後は誰がどんなスレッドを持ってるのかとかも、おそらくアプリ側で管理することになる気がします。あくまで Communication Services が提供するのは作成と ID 指定での検索だけっぽいです。
なのでちゃんと作るとしたら最低でも以下のような雰囲気になる気がします。仕様なのかバグなのかわかりませんが C# SDK は今のところポーリングしてメッセージとってくるのですが、毎回メッセージが全部返ってくるので、誰か差分だけいい感じにとってくる方法を教えてください。
startTime 引数と AsPages で continueousToken を指定しても、全部返ってきました orz
音声チャットも試してみたいな。
- 投稿日:2020-09-26T17:24:45+09:00
【C#】Reflectionでジェネリックを扱う方法
はじめに
C#でReflectionを使ったことがありますか?
Reflectionを使うことで今まではできなかったような処理を書くことができます
その一つとしてgenericのクラスの呼び出し方法を今回はご紹介します。Typeを取得する
普通に取得する方法
呼び出したいインスタンス.GetType();Type.GetType("genericの型`引数の型の数[[型],[型].....]"):typeof(genericの型<型>);注意点としては文字列で<>ではなく[]にする必要があり、ジェネリックの数が`の後に必要です
通常ライブラリを使うだけならなにも問題はありません
しかし、動的コード生成する場合や動的にDLLを読み込むときに使用する場合は問題が発生します。動的な読み込みの際に取得する方法
動的にDLL等を読み込むときは上記の方法ではできません
なので別な方法でする必要があります。
これは他のクラスを取得する方法と同じだと思っていました
しかしそれではダメだったのでご紹介しますダメだった方法
呼び出したいアセンブリ型のインスタンス.GetType("genericの型`引数の型の数[[型],[型].....]"):この方法では他の型を読み込んでくれないためだめでした。
つまずいた方法
調べているとMakeGenericTypeメソッドでgenericの型の引数を任意に決めれると書いてあったのでやってみました。
Type t = 呼び出したいアセンブリ型のインスタンス.GetType("genericの型`引数の型の数"): //ジェネリック自体の型を取得 t.MakeGenericType(指定したい型のType);しかし問題がこれを書いてブレークポイントで確認してみましたが
tの中は変わっていませんでした。実はMakeGenericTypeは適応したgeneric型のtypeを新しく作るメソッドでした
つまり...
正しい方法
Type t1 = 呼び出したいアセンブリ型のインスタンス.GetType("genericの型`引数の型の数"): //ジェネリック自体の型を取得 Type t2 = t.MakeGenericType(指定したい型のType);このようにすることで動的に読み込んだgeneric型のTypeを取得することができます。
MakeGenericTypeメソッドは新しく作ったType型を戻り値として出すメソッドですので
ちゃんと代入しないとだめですよねまとめ
まとめとしてはちゃんとメソッドの仕様を調べて使うことにしましょう。
でないと、私みたいに痛い目を見ます。
皆様お気を付けてこの辺りで失礼します。
- 投稿日:2020-09-26T16:02:12+09:00
[ASP.NET Core] HTMLヘルパーを使ってリスト形式の入力画面を作るときの注意
HTMLヘルパーを入力画面かつList型のクラスで使うときの注意
ASP.NET Core (MVC)でHTMLヘルパーを使って、テーブルタグで複数行入力し、List型のクラスでポストデータを受け取る画面を作ろうとしたら、入力データがポストされずハマったのでメモ
結論はforeachじゃなくて普通のforでやればOK
NG
foreachだとpostされてこないよ
@foreach (var row in Model.Rows) { <tr> <td>@Html.EditorFor(m => row.ColA)</td> <td>@Html.EditorFor(m => row.ColB)</td> </tr> }OK
forだとOK
@for(var i = 0; i < Model.Rows.Count; i++) { <tr> <td>@Html.EditorFor(m => m.Rows[i].ColA)</td> <td>@Html.EditorFor(m => m.Rows[i].ColB)</td> </tr> }
- 投稿日:2020-09-26T13:03:25+09:00
[.NET][C#]Entity Framework Coreを使用してDBアクセス・ORマッピング
はじめに
急遽来月からC#でシステム立ち上げの案件に入ることになり、
最近の.NET周りの技術について勉強することに。以前関わっていたプロジェクトでは、
ASP.Net MVCで構築し、その際ORマッパーなどは使用していなかったが、
.NET Core勉強中にふとEntity Framework CoreというORマッパーが目についたので
触ってみたところ、とても良かったので記事にした次第です。今回は1つのテーブルからデータを取得する処理を
Entity Framework Coreを使用してやってみました。環境
言語:C# 8.0
FW:.NET Core 3.1.401
DB:MySQL 8.0.21
その他:Docker, Adminerなど事前準備
1. Entity Framework CoreのNuGetパッケージをインストール
EFCoreのパッケージをインストールします。
今回はDBにMySQLを使うので、.NETプロジェクト直下で以下コマンドを実行。dotnet add package MySql.Data.EntityFrameworkCore -v 8.0.21NuGetパッケージ名は以下サイトに記載してあります。
有名どころのDBは大体あると思う。
https://docs.microsoft.com/ja-jp/ef/core/providers/?tabs=dotnet-core-cli2. DB構築
今回は以下のような企業の株価を保持するテーブルを準備します。
とりあえず上記テーブルに2020/9/1時点の東証1部の会社の株価データレコードを挿入します。
本当は時価総額が良かったけどデータが見つからなかった。実装
1. Entityクラスの作成
テーブルのレコードを保持するいわゆるエンティティクラスを作ります。
public class CompanyData { [Key] public string StockCode {get; set;} public string CompanyName {get; set;} public string IndustryCode {get; set;} public int StockPrice {get; set;} }プロパティは、テーブルのカラム名・型を一致させる必要があります。
また、基本的にEntityクラスには主キーのプロパティが必要で、
これが無いとクエリ発行時に例外が投げられます。
主キーにあたるプロパティに[Key]注釈をつけるか、
プロパティ名をidにすると主キーとして認識されます。
(プロパティ名をidにする場合、テーブルのカラム名もidにする必要あり)どうしても主キーを付けられない場合は、クラスに[Keyless]注釈をつけるようです。
(振る舞いがどう変わるかは以下参考。)
https://docs.microsoft.com/ja-jp/ef/core/modeling/keyless-entity-types2. DbContext拡張クラスを作成
DbContextを継承した拡張DbContextクラスを作成します。
public class MyContext:DbContext { public DbSet<CompanyData> CompanyStockPrice{get;set;} //このプロパティ名がテーブル名と一致してないとダメ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseMySQL(@"server=localhost;database=hoge;userid=hoge;password=hoge;sslmode=none;"); }DbSet型のプロパティとDB接続文字列を設定するOnConfiguringをオーバーライドしたメソッドを実装します。
(実際のシステムでは、接続文字列は外部ファイルや環境変数に外出しした方が良いでしょう。)
このDbSet型のプロパティ名のテーブルを参照するため、
アクセスしたいテーブル名とプロパティ名を一致させる必要があります。これだけでORマッピングの実装は完了です。
昔はJavaでマッピング用の設定ファイルをガリガリ書いてたので
簡単すぎてびっくりしました。3. DBアクセスロジックの実装
最後にDBにSQLクエリを発行して結果を受け取って処理するクラスを実装します。
サンプルとして、指定した業種・株価上位件数の会社の株価を表示するコードを書いてみます。SQLクエリの構築、発行の実装方法としてLINQを使う方法とSQLクエリを書く方法があります。
3-1. LINQでSQLクエリ構築
public class CompanyStockPriceSearch { static void Main(string[] args) { Console.Write("業種コード入力:"); String target = Console.ReadLine(); Console.Write("上位何件?:"); int tops = int.Parse(Console.ReadLine()); Console.WriteLine("---------------------------"); using(var context = new MyContext()) { Console.WriteLine($"業種コード「{target}」の株価Top{tops}"); // post SQL query context.CompanyStockPrice .Where(com => com.IndustryCode.Equals(target)) .OrderByDescending(com => com.StockPrice) .Take(tops) .ToList() .ForEach(com => Console.Write($"[{com.StockCode}]{com.CompanyName} 株価={com.StockPrice}\n")); } } }上記のようにLINQでSQLクエリの構築と発行ができます。
なんと・・・これは快適・・・?
SQL書かなくてもコードアシストでサクサク問い合わせ内容を掛けるのは良いですね!実行結果は以下の通りです。
> dotnet run 業種コード入力:10 上位何件?:5 --------------------------- 業種コード「10」の株価Top5 [7974]任天堂 株価=58590 [9435]光通信 株価=25510 [4684]オービック 株価=18830 [9605]東映 株価=15760 [4661]オリエンタルランド 株価=142853-2. 生SQLクエリを書く
public class CompanyStockPriceSearch { static void Main(string[] args) { // ~~~~[中略]~~~~ using(var context = new MyContext()) { Console.WriteLine($"業種コード「{target}」の株価Top{tops}"); // post SQL query context.CompanyStockPrice .FromSqlRaw("SELECT * FROM CompanyStockPrice" + " WHERE IndustryCode = {0}" + " ORDER BY StockPrice DESC" + " LIMIT {1}" , target, tops) .ToList() .ForEach(com => Console.Write($"[{com.StockCode}]{com.CompanyName} 株価={com.StockPrice}\n")); } } }SQLを直接書く場合は上記のようにDbSet#FromSqlRawメソッドで指定します。
バインドパラメータを使う場合は{0},{1}のように指定して、
SQL文に続く引数に値をバインドパラメータ分だけ指定します。またSELECT句にカラムを指定する場合は、
必ずEntityクラスのすべてのプロパティのデータを返すように指定する必要があります。結果は下記のとおり、先ほどと同じです。
> dotnet run 業種コード入力:10 上位何件?:5 --------------------------- 業種コード「10」の株価Top5 [7974]任天堂 株価=58590 [9435]光通信 株価=25510 [4684]オービック 株価=18830 [9605]東映 株価=15760 [4661]オリエンタルランド 株価=142854. 補足
LINQで実装した場合、どんなSQLクエリが投げられてるんだろう??
まさかSELECT * FROM CompanyStockPrice;
で全件取ってきてないよな・・・?
と思ったのでMySQLのSQLログを有効にして発行されたSQLクエリをDBサーバ側で取ってみました。SELECT `c`.`StockCode`, `c`.`CompanyName`, `c`.`IndustryCode`, `c`.`StockPrice` FROM `CompanyStockPrice` AS `c` WHERE `c`.`IndustryCode` = '10' ORDER BY `c`.`StockPrice` DESC LIMIT 5おぉーLINQの処理内容がそのままSQLクエリに反映された形になってますね。
リテラルに指定の値がそのまま記載されてますが
バインドパラメータが埋め込まれたSQLクエリ発行したときでも上記のようにログに出るので
LINQ時もバインドパラメータ化されてるでしょう・・・きっと。まとめ
ORマッピングが設定ファイル無しでコードだけで済むのは本当に良いですね!
LINQでSQLクエリを構築する方法もやってみると実装が快適です。ただ、実際のシステムのSQLクエリはもっと複雑なものもあるので
LINQで書く場合と生SQLを書く場合とで使い分けが必要そうです。
また、どちらでやる場合も↑には記載していない注意点がいくつかあるので
公式ドキュメントに目を通しておいた方が良いと思います。参考
- 公式Entity Framework ドキュメント https://docs.microsoft.com/ja-jp/ef/
- 投稿日:2020-09-26T11:42:07+09:00
UnityでランダムMAP生成の方法【2Dゲーム】
UnityLearnで
用意されているローグライクゲーム。これの
・MAPランダム生成
・オブジェクトのランダム生成
部分を解説して行きます。この部分はローグライクの肝ですし、
何より他のゲームでも使い回せるテクニックになっています!環境は
MacOS Catalina:ver 10.15.4
Unity:ver2019.4.3f1
です。MAP&オブジェクトのランダム化
ランダム化のコード
BoardManager.csusing System.Collections; using System.Collections.Generic; using UnityEngine; //Map生成のscriptを記述する public class BoardManager : MonoBehaviour { //Map上にランダム生成するアイテム最小値、最大値を決めるclass public class Count { public int minmum; public int maximum; public Count(int min,int max) { minmum = min; maximum = max; } } //Mapの縦横 public int columns = 8; public int rows = 8; //生成するアイテムの個数 public Count Wallcount = new Count(3, 9); public Count foodcount = new Count(1, 5); //MAPの材料 public GameObject exit; public GameObject floor; public GameObject Wall; public GameObject OuterWall; public GameObject enemy; public GameObject food; //Object整理用(ランダム配置されたobjectの親に設定) private Transform boardHolder; //6×6のマスでobjectがない場所を管理する private List<Vector3> gridPositons = new List<Vector3>(); void Start() { //Mapの生成 BoardSetup(); //gridPositionsのクリアと再取得 InitialiseList(); //ランダムに壁を生成 LayoutObjectAtRandom(Wall, Wallcount.minmum, Wallcount.maximum); //食べ物生成 LayoutObjectAtRandom(food, foodcount.minmum, foodcount.maximum); //出口の設置 Instantiate(exit, new Vector3(columns - 1, rows - 1, 0), Quaternion.identity); } //フィールド生成 void BoardSetup() { //Boardをインスタンス化してboardHolderに設定 boardHolder = new GameObject("Board").transform; for (int x = -1; x < columns + 1; x++) { for (int y = -1; y < rows + 1; y++) { //床を設置してインスタンス化の準備 GameObject toInsutantiate = floor; //8×8マス外は外壁を設置してインスタンス化の準備 if (x == -1 || x == columns || y == -1 || y == rows) { toInsutantiate = OuterWall; } //toInsutantiateに設定されたものをインスタンス化 GameObject instance = Instantiate(toInsutantiate, new Vector3(x, y, 0), Quaternion.identity) as GameObject; //インスタンス化された床or外壁の親要素をboardHolderに設定 instance.transform.SetParent(boardHolder); } } } //gridPositionsをクリアする void InitialiseList() { //リストをクリアする gridPositons.Clear(); //6×6のますをリストに取得する for(int x = 1; x < columns -1; x++) { for(int y = 1;y < rows -1; y++) { //gridPositionsにx,yの値をいれる gridPositons.Add(new Vector3(x, y, 0)); } } } //gridPositionsからランダムな位置を取得する Vector3 RandomPosition() { //randomIndexを宣言して、gridPositionsの数から数値をランダムで入れる int randomIndex = Random.Range(0, gridPositons.Count); //randomPositionを宣言して、gridPositionsのrandomIndexに設定する Vector3 randomPosition = gridPositons[randomIndex]; //使用したgridPositionsの要素を削除 gridPositons.RemoveAt(randomIndex); return randomPosition; } //Mapにランダムで引数のものを配置する(敵、壁、アイテム) void LayoutObjectAtRandom(GameObject tile,int minimum,int maximum) { //生成するアイテムの個数を最小最大値からランダムに決め、objectCountに設定する int objectCount = Random.Range(minimum, maximum); //設置するオブジェクトの数分ループで回す for(int i = 0; i < objectCount; i++) { //現在オブジェクトが置かれていない、ランダムな位置を取得 Vector3 randomPosition = RandomPosition(); //生成 Instantiate(tile, randomPosition, Quaternion.identity); } } }コード解説
BoardSetup
ループを使いフィールドを生成しています
下記の図を見てください
フィールド全体は
10×10で生成されており、
一番外側は
破壊不可なOuterWall(外壁)を設置する場所です。8×8はPlayerが移動可能な範囲で、
なおかつfloor(床)を設置する場所。Playerが現在立っている位置を
(0,0)としているため、
外壁が配置されるのは
x,yのどちらかまたは両方に-1or8の位置がある時になります。なのでループの開始を-1から始め10進んだ数値の8までループが回るようにし、
x,yに-1or8が含まれていたらOuterWall(外壁)、
それ以外はfloor(床)を配置するようにしています。InitialiseList
上記画像の6×6の場所を
リスト(gridPositons)に数値としてに保持するこのgridPositonsに保持されている数値は、
objectが無い状態です。アイテムを配置する際にこのリストを使って
Objectがない位置を把握して、配置して行きます。RandomPosition
randomIndexに
gridPositonsに保持されている位置の要素数から
ランダムに数値を選び設定する。randomPositionにgridPositonsの
randomIndexに保持されている場所を設定する。一度randomPositionに設定された数値は削除して、
アイテムが二重で置かれることがないようにしている。
LayoutObjectAtRandom
アイテムごとに決められた、
最小最大値からランダムでアイテムを設置するべき数値を決める。決められた数値objectCount分ループで回して、
randomPositionに設置していく。最後に
MAPのランダム生成について解説して行きました。
今後もゲーム開発に役立つテクニックを
投稿していこうと思っています!また自分のブログでは
初心者でもゲーム開発ができるように1から丁寧に解説しています。
ローグライクのゲームの作り方を
1から知りたい方はぜひブログを覗いてみてください!
ブログを覗いてみる