第1章:開発環境とプロジェクト構造
Android (Java) 開発者が Visual Studio (C#) へ移行する際に、まず「どこに何があるか」をマッピングします。
プロジェクト・階層構造の対比
開発環境全体の構成単位と、物理的なファイル階層の対応表です。
| 項目 | Visual Studio / C# | Android Studio / Java |
|---|---|---|
| 全体容器 | ソリューション (.sln) 複数のプロジェクトを束ねるRootファイル。 |
Project 最上位のプロジェクトフォルダ。 |
| 開発/ビルド単位 | プロジェクト (.csproj) 実行ファイルやライブラリの単位。Moduleに相当。 |
Module appモジュールやlibモジュール。 |
| ライブラリ管理 | NuGet GUIまたはパッケージコンソールで管理. 情報は.csprojに保存される。 |
Gradle build.gradle (dependencies) で記述。 |
| 画面ファイル | Form1.cs + Form1.Designer.cs ロジックとUI(自動生成コード)を分離して保持。 |
MainActivity.java + activity_main.xml JavaコードとXMLレイアウトファイル。 |
| 設定/マニフェスト | App.config / Properties/ アプリの構成情報を管理。 |
AndroidManifest.xml 権限やActivityの定義。 |
| ビルド成果物 | bin/Debug(またはRelease)/ .exe や .dll が出力される。 |
build/outputs/apk/ .apk や .aab が出力される。 |
| UIコンポーネント(コントロール変数名) | btnAdd, txtName, chkEnabled(接頭辞+camelCase) |
WinForms の Designer 既定は「接頭辞(btn/txt/chk/lst/dgv...)+ camelCase」が多い。 型名(Button / TextBox など)は PascalCase。イベントハンドラは例: btnAdd_Click。
|
.cs ファイル名(クラス名)をどう付ける?
C# では「public クラス名 = ファイル名」に揃えるのが一般的です。強制ではありませんが、保守性が上がります。
- 基本:
public class Form1はForm1.cs - 1ファイルに複数クラスは可能。ただし public 型は 1 つが無難
- WinForms は partial class を使い、Designer 生成コードが
Form1.Designer.csに分離される - 関連クラスはフォルダで整理:例
Models/UserDto.cs,Services/AnimalApiClient.cs
リリースビルドの方法と場所
配布用の実行ファイル(.exe)を作成する手順です。
1. ビルド構成を切り替える
Visual Studio のツールバー上部にあるドロップダウンメニュー(通常は「Debug」になっている場所)をクリックし、「**Release**」を選択します。
2. ビルドを実行する
メニューの「ビルド」→「**ソリューションのビルド**」(ショートカット: Ctrl + Shift + B)を実行します。
3. 成果物 (.exe) の場所
プロジェクトフォルダ内の以下のパスに生成されます。
[プロジェクトフォルダ]\bin\Release\[フレームワーク名]\YourApp.exe
NuGet の使い方 (Gradle相当)
- GUIでの管理: ソリューションエクスプローラーでプロジェクト名を右クリック →「**NuGet パッケージの管理**」を選択。パッケージを検索・インストールできます。
- 設定の保存: インストールした情報は、
.csprojファイルの中に自動的に書き込まれます。
using の自動補完 (import相当)
- クイックアクション (Ctrl + .): Android Studioの `Alt + Enter` と同じです。未定義のクラスを書くと文字が赤くなるので、そこで **
Ctrl + .** を押すと必要なusing文を自動挿入します。 - 暗黙的な using (Implicit Usings): 最新のC#では、
SystemやLinqなどの頻出名前空間は、ファイルに記述しなくても最初から使える設定がデフォルトになっています。
new Form1() と onCreate の対応
// Program.cs (エントリポイント)
static void Main() {
// ここでの new Form1() が Android の初期化開始地点に近い
Application.Run(new Form1());
}
Form1()(Constructor): AndroidのsetContentView()に相当。内部で自動生成されるInitializeComponent()がコントロールをnewします。Form1_Loadイベント: AndroidのonCreateの後半。UIパーツの準備が100%整った直後. データの初期セットに最適。Shownイベント: AndroidのonResumeに相当. 画面が実際に表示された直後の処理。
コード変換例:UI設定とイベントリスナー
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_main);
/* ボタンを非表示にしてイベント登録 */
Button btn1 = findViewById(R.id.btnOpenPower);
btn1.setVisibility(View.INVISIBLE);
btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
openPowerSettings();
}
});
Button btn2 = findViewById(R.id.btnCheckBattery);
btn2.setOnClickListener(v -> batteryCheck());
}
// Form1.cs
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
// 1. findViewByIdは不要!
// デザイナーで指定した「(Name)」がそのまま変数名
// 2. Visibilityの制御
btnOpenPowerSetting.Visible = false;
// 3. Clickイベントの登録(+= 演算子)
btnOpenPowerSetting.Click += (s, e) => {
OpenPowerSettings();
};
btnCheckBattery.Click += (s, e) => {
BatteryCheck();
};
}
}
partial class とは何なのか
1つのクラス定義を**複数のファイルに分割して記述できる**仕組みです。主に「自動生成コード」と「自分のコード」を分けるために使われます。
- WinFormsでの役割: デザイナーがボタン生成などを書く
Form1.Designer.csと、あなたがロジックを書くForm1.csを分けることで、自動生成部分を壊すリスクを減らします。 - classとだけ書いたら?: 両方のファイルで
class Form1と書くと、同一プロジェクト内に同じクラスが2つあることになり、「定義の重複」でビルドエラーになります。
UI操作の具体的なコード例
UI上に label1, button1, input1 (テキスト入力) が配置されている場合の典型的な実装例です。
// Form1.cs
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
// button1が押された時の処理を登録
button1.Click += (sender, e) => {
// input1 (TextBox) の入力内容を取得
string inputText = input1.Text;
if (string.IsNullOrEmpty(inputText)) {
// 未入力(またはnull)の場合
label1.Text = "空";
} else {
// 入力内容を label1 に反映
label1.Text = inputText;
}
};
}
}
デバッグとログ出力 (Logcat対比)
Android Studio の Logcat に慣れた人のための、Visual Studio でのログ・デバッグ運用ガイドです。
1. ログを見るための操作(Logcatウィンドウの出し方)
Visual Studio では「**出力ウィンドウ**」を使用します。
- 表示方法: メニューの「表示」→「出力」を選択(ショートカット:
Ctrl + Alt + O)。 - 重要設定: ウィンドウ上部の「出力元」ドロップダウンで「**デバッグ**」を必ず選択してください。※ここが「ビルド」のままだとコンパイル結果しか見えません。
2. Log.d() に相当するタグ (カテゴリ) の指定
// 第2引数が「カテゴリ」となり、出力ウィンドウでタグのように機能する
System.Diagnostics.Debug.WriteLine("処理を開始しました", "MyNetworkTag");
// 表示例 -> MyNetworkTag: 処理を開始しました
3. インスタンス(クラス・構造体)をログに出す方法
Java の Log.d(tag, obj) はオブジェクトを渡すだけで中身が見えますが、C# の Debug.WriteLine にインスタンスをそのまま渡すと型名(例: MyProject.User)しか表示されません。
クラスや構造体(struct)の中身(全フィールド・プロパティの値)をダンプしたい場合, System.Text.Json を使ってJSON文字列化して出力するのが C# の定石です。
// class でも struct でも同様に使えます
var user = new User { Id = 100, Name = "Taro" };
// JSONにシリアライズしてダンプ
System.Diagnostics.Debug.WriteLine(
System.Text.Json.JsonSerializer.Serialize(user),
"DataDump"
);
// 表示例 -> DataDump: {"Id":100,"Name":"Taro"}
4. Exception のログ出力(トレースを出す)
例外オブジェクトに関しては、JSON化してはいけません。スタックトラースなどの深い階層の情報がシリアライズされないためです。
解決策: 例外オブジェクトの ToString() を使います。これで型名, エラーメッセージ, フルスタックトレースが一度に吐き出されます(Androidの ex.printStackTrace() 相当)。
try {
// 例外が発生する処理
} catch (Exception ex) {
// 全情報を出力(これが最も推奨される方法)
System.Diagnostics.Debug.WriteLine(ex.ToString(), "Error");
}
第2章:C# コーディング規約と基本構文
C# 標準コーディング規約
| 項目 | C# (PascalCase) | Java (camelCase) |
|---|---|---|
| メソッド / プロパティ / クラス | DoWork / Count / MyClass | doWork / count / MyClass |
| プライベートフィールド | _count (アンダースコア) | count |
波括弧 { } | 改行して開始 | 行末から開始 |
| UIコンポーネント(コントロール変数名) | btnAdd, txtName, chkEnabled(接頭辞 + camelCase) |
WinForms の Designer 既定は 「接頭辞(btn / txt / chk / lst / cmb / dgv …)+ camelCase」。 型名(Button / TextBox など)は PascalCase。 イベントハンドラ例: btnAdd_Click
|
.cs ファイル名の付け方(C# の慣習)
C# では public クラス名とファイル名を一致させるのが一般的です。 言語仕様での強制ではありませんが、可読性と保守性が大きく向上します。
public class UserService→UserService.cspublic partial class Form1→Form1.cs(ロジック)+Form1.Designer.cs(Designer 自動生成)- 1 ファイルに複数クラスは可能。ただし public 型は 1 つが推奨
- DTO / Model は用途別に分離:
Models/UserDto.cs,Models/AnimalRequest.cs
namespace はどこで決まる?(基本はプロジェクト名)
namespace は「クラスが属する論理的な名前空間」です。多くのプロジェクトでは、
既定の namespace = プロジェクト名になります(Visual Studio のテンプレートがそう生成するため)。
- 例:プロジェクト名が
WinFormsAnimalSampleなら、既定はnamespace WinFormsAnimalSample - フォルダ構成を namespace に反映する設定も一般的:例
Models/AnimalRequest.cs→namespace WinFormsAnimalSample.Models - 既定の namespace は プロジェクトのプロパティ(既定の名前空間)で変更可能
.csのファイル名は namespace を決めません(namespace はソースコード内で宣言する)
Android の
package と役割が近いです(ただし C# は 1 ファイルに複数 namespace 宣言も可能)。
変数・型システム・定数
C#は静的型付け言語ですが、強力な型推論を備えています。
1. 基本データ型 (Javaとの比較)
| 種類 | C# 型名 | Java 型名 | 備考 |
|---|---|---|---|
| 整数 | int / long | int / long | 32bit / 64bit |
| 小数 | double / float | double / float | 基本はdoubleを使用 |
| 真偽値 | bool | boolean | |
| 文字列 | string | String | C#は小文字が標準 |
2. 静的型付けと推論型 (var)
右辺から型が明らかな場合、var を使って記述を簡略化できます。
var list = new List<string>(); // 型推論
int count = 10; // 明示的型付け
3. 変数のスコープと定義場所
- グローバル変数: C#には存在しません。すべての変数はクラスまたはメソッドに属します。共有したい場合は
staticメンバを使用します。 - クラス変数 (フィールド): クラス直下で定義.
staticを付けると全インスタンスで共有されます。privateなら_camelCase。 - ローカル変数: メソッド内で定義.
camelCase。 - 制御構文内での定義:
ifやforのブロック内で変数を定義可能. スコープはブロック内に閉じます。
public class MyClass {
public static int SharedCounter = 0; // static変数(クラス変数)
private int _instanceCount = 0; // インスタンス変数
public void DoSomething() {
if (true) {
var temp = "Hello"; // if文の中だけで有効
}
// ここで temp は使えない
for (int i = 0; i < 5; i++) {
var loopVar = i * 2; // ループの中だけで有効
}
}
}
4. 定数 (const / readonly)
Javaの static final に相当します。
// 1. const: コンパイル時定数. staticを付けなくても自動的にstatic扱い
public const int MaxValue = 100;
// 2. readonly: 実行時定数. コンストラクタ内で代入可能(インスタンスごとに異なる定数を持てる)
private readonly string _id;
public MyClass() {
_id = Guid.NewGuid().ToString();
}
5. 型の取得と変換 (typeof / キャスト / 継承)
実行時の型情報の取得や、型の安全な変換方法、および継承関係でのキャストについてです。
// 1. typeof: クラス名から型情報を取得 (Javaの MyClass.class 相当)
Type t = typeof(User);
// 2. 基本的な数値キャスト (明示的変換)
double d = 123.45;
int i = (int)d; // 小数点以下切り捨て
// 3. 安全なキャスト (as 演算子)
// 変換できない場合は null を返す (Javaのキャストは例外が出る)
var person = someObject as Person;
if (person != null) { ... }
// 4. 型判定 (is 演算子)
if (someObject is Person p) {
// p を Person型としてそのまま使用可能
}
// 5. 継承クラス間のキャストと代入
// Dog : Animal (DogはAnimalを継承) と仮定
Animal myAnimal = new Dog(); // アップキャスト:暗黙的に可能
Dog myDog = (Dog)myAnimal; // ダウンキャスト:明示的なキャストが必要
Dog optDog = myAnimal as Dog; // 安全なダウンキャスト
アクセス修飾子 (Scope)
クラスやメンバ(変数・メソッド)の公開範囲を制御します。
| 修飾子 | 範囲 | 備考 |
|---|---|---|
public | 制限なし | どこからでも参照可能 |
private | クラス内のみ | C#のデフォルト(省略時) |
protected | クラス内 + 派生クラス | Javaのprotectedとほぼ同じ |
internal | プロジェクト内のみ | Javaのパッケージプライベートに近い |
public class Base {
private int _id; // 外部からも子クラスからも見えない
protected string Name; // 子クラスからのみアクセス可能
public void Run() { } // 誰でも呼べる
}
演算子 (算術・代入・ビット)
C#で使われる主要な演算子一覧です。
// 1. 基本代入と複合代入
int a = 10;
a += 5; // a = a + 5
a *= 2; // a = a * 2
// 2. Null合体代入 (??=)
// 変数が null の場合のみ右辺を代入する
string name = null;
name ??= "Guest"; // name は "Guest" になる
// 3. 三項演算子 (条件演算子)
// 条件 ? 真の場合 : 偽の場合
int age = 20;
string type = (age >= 18) ? "Adult" : "Child";
// 4. ビット演算子
uint flags = 0b_0000_0101; // 5
flags = flags << 1; // 左シフト (10になる)
flags = flags | 0b_0001; // ビット論理和 (OR)
flags = flags & 0b_1110; // ビット論理積 (AND)
flags = flags ^ 0b_1111; // 排他的論理和 (XOR)
flags = ~flags; // ビット反転 (NOT)
論理演算子
条件分岐で多用する論理演算です。
bool isReady = true;
bool hasError = false;
// 1. 論理積 (AND) - 両方真なら真
if (isReady && !hasError) { ... }
// 2. 論理和 (OR) - どちらか真なら真
if (isReady || hasError) { ... }
// 3. 排他的論理和 (XOR) - 片方だけが真なら真
bool diff = isReady ^ hasError;
// 4. 短絡評価 (Short-circuit)
// C#の && と || は左辺で結果が決まれば右辺を評価しません。
if (list != null && list.Count > 0) { ... } // listがnullなら右辺は動かないので安全
比較演算子
値やオブジェクトの比較を行う演算子です。
| 演算子 | 意味 | コード例 |
|---|---|---|
== | 等しい | if (a == b) |
!= | 等しくない | if (a != b) |
> / < | 大きい / 小さい | if (a > b) |
>= / <= | 以上 / 以下 | if (a >= b) |
C#では、
string 型に対して **== 演算子で「内容の比較」**が行えます。Javaのように .equals() を強制されることはありません。string s1 = "hello";
string s2 = "hello";
if (s1 == s2) {
// Javaと違い、インスタンスが別でも内容が同じなら「真」になります
}
※ 参照(アドレス)自体が同じかどうかを判定したい場合は、object.ReferenceEquals(s1, s2) を使用します。
算術演算子 (数値計算)
基本的な数値計算に使用する演算子です。
| 演算子 | 名称 | 例 | 結果 |
|---|---|---|---|
+ | 加算 | 5 + 2 | 7 |
- | 減算 | 5 - 2 | 3 |
* | 乗算 | 5 * 2 | 10 |
/ | 除算 | 5 / 2 | 2 (整数同士なら切捨て) |
% | 剰余 (余り) | 5 % 2 | 1 |
++ | インクリメント | i++ | i = i + 1 |
-- | デクリメント | i-- | i = i - 1 |
// 除算の注意点
int a = 5;
int b = 2;
double result = a / b; // 結果は 2.0 (int同士の計算が先に行われるため)
double result2 = (double)a / b; // 結果は 2.5 (片方をキャストすれば小数まで計算)
// 文字列結合
string greeting = "Hello " + "World"; // "Hello World"
文字列・数値の操作と変換
string 型や double 型に備わっている主要なメンバー関数と、型同士の変換方法です。
1. string の主要なメンバー関数
C# の Replace は指定した文字/文字列のすべてを置換します(Javaの replaceAll 相当)。また、`sprintf` 相当の機能として `string.Format` や「文字列補完」が用意されています。
string s = " C# Programming C# ";
// 文字数 (プロパティ)
int len = s.Length;
// 前後の空白削除
string trimmed = s.Trim();
// 部分一致・検索
bool hasC = s.Contains("C#");
int index = s.IndexOf("Pro"); // 見つからない場合は -1
// 切り出し (0から5文字)
string sub = s.Substring(0, 5);
// 置換 (すべての "C#" を置換)
string replaced = s.Replace("C#", "DotNet");
// 大文字・小文字変換 (JavaのtoLowerCase/toUpperCase相当)
string lower = s.ToLower();
string upper = s.ToUpper();
// --- フォーマット (sprintf相当) ---
string name = "Taro";
int score = 90;
// 1. string.Format (古典的)
string f1 = string.Format("Name: {0}, Score: {1}", name, score);
// 2. 文字列補完 (モダン・推奨) : 文字列の前に $ をつける
string f2 = $"Name: {name}, Score: {score}"; // 変数を直接 { } 内に書ける
2. double の主要な操作と数値の書式設定
数値計算には System.Math クラスを使用します。また、ToString() による強力な書式設定機能があります。
double val = 1234.5678;
// --- 数値計算 (Mathクラス) ---
double c = Math.Ceiling(1.1); // 2.0 (切り上げ)
double f = Math.Floor(1.9); // 1.0 (切り捨て)
// 丸め (第2引数で小数点以下の桁数を指定)
double r1 = Math.Round(1.555, 2); // 1.56
// 【重要】デフォルトのRoundは「銀行型丸め」です。
// 四捨五入(0.5なら上へ)を確実に行いたい場合は、第3引数を指定します。
double r2 = Math.Round(1.5, MidpointRounding.AwayFromZero); // 2.0
// --- 文字列への書式設定 ---
string s1 = val.ToString("F2"); // "1234.57" (小数点以下2桁)
string s2 = val.ToString("N0"); // "1,235" (カンマ区切り)
string s3 = val.ToString("C"); // "¥1,235" (通貨表示)
3. string ⇔ 数値型の相互変換
// --- 文字列から数値へ ---
string strNum = "123.45";
// 1. TryParse (推奨:失敗しても例外が出ず安全)
if (double.TryParse(strNum, out double d2)) { /* 成功 */ }
// 2. Parse (失敗すると例外が発生)
double d1 = double.Parse(strNum);
// --- 数値から文字列へ ---
double myNum = 99.9;
string out1 = myNum.ToString();
string out2 = $"{myNum}"; // 文字列補完 (推奨)
正規表現 (Regex)
文字列の高度なパターンマッチングには System.Text.RegularExpressions.Regex クラスを使用します。
using System.Text.RegularExpressions;
string input = "Date: 2023-10-05";
string pattern = @"(\d{4})-(\d{2})-(\d{2})"; // ( ) はグループ化
// 1. 一致判定
if (Regex.IsMatch(input, pattern)) { ... }
// 2. グループ取得 (括弧内の値を抽出)
Match match = Regex.Match(input, pattern);
if (match.Success) {
string year = match.Groups[1].Value; // "2023"
string month = match.Groups[2].Value; // "10"
string day = match.Groups[3].Value; // "05"
}
// 3. 置換
string result = Regex.Replace(input, @"\d", "*"); // 全数字を*に置換
主要なコレクション(配列・リスト・辞書)
1. 配列 (Array)
固定長です。初期化後にサイズ変更はできませんが、値の書き換えは自由です。
// 1. サイズ指定で初期化(中身は空/ゼロ)
int[] numbers = new int[3];
// 2. 代入・入れ替え
numbers[0] = 100;
numbers[1] = 200;
numbers[0] = 300; // 上書き
// 3. ループ (while)
int i = 0;
while (i < numbers.Length) {
System.Diagnostics.Debug.WriteLine(numbers[i]);
i++;
}
2. リスト (List<T>)
動的にサイズが変わる可変長配列です。
var list = new List<string> { "Apple", "Banana" };
// 0. 末尾への追加 (add)
list.Add("Cherry");
// 1. 取得 (get)
string s = list[0];
// 2. 存在確認 (contains)
bool hasBanana = list.Contains("Banana");
// 3. 置換 (replace)
list[1] = "Grapes";
// 4. 途中に挿入 (insert)
list.Insert(1, "Orange"); // インデックス1の位置に割り込む
// 5. 配列へ変換
string[] array = list.ToArray();
// 6. ループ処理 (foreach / while)
foreach (var item in list) { ... }
int j = 0;
while (j < list.Count) {
System.Diagnostics.Debug.WriteLine(list[j]);
j++;
}
3. 辞書 / ハッシュマップ (Dictionary<K, V>)
Java의 HashMap に相当します。キーと値のペアを管理します。
- 全取得:
Keysプロパティ,Valuesプロパティを使用します。 - タプル解釈:
foreachループで(key, value)の形式で直接受け取れます。
var map = new Dictionary<string, int>();
map["Alice"] = 25;
map["Bob"] = 30;
// 1. 全キー・全値の取得
var allKeys = map.Keys; // Keyコレクション
var allValues = map.Values; // Valueコレクション
// 2. ループ処理 (while / Enumerator)
var enumerator = map.GetEnumerator();
while (enumerator.MoveNext()) {
var pair = enumerator.Current;
System.Diagnostics.Debug.WriteLine($"{pair.Key}: {pair.Value}");
}
// 3. モダンなループ(タプルによる受け取り)
foreach (var (name, age) in map) {
System.Diagnostics.Debug.WriteLine($"名前: {name}, 年齢: {age}");
}
- C#のコレクションは
System.Linqを使うことで,.Where()や.Select()といった関数型プログラミング的な操作(JavaのStream API相当)が非常に強力に行えます。
Null安全とNull合体演算子
Kotlinに近い、非常に強力なNull操作が可能です。
string input = GetNullableString();
// 1. Null条件演算子 (?.)
// inputがnullならnullを返し、そうでなければLengthを返す
int? length = input?.Length;
// 2. Null合体演算子 (??)
// 左辺がnullなら右辺の値を採用する
string result = input ?? "デフォルト値";
// 3. Null合体代入演算子 (??=)
// 変数がnullの場合のみ代入する
_logger ??= new Logger();
Object・Tuple・dynamic
1. object 型とボックス化 (Boxing)
Javaの Object と同様、C#の object はすべての型のルートです。しかし、C#特有の「値型」を扱う際には注意が必要です。
- ボックス化 (Boxing): 値型(int, double, struct envyなど)を
object型に代入すること。データがスタックからヒープへコピーされ、メモリ割り当てのオーバーヘッドが発生します。 - ボックス化解除 (Unboxing):
objectから元の値型へキャストして戻すこと。
int i = 123;
object o = i; // ボックス化(スタックのiをヒープのoへコピー)
int j = (int)o; // ボックス化解除
2. タプル (Tuple)
Javaの Pair や Triple を自作する必要はありません。C#のタプルは非常に軽量で、3つ以上の値も制限なく保持でき、名前を付けることも可能です。
// 1. 初期化と定義(名前なし)
(int, string, bool) user = (1, "Taro", true);
System.Diagnostics.Debug.WriteLine(user.Item2); // Taro
// 2. 名前付きタプル(推奨・3つ以上の例)
var data = (Id: 10, Name: "Alice", IsActive: false, Score: 95.5);
System.Diagnostics.Debug.WriteLine(data.Name); // Alice
System.Diagnostics.Debug.WriteLine(data.Score); // 95.5
// 3. 多値戻り値としての利用(分解)
var (ok, msg) = Save();
3. dynamic 型
Javaには存在しない型で、**コンパイル時の型チェックを一切行いません**。実行時に型が確定し、動的にメソッドやプロパティを呼び出します。
dynamic d = "Hello World";
System.Diagnostics.Debug.WriteLine(d.Length); // 文字列として実行
d = 100; // 途中で数値を代入してもOK
d = d + 50;
// 存在しないメソッドを呼んでもコンパイルエラーにならない(実行時に例外が出る)
// d.NotExistMethod();
列挙型 (Enum) とビットフラグ
C#の enum は単なる整数値のエイリアスですが、[Flags] 属性を付けることで強力なビットフラグとして運用できます。
1. 基本的な enum
public enum Season { Spring, Summer, Autumn, Winter }
Season current = Season.Summer;
2. ビットフラグ (Flags)
複数の状態を1つの変数に持たせたい(論理和で結合したい)場合に使用します。値は必ず「2の階乗」で定義します。
[Flags]
public enum UserPermissions
{
None = 0, // 0000
Read = 1 << 0, // 0001 (1)
Write = 1 << 1, // 0010 (2)
Delete = 1 << 2, // 0100 (4)
Admin = Read | Write | Delete // 0111 (7)
}
// 権限の付与 (OR結合)
var myPerm = UserPermissions.Read | UserPermissions.Write;
// 権限の判定 (AND判定)
bool canDelete = (myPerm & UserPermissions.Delete) == UserPermissions.Delete;
// またはモダンな書き方
bool canWrite = myPerm.HasFlag(UserPermissions.Write);
// 権限の削除
myPerm &= ~UserPermissions.Write;
制御構造文 (if, for, foreach, while, etc.)
基本はJavaと同じですが、C#独自の拡張も含まれます。
// 1. if / else
if (score > 80) { ... }
else if (score > 50) { ... }
else { ... }
// 2. for (回数指定)
for (int i = 0; i < 10; i++) { ... }
// 3. foreach (列挙可能なコレクション全走査)
foreach (var item in list) { ... }
// 4. while (条件継続)
while (queue.Any()) { var x = queue.Dequeue(); }
// 5. switch (多分岐)
switch (category) {
case "A":
DoA();
break; // C#はbreak必須(フォールスルー禁止)
case "B" when isAdmin: // ガード句付き
DoB();
break;
default:
DoOther();
break;
}
// 6. try - catch - finally (例外処理)
try {
PerformDangerTask();
} catch (IOException ex) when (ex.HResult == 123) { // 例外フィルタ
Log(ex.Message);
} catch (Exception ex) {
HandleGeneral(ex);
} finally {
CloseConnection(); // 必ず実行
}
private)private)nullstruct も class と同様に、メンバ(変数やメソッド)に public や private などを付けることができます。何も書かない場合は private と見なされます。
2. 代入時の挙動の違い (サンプルコード)
class PointC { public int X; }
var a = new PointC { X = 10 };
var b = a; // 参照(アドレス)をコピー
b.X = 99;
// a.X も 99 になる!
// a と b は同じ実体を見ている
struct PointS { public int X; }
var a = new PointS { X = 10 };
var b = a; // 値が丸ごとコピーされる
b.X = 99;
// a.X は 10 のまま!
// a と b は完全に別々の実体
3. this キーワードの役割
this は「その時点での自分自身のインスタンス」を指します。
クラスでの this
主にフィールド名と引数名の重複を避けるためや、自分自身を他のメソッドに渡すために使用します。もちろん使用可能です。
public class User {
private string name;
// 1. 名前の衝突回避 (引数のnameとフィールドのnameを区別)
public void SetName(string name) {
this.name = name;
}
// 2. メソッドチェーン
public User DoSomething() {
// 処理...
return this; // 自分自身を返す
}
}
構造体での this
構造体でも this は使えますが、挙動が少し異なります。
- クラスの
thisは読み取り専用の参照ですが、構造体のthisは自分自身という変数そのもののように振る舞います。 - 構造体のコンストラクタ内では、
thisを使って全てのフィールドを初期化する必要があります。
struct Point {
public int X, Y;
public Point(int x, int y) {
this.X = x; // 自分のフィールドに代入
this.Y = y;
}
public void Reset() {
this = new Point(0, 0); // 自分自身を丸ごと書き換えることも可能!
}
}
抽象クラス / インターフェース / ジェネリクス
より高度な設計に欠かせない、型の定義手法です。
1. インターフェース (Interface)
「何ができるか」という機能の契約を定義します。C#では慣習的に頭文字に I を付けます。
なぜインターフェースが必要なのか?(ストーリーで理解)
あなたは「レポートを保存する機能」を作っています。最初は「ローカルファイルに保存」するだけで済みましたが、将来的に「データベースに保存」したり「クラウドに送信」する機能が必要になるかもしれません。
この時、呼び出し側のコードが
LocalFileSaver クラスに直接依存していると、保存先を増やすたびにコードを書き換える必要が出てきます。
解決策:インターフェースによる「付け替え」
「保存できる(Saveできる)」という契約だけを決めておきます。こうすることで、メインの処理は「どう保存されるか」を気にせず、「保存を依頼する」だけで済みます。
// 1. 契約を決める (ISaverインターフェース)
public interface ISaver {
void Save(string data);
}
// 2. 具体的な実装を作る (ファイル保存)
public class FileSaver : ISaver {
public void Save(string data) => File.WriteAllText("test.txt", data);
}
// 3. 別の実装を作る (DB保存)
public class DatabaseSaver : ISaver {
public void Save(string data) => Console.WriteLine("DBに保存しました");
}
// --- 利用側 ---
public void ProcessData(ISaver saver) {
saver.Save("重要データ"); // saverがFileSaverかDatabaseSaverか知らなくて良い
}
2. 抽象クラス (Abstract Class)
「共通の性質」を持ちつつ、一部の実装をサブクラスに任せる場合に使用します。インターフェースとの決定的な違いは、「共通の変数(フィールド)」や「共通のロジック」を持てることです。
インターフェースとの使い分け
- インターフェースを選ぶべき時: 異なる種類のクラスに「共通の機能」だけを付けたい時(例:ロボットと人間はどちらも『食べる』ができるが、共通の親を持つ必要はない)。
- 抽象クラスを選ぶべき時: 「仲間内」で共通の基盤コードを使い回したい時(例:犬と猫はどちらも『動物』であり、心拍数の計算などの共通ロジックを持つ)。
public abstract class Animal {
public string Name { get; set; } // フィールドを持てる
public void Sleep() => Console.WriteLine("眠る..."); // 共通ロジック
public abstract void MakeSound(); // 子クラスで必ず実装させる
}
public class Dog : Animal {
public override void MakeSound() => Console.WriteLine("ワン!");
}
3. ジェネリクス (Generics / 汎用型)
型をパラメータとして受け取れる仕組みです。主に以下の目的で使用します:
- コードの再利用: 型ごとにクラスを作る必要がなくなる。
- 型安全:
object型で扱うのと違い、実行時のキャストエラーを防止できる。 - パフォーマンス: 値型を扱う際の「ボックス化」を回避できる。
// どんな型でも格納できる箱
public class Box<T> {
private T _content;
public void SetContent(T content) => _content = content;
public T GetContent() => _content;
}
// 使用例
var intBox = new Box<int>(); // int専用
var strBox = new Box<string>(); // string専用
関数の定義 / 戻り値 / 引数
メソッド(関数)を定義する際の基本的な構造と、戻り値の種類による挙動の違い、引数の柔軟な指定方法について解説します。
1. 戻り値のパターンとメモリ挙動
| 戻り値の型 | メモリ挙動(重要) | コード例 |
|---|---|---|
| void | 何も返しません。 | |
| 基本型 | 値そのものがコピーされて返ります。 | |
| コレクション (List/Array) |
ヒープ上にあるインスタンスへの参照(アドレス)を返します。 | |
| クラス | インスタンスへの参照を返します。null を返すことも可能です。 |
|
| 構造体 (struct) | インスタンスの値そのもの(全フィールド)がコピーされて返ります。 | |
ラムダ式(ラムダ関数)の基本
ラムダ式(ラムダ関数)は、名前を持たない小さな関数を その場で定義するための記法です。 主に LINQ やコールバック処理で使用されます。
Java のラムダ式との比較(C# でも同じ書き方ができる)
Java では aa(() -> { ... }) のように、
メソッド呼び出しの引数としてラムダ式を直接渡す書き方が一般的です。
C# でもほぼ同じ書き方が可能です。
Java の例
void aa(Runnable r) {
r.run();
}
aa(() -> {
System.out.println("hello");
});
C# の対応する書き方
void Aa(Action action)
{
action();
}
Aa(() =>
{
Console.WriteLine("hello");
});
記法は似ていますが、考え方に違いがあります。
- Java:ラムダは 関数型インターフェース に渡される
- C#:ラムダは デリゲート型(
Action,Funcなど)に渡される
戻り値がある場合
void Aa(Func<int> func)
{
int value = func();
Console.WriteLine(value);
}
Aa(() => 123);
対応表(Java → C#)
| Java | C# |
|---|---|
| Runnable | Action |
| Consumer<T> | Action<T> |
| Supplier<T> | Func<T> |
| Function<T, R> | Func<T, R> |
このため C# でも、Java と同じ感覚で 「処理そのものを引数として渡す」コードを書くことができます。
基本的な書き方
// 引数なし・戻り値あり
Func<int> getNumber = () => 10;
// 引数あり・戻り値あり
Func<int, int> square = x => x * x;
// 引数あり・戻り値なし
Action<string> print = message =>
{
Console.WriteLine(message);
};
=> は「この引数を使って、この処理を行う」という意味を持ち、
左側が引数、右側が処理内容です。
通常のメソッドとの対応関係
// 通常のメソッド
int Square(int x)
{
return x * x;
}
// 上と同じ意味のラムダ式
Func<int, int> square = x => x * x;
ラムダ式は「短く・一度きり使う処理」を書くためのもので、 複雑な処理や再利用する処理は通常のメソッドとして定義します。
デリゲート(delegate)の基本
デリゲート(delegate)は、「メソッド(関数)そのものを変数として扱うための型」です。
C# では、処理を引数として渡したり、後から呼び出したりするための
基礎となる仕組みです。
なぜ delegate が必要なのか
通常のメソッド呼び出しでは、「どの処理を行うか」はコードに固定されます。 一方 delegate を使うと、 「どの処理を行うか」を呼び出し側が決めることができます。
基本的な定義
// int を受け取り、int を返すメソッドを表す delegate
delegate int Calculate(int x);
delegate にメソッドを代入して呼び出す
int Square(int x)
{
return x * x;
}
Calculate calc = Square;
int result = calc(5); // 25
このように、メソッド名をそのまま代入できる点が特徴です。
delegate を引数として使う例
void Execute(Calculate calc)
{
int value = calc(10);
Console.WriteLine(value);
}
Execute(Square);
この例では、Execute メソッドは
「どんな計算をするか」を知らず、
計算方法は呼び出し側から渡されています。
delegate とラムダ式の関係
ラムダ式は、delegate を簡潔に書くための記法です。 先ほどの delegate は、次のようにラムダ式で渡すこともできます。
Execute(x => x * x);
Action / Func との関係
Action や Func は、
よく使われる delegate をあらかじめ用意したものです。
Action:戻り値なしFunc<T>:戻り値あり
そのため、実務では delegate を自分で定義するよりも、
Action / Func を使うことが多くなります。
2. 引数の応用パターンと呼び出し例
// --- 定義側 ---
// 1. 可変長引数 (params)
// Javaの String... args と同様。
public void Log(params string[] messages) {
foreach (var m in messages) Console.WriteLine(m);
}
// 2. オプション引数 (初期値つき引数)
public void Save(bool autoFlush = true, string path = "log.txt") {
// 処理...
}
// 3. 名前付き引数を利用する関数
public void Config(int port, string host, bool useSsl) {
// 処理...
}
// --- 呼び出し側 ---
// 1. 可変長引数
Log("Error", "Critical", "Memory Out");
// 2. オプション引数
Save(); // autoFlush=true, path="log.txt" が使われる
Save(false); // autoFlush=false, path="log.txt" が使われる
Save(true, "backup.txt"); // 全て指定
// 3. 名前付き引数 (引数の順序を自由に変えられる)
Config(useSsl: true, host: "127.0.0.1", port: 8080);
コンストラクタと破棄 (Dispose)
オブジェクトの生成時と破棄時の処理を管理します。Javaのガベージコレクション(GC)任せとは異なる「明示的な破棄」が重要になります。
1. コンストラクタ (Constructor)
class と struct で挙動が異なります。
- クラス: 自由度が高い。引数なしコンストラクタも定義可能。
- 構造体: 全てのフィールドを初期化しなければならない。※C# 10以前は引数なしコンストラクタの定義は禁止されていました。
public class MyClass {
public int Value;
// クラスのコンストラクタ
public MyClass(int v) { this.Value = v; }
}
public struct MyStruct {
public int X;
// 構造体のコンストラクタ
public MyStruct(int x) { this.X = x; } // 全フィールドの初期化が必須
}
2. 破棄と IDisposable (Dispose)
ファイル、ネットワーク、データベース接続などの「アンマネージリソース」を扱う場合、GCを待たずに即座に解放する必要があります。そのための仕組みが IDisposable インターフェースです。
public class MyResource : IDisposable {
// 外部リソースを解放するロジック
public void Dispose() {
Console.WriteLine("リソースを解放しました");
}
}
// 使用時:using 文を使うと、スコープを抜けるときに自動で Dispose() が呼ばれる
using (var res = new MyResource()) {
// 処理...
} // ここで Dispose() が確実に行われる
static メンバと拡張メソッド
インスタンス化せずに利用する仕組みや、既存の型を拡張する機能です。
1. static メンバ
クラス全体で共有されるデータや機能です。 class にも struct にも、さらにジェネリッククラス MyClass<T> にも定義できます。
public class Counter {
public static int Count = 0; // 全インスタンスで1つの領域を共有
public static void Increment() => Count++;
}
// ジェネリクスの場合:型引数ごとに static 領域が作られる
public class Container<T> {
public static int Version = 1; // Container<int> と Container<string> は別物
}
2. 拡張メソッド (Extension Methods)
既存のクラス(stringやList、あるいは自作インターフェース)に、外側からメソッドを「生やす」ことができる強力な機能です。
- 定義条件:トップレベルの
static class内で定義し、第1引数にthisを付ける。 - 適用範囲:
class,struct,interfaceすべてに適用可能。
// 1. staticクラスを作る
public static class StringExtensions {
// 2. staticメソッドを作り、第1引数に this を付ける
public static bool IsEmail(this string str) {
return str.Contains("@"); // 簡易チェック
}
}
// 3. 呼び出し時(あたかも string のメンバであるかのように呼べる)
string myMail = "test@example.com";
bool result = myMail.IsEmail();
第4章:高度なオブジェクト指向
カプセル化を促進するプロパティ機能や、メタデータを扱う属性について解説します。
プロパティ (脱Getter/Setter)
Javaの getField() / setField() は、C#では言語レベルでサポートされています。これを「プロパティ」と呼びます。
1. 基本形と自動実装プロパティ
public class User {
// 内部変数(バッキングフィールド)
private string _name;
// 手動定義
public string Name {
get { return _name; }
set { _name = value; } // value というキーワードがセットされる値
}
// 自動実装プロパティ (推奨:中身が単純な場合)
public int Age { get; set; }
// 読み取り専用
public string ID { get; private set; }
// init専用 (初期化時のみ書き込み可)
public string AccountId { get; init; }
}
🔍 private set; / init; とは何か
private set; は「外部からは読み取り専用にしたいが、クラス内部では値を変更したい」 場合に使用します。たとえば、コンストラクタや内部ロジックでのみ値を更新し、 呼び出し元からの不用意な変更を防ぎたいときに使います。
init; は C# 9.0 以降で導入された仕組みで、 「オブジェクト生成時にだけ代入でき、その後は変更不可」にしたい場合に使います。 設定クラス(appsettings)や DTO など、生成後に値が変わらない前提のデータに向いています。
private set;:生成後もクラス内部では変更可能init;:生成時のみ代入可能。以後は完全に読み取り専用
どちらも「不変性を高めてバグを防ぐ」ための設計手段で、 業務コードでは非常によく使われます。
📌 init を使ったサンプルコード
init; は「オブジェクト生成時のみ値を設定できる」ため、
設定クラスや DTO(データ受け渡し専用クラス)でよく使われます。
public class AppConfig
{
public string AppName { get; init; }
public int MaxRetryCount { get; init; }
}
生成時の初期化は、オブジェクト初期化子を使って次のように行います。
var config = new AppConfig
{
AppName = "Sample Application",
MaxRetryCount = 3
};
この時点で config は完全に初期化され、
以降は値を変更できません。
config.MaxRetryCount = 5; // ❌ コンパイルエラー
この仕組みにより、「途中で設定値が書き換わってしまう」 といったバグを防ぐことができます。
属性 (Attribute) の活用
クラスやメソッドに「付加情報(メタデータ)」を付与します。Javaの @Annotation に相当します。
[Serializable] // このクラスはシリアライズ可能であるという印
public class Player {
[Obsolete("このメソッドは古いので NewMethod() を使ってください")]
public void OldMethod() { ... }
// 自分で定義した属性なども使える
}
属性(Attribute)の基本
C# の 属性(Attribute)は、クラスやメソッド、プロパティなどに
「メタ情報(注釈)」を付ける仕組みです。
Android/Java の @Deprecated, @Override, @SuppressWarnings に近い概念です。
[Obsolete] の意味(何が起きる?)
[Obsolete] は「この API は古いので使わないでください」という意思表示です。
主な効果は コンパイル時(またはビルド時)に警告/エラーを出させることです。
実行時に自動で特別な処理が走るわけではありません(例外を投げる等はしません)。
[Obsolete("理由")]:呼び出し側に 警告 を出す[Obsolete("理由", true)]:呼び出し側を コンパイルエラー にする
[Obsolete("このメソッドは古いので NewMethod() を使ってください")]
public void OldMethod() { /* ... */ }
[Obsolete("このメソッドは古いので NewMethod() を使ってください", true)]
public void VeryOldMethod() { /* ... */ } // 呼び出すとコンパイルエラー
もし「実行時に古いメソッドの使用を検出してログを出す」などをしたい場合は、 リフレクションで属性を読み取って自分で処理します。 ただし一般的には、Obsolete は「コンパイラに注意させる」用途が中心です。
[Serializable] の意味(何のため?)
[Serializable] は「この型は(特定の)シリアライズ方式で扱える」という目印です。
ただし、JSON シリアライズ(System.Text.Json)では通常不要です。
これは System.Text.Json が [Serializable] を見ないためです。
一方で、昔の .NET のバイナリ直列化(BinaryFormatter など)では参照されましたが、
BinaryFormatter は現在セキュリティ上の理由で非推奨です。
自作 Attribute の例(Java のアノテーションに近い)
自分で Attribute を作ると、例えば「ログ対象メソッド」「権限が必要な API」などの目印を付けられます。 付けた情報はリフレクションで読み取ります。
// 1) 自作 Attribute を定義
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class LoggableAttribute : Attribute
{
public string Tag { get; }
public LoggableAttribute(string tag) => Tag = tag;
}
// 2) 使う側
public class Player
{
[Loggable("battle")]
public void Attack()
{
// ...
}
}
// 3) 実行時に読む(例)
var m = typeof(Player).GetMethod("Attack");
var attr = (LoggableAttribute?)Attribute.GetCustomAttribute(m!, typeof(LoggableAttribute));
if (attr != null)
{
Console.WriteLine("Log tag = " + attr.Tag);
}
よく使う Attribute 例
[Obsolete]:古い API の警告/禁止[Flags]:enum をビットフラグとして扱う[DllImport]:ネイティブ DLL 呼び出し(P/Invoke)[JsonPropertyName]:JSON のキー名を指定(System.Text.Json)[Required],[Range]:入力チェック(DataAnnotations)
第5章:コレクションとLINQ
データの集合を扱うための「コレクション」と、それを直感的に操作する「LINQ」を解説します。
よく使うコレクション
| 名称 | 型 | 特徴 |
|---|---|---|
| List | List<T> | 可変長配列。最も多用される。 |
| Dictionary | Dictionary<K,V> | キー・値ペア。検索が高速。 |
| HashSet | HashSet<T> | 重複を許さない集合。 |
LINQの基本操作
LINQ (Language Integrated Query) は、コレクションに対するフィルタリングや変換を SQL のように記述できる機能です。
using System.Linq;
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8 };
// 1. Where (抽出)
var even = numbers.Where(n => n % 2 == 0); // 2, 4, 6, 8
// 2. Select (変換/射影)
var doubled = numbers.Select(n => n * 2); // 2, 4, 6...
// 3. FirstOrDefault (最初の一つを取得。なければnull/デフォルト)
int first = numbers.FirstOrDefault(n => n > 10); // 0 (intのデフォルト)
// 4. ToList / ToArray (結果を実体化させる)
var listResult = even.ToList();
// 5. OrderBy (ソート)
var sorted = numbers.OrderByDescending(n => n);
varは「型を省略する書き方」です。
右辺からコンパイラが型を推論し、実際の型は決まっています(動的型ではありません)。
例:var even = numbers.Where(...)のevenの型はIEnumerable<int>です。Where/Select/OrderByは「遅延実行(Deferred Execution)」が基本です。
この段階では「検索条件(式)を持った結果オブジェクト」を作っているだけで、まだループ処理(実際の計算)は動いていません。- だから
foreachで回す理由:
IEnumerable<T>は「列挙できる(並んだ値を順番に取り出せる)」という意味で、
中身を1件ずつ取り出して初めて処理が走ります。その代表的な方法がforeachです。
(foreach自体が特別な魔法ではなく、内部ではGetEnumerator()で列挙しています) - 「実体化(すべて計算して固定する)」したい場合:
ToList()/ToArray()を呼ぶと、その時点で列挙が行われ、結果がメモリ上に確定します。
その後にnumbersが変更されても、listResult側の内容は変わりません。 FirstOrDefaultの注意:
見つからない場合は「型のデフォルト値」が返ります。
intなら0、stringならnullです。
「見つからないこと」を区別したいなら、int?(nullable)にする・またはAny()で先に確認するのが安全です。
よくある実行例:
foreach (var n in even) { System.Diagnostics.Debug.WriteLine(n); }
または var listResult = even.ToList(); のように ToList() で実体化してから使います。
第6章:UI操作リファレンス
メッセージボックスとダイアログ
JavaScript の alert() や confirm() に相当する操作は、WinForms では
MessageBox.Show() を使用します。
| JS / Web | C# / WinForms | 備考 |
|---|---|---|
alert("Hello") |
MessageBox.Show("Hello") |
最も単純な通知。 |
confirm("OK?") |
var res = MessageBox.Show("OK?", "確認", MessageBoxButtons.YesNo); |
戻り値で Yes か No かを判定。 |
// 使用例:Yes/No 判定
DialogResult result = MessageBox.Show(
"ファイルを削除しますか?",
"削除確認",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning
);
if (result == DialogResult.Yes)
{
// はい の時の処理
}
else
{
// いいえ の時の処理
}
ボタン:Click
WinForms の基本は「イベント」で、Android の setOnClickListener に相当します。
もっとも多いのが Button.Click です。
// Designer で btnSave を置いた想定
private void btnSave_Click(object sender, EventArgs e)
{
MessageBox.Show("保存しました");
}
// イベントをコードで登録する場合(Form_Load などで)
private void Form1_Load(object sender, EventArgs e)
{
btnSave.Click += btnSave_Click; // 追加
// btnSave.Click -= btnSave_Click; // 解除
}
テキスト入力:TextBox / RichTextBox
1行入力は TextBox、複数行(テキストエリア相当)は TextBox.Multiline=true または
RichTextBox を使います。
// 値のセット/取得
textBoxName.Text = "Taro";
string name = textBoxName.Text;
// 変更イベント(入力に応じて動く)
private void textBoxName_TextChanged(object sender, EventArgs e)
{
lblPreview.Text = textBoxName.Text;
}
// 複数行にする(Designer でも可)
textBoxMemo.Multiline = true;
textBoxMemo.ScrollBars = ScrollBars.Vertical;
チェックボックス:CheckedChanged
ON/OFF の状態は CheckBox.Checked です。イベントは CheckedChanged が一般的です。
// 値のセット/取得
checkBoxEnabled.Checked = true;
bool enabled = checkBoxEnabled.Checked;
// 状態が変わったとき
private void checkBoxEnabled_CheckedChanged(object sender, EventArgs e)
{
textBoxName.Enabled = checkBoxEnabled.Checked;
}
ラジオボタン:CheckedChanged
RadioButton は「グループ内で1つだけ選択」です。グループ化は通常、同じ親コンテナ(Panel/GroupBox)に入れます。
// 選択状態(Checked)で判断
private void radioMale_CheckedChanged(object sender, EventArgs e)
{
if (radioMale.Checked)
{
lblGender.Text = "Male";
}
}
// 取得例
string gender = radioMale.Checked ? "Male" : "Female";
リスト:ListBox / ComboBox / ListView
単純な一覧は ListBox、ドロップダウンは ComboBox。
表形式の一覧(列あり)が必要なら ListView または DataGridView を使います。
// ListBox
listBox1.Items.Clear();
listBox1.Items.Add("A");
listBox1.Items.Add("B");
private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
{
string? selected = listBox1.SelectedItem?.ToString();
lblStatus.Text = selected ?? "(none)";
}
// ComboBox
comboBox1.Items.AddRange(new object[] { "Small", "Medium", "Large" });
comboBox1.SelectedIndex = 0;
string size = comboBox1.SelectedItem?.ToString() ?? "Small";
カレンダー:DateTimePicker / MonthCalendar
日付入力は DateTimePicker が定番です(Android の DatePicker に近い)。
値は Value(DateTime)で取得します。
// DateTimePicker
dateTimePicker1.Value = DateTime.Today;
DateTime d = dateTimePicker1.Value;
private void dateTimePicker1_ValueChanged(object sender, EventArgs e)
{
lblDate.Text = dateTimePicker1.Value.ToString("yyyy-MM-dd");
}
UpDown:NumericUpDown
数値入力は NumericUpDown を使うと安全です(文字→数値変換の例外を避けやすい)。
値は Value(decimal)です。
// セット/取得
numericTimeout.Minimum = 0;
numericTimeout.Maximum = 600000;
numericTimeout.Value = 5000; // 表示したい値(ms)
int timeoutMs = (int)numericTimeout.Value; // 取得(必要ならキャスト)
表:DataGridView(概要)
DataGridView は UI コンポーネント(表表示)です。
ただし「表示するデータ」は別オブジェクト(データソース)に持たせ、DataGridView にバインドします。
- DataTable:列・行を持つ “表データ” を表現するクラス(System.Data)
- BindingList<T>:T のリストを “データバインド可能” にしたクラス(System.ComponentModel)
// DataTable を表示(列が可変なときに便利)
DataTable table = new DataTable();
table.Columns.Add("Id", typeof(int));
table.Columns.Add("Name", typeof(string));
table.Rows.Add(1, "Taro");
dataGridView1.AutoGenerateColumns = true;
dataGridView1.DataSource = table;
// BindingList<T> を表示(列が固定で型安全に扱いたいとき)
public class UserRow { public int Id { get; set; } public string Name { get; set; } = ""; }
var list = new BindingList<UserRow>(new List<UserRow>
{
new UserRow { Id = 1, Name = "Taro" }
});
dataGridView1.DataSource = list;
DataGridView:編集、行追加、選択行取得(業務でよく使う操作)
編集検知:CellValueChanged
private bool _dirty = false;
private void dataGridView1_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
_dirty = true;
lblStatus.Text = "変更あり";
}
// チェックボックス列など、編集確定を即座に反映したい場合
private void dataGridView1_CurrentCellDirtyStateChanged(object sender, EventArgs e)
{
if (dataGridView1.IsCurrentCellDirty)
{
dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit);
}
}
行追加:DataTable / BindingList それぞれ
// A) DataTable の場合(Rows.Add)
var table = (DataTable)dataGridView1.DataSource;
table.Rows.Add(3, "Saburo");
// B) BindingList<T> の場合(Add)
var list = (BindingList<UserRow>)dataGridView1.DataSource;
list.Add(new UserRow { Id = 3, Name = "Saburo" });
選択行取得:CurrentRow / SelectedRows
// 現在行(フォーカスがある行)
var row = dataGridView1.CurrentRow;
if (row != null)
{
var id = row.Cells["Id"].Value; // 列名で参照(AutoGenerateColumns時)
var name = row.Cells["Name"].Value;
}
// 選択行(複数選択にも対応)
foreach (DataGridViewRow r in dataGridView1.SelectedRows)
{
var id = r.Cells["Id"].Value;
var name = r.Cells["Name"].Value;
}
「一覧で編集 → 変更行だけ UPDATE」する場合は、
(1)
CellValueChanged で変更フラグ、(2) 保存ボタンでDB反映、が定番です。
フォーム遷移:Form1 から Form2 に切り替える
WinForms では、画面(Activity)の切り替えに相当するものが フォーム(Form) です。
Android の startActivity() に近い考え方で、
Form1 から Form2 のインスタンスを生成して表示します。
構成
- Form1:起動時に表示されるメイン画面(ボタンあり)
- Form2:Form1 のボタン操作で表示される画面
Form2 側のコード(特別な処理は不要)
// Form2.cs
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
}
}
Form1 側:ボタンを押して Form2 を表示
// Form1.cs
private void btnOpenForm2_Click(object sender, EventArgs e)
{
// Form2 のインスタンスを生成
var form2 = new Form2();
// 1) モーダル表示(元の画面を操作させない)
// form2.ShowDialog();
// 2) モードレス表示(元の画面も操作可能)
form2.Show();
}
Show():モードレス。Form1 と Form2 を同時に操作できるShowDialog():モーダル。Form2 を閉じるまで Form1 は操作不可
Form1 を非表示にして Form2 へ切り替える例
「画面を完全に切り替えたい」場合は、Form1 を非表示にしてから Form2 を表示します。 Form2 が閉じられたら Form1 を再表示する、という流れが一般的です。
private void btnOpenForm2_Click(object sender, EventArgs e)
{
using (var form2 = new Form2())
{
this.Hide(); // Form1 を隠す
form2.ShowDialog(); // Form2 を表示(モーダル)
this.Show(); // Form2 が閉じたら Form1 を再表示
}
}
- Activity → Form
- startActivity() → new Form().Show() / ShowDialog()
- finish() → Form.Close()
第7章:非同期処理とマルチスレッド(Android Java対比)
WinForms では、ボタン押下や画面描画などの UI 処理は UIスレッド(メインスレッド)で実行されます。 ここで重い処理(ファイルI/O、ネットワーク、画像処理など)を実行すると、フォームが固まり「応答なし」になります。 そのため 重い処理はバックグラウンドで実行し、完了後に UIスレッドへ結果を戻すのが基本です。
スレッドの基本とライフサイクル
「スレッドを作成 → 実行 → 終了するまで」の流れは Java も C# も同じです。
スレッドは run 相当の処理が終われば自然に終了します(明示的な destroy は不要)。
「終了を待つ」必要がある場合は Join(Java)/ Join()(C#)を使います。
// 1) 作成
Thread t = new Thread(() -> {
doWork(); // 2) 実行される処理
});
// 3) 開始
t.start();
// 4) 終了待ち(必要な場合のみ)
t.join(); // ここで doWork() の終了まで待つ
// 1) 作成
var t = new Thread(() =>
{
DoWork(); // 2) 実行される処理
});
// 3) 開始
t.Start();
// 4) 終了待ち(必要な場合のみ)
t.Join(); // ここで DoWork() の終了まで待つ
WinForms で
Thread を直接使うこともできますが、
.NET では一般的に Task / async / await を使う方が簡潔で安全です(後述)。
排他制御:synchronized / lock
複数スレッドが同じデータ(例:カウンタ、リスト、辞書、状態フラグ)を同時に更新すると、 更新が欠けたり順序が壊れたりします(レースコンディション)。 これを防ぐため、「この区間は同時に1スレッドだけ」という排他区間を作ります。
lock の「object」は何なのか?(最重要)
C# の lock (obj) の obj は
ロックの目印(ロック対象)として使う参照型オブジェクトです。
「lock専用のオブジェクト」を用意するのが基本で、典型的にはクラス内に
private readonly object _sync = new object(); を持ちます。
やってはいけない例:lock(this)(外部からも同じインスタンスでロックされ得る)、
lock("文字列")(intern により共有され得る)など。
private final Object lockObj = new Object();
private int counter = 0;
public void inc() {
synchronized (lockObj) {
counter++;
}
}
private readonly object _sync = new object();
private int _counter = 0;
public void Inc()
{
lock (_sync)
{
_counter++;
}
}
lock が必要な「一連の例」(悪い例→良い例)
次のコードは、複数スレッドから同時に呼ばれるとカウンタ更新が壊れます。
// 悪い例:競合が起きる
private int _count = 0;
public void Add()
{
_count++; // これは「読み取り→+1→書き込み」の3段階で、途中で割り込まれる
}
lock を使うと「1スレッドずつ」にできます。
// 良い例:排他制御
private readonly object _sync = new object();
private int _count = 0;
public void Add()
{
lock (_sync)
{
_count++;
}
}
Interlocked.Increment(ref _count) はロックより軽量な場合があります。
ただし「複数の値をまとめて整合させる」場合は lock が必要です。
Task / async / await(推奨モデル)
Java では Future / ExecutorService を使って非同期処理を組み立てます。
.NET ではそれに相当するのが Task です。
さらに async / await により「非同期なのに同期っぽく書ける」形になります。
// 返り値ありの非同期メソッド
private async Task<int> CalculateAsync()
{
// 例:I/O待ちを模した遅延
await Task.Delay(500);
return 123;
}
// 呼び出し側
private async Task UseAsync()
{
int value = await CalculateAsync();
Console.WriteLine(value);
}
await を使うメソッドは、基本的にそのメソッド自体も async にする必要があります。
その結果「呼び出し元も async、その呼び出し元も async...」と連鎖します。これは自然な設計です。
- 原則:
awaitを書くメソッドはasync(返り値はTask/Task<T>)。 - 連鎖の止め方:最上位では「イベントハンドラ」などが受け口になります(WinForms のクリックイベントなど)。
- 例外:同期で待つ
.Result/.Wait()は UI でデッドロック要因になり得るため基本は避けます。
UIスレッドと同期(WinForms)
WinForms のコントロール(Label, TextBox, Button 等)は UIスレッドからのみ更新可能です。
バックグラウンドから直接触ると例外(Cross-thread operation)になり得ます。
そのため Invoke/BeginInvoke で UI スレッドへ戻します。
// 重い処理をバックグラウンドで実行し、UIへ戻す例
private void btnRun_Click(object sender, EventArgs e)
{
btnRun.Enabled = false;
label1.Text = "実行中...";
Task.Run(() =>
{
// バックグラウンド処理
int result = HeavyWork();
// UIスレッドへ戻して画面更新
this.Invoke(() =>
{
label1.Text = $"完了: {result}";
btnRun.Enabled = true;
});
});
}
private int HeavyWork()
{
Thread.Sleep(1000);
return 42;
}
イベントハンドラを
async void にして await を使うと、
「UIに戻す処理」を手書きしなくて済むケースもあります(次節で紹介)。
キャンセルと進捗(実務パターン)
実務では「長い処理を途中で止めたい」「進捗を表示したい」が頻出です。
.NET の定番は CancellationToken と IProgress<T> です。
// フィールドにキャンセル用トークンを保持
// - Start ボタンを押したときに作成
// - Cancel ボタンで Cancel() するために、フィールドに保持する
private CancellationTokenSource? _cts;
// Start ボタンのクリックイベント(イベントハンドラなので async void でOK)
private async void btnStart_Click(object sender, EventArgs e)
{
// 連打防止
btnStart.Enabled = false;
btnCancel.Enabled = true;
// キャンセルの受け口を作る
_cts = new CancellationTokenSource();
// 進捗報告:Progress<T> は WinForms の UI スレッドに戻してくれる
var progress = new Progress<int>(p =>
{
// UI 更新(ここは UI スレッド上)
progressBar1.Value = p;
label1.Text = $"進捗: {p}%";
});
try
{
// 長い処理を開始(途中キャンセル可能)
await LongWorkAsync(progress, _cts.Token);
label1.Text = "完了";
}
catch (OperationCanceledException)
{
// token.ThrowIfCancellationRequested() などで投げられる
label1.Text = "キャンセルしました";
}
finally
{
// UI を元に戻す
btnStart.Enabled = true;
btnCancel.Enabled = false;
_cts = null;
}
}
// Cancel ボタン:キャンセル要求を出す(処理側が token を見て止まる)
private void btnCancel_Click(object sender, EventArgs e)
{
_cts?.Cancel();
}
// 実際の長い処理(例:I/O待ちを模したループ)
// - token を定期的にチェックしてキャンセルできるようにする
// - progress.Report で進捗を UI へ通知
private async Task LongWorkAsync(IProgress<int> progress, CancellationToken token)
{
for (int i = 0; i <= 100; i += 10)
{
// ここでキャンセル要求が来ていないか確認
token.ThrowIfCancellationRequested();
// 例:待ちが発生する処理(ネットワーク/ファイルI/O等の代わり)
await Task.Delay(200, token);
// 進捗を通知
progress.Report(i);
}
}
第8章:ファイル・ディレクトリ・I/O
デスクトップアプリでは「設定ファイル」「ログ」「一時ファイル」「ユーザーが選んだファイル」など、 ファイル・ディレクトリ操作は避けて通れません。 この章では、失敗しやすいI/Oを安全に書くための型(try/catch, using, Path)を中心に整理します。
ファイル読み書きと例外設計
まずは最もシンプルな読み書きです。小さめのテキストファイルなら File.ReadAllText が最短です。
ただし「ファイルがない」「権限がない」「他プロセスがロック中」などで失敗するため、例外処理が前提になります。
try
{
string text = File.ReadAllText("sample.txt", Encoding.UTF8);
File.WriteAllText("output.txt", text, Encoding.UTF8);
}
catch (UnauthorizedAccessException ex)
{
MessageBox.Show("権限がありません: " + ex.Message);
}
catch (IOException ex)
{
MessageBox.Show("I/Oエラー: " + ex.Message);
}
File.ReadAllText や File.WriteAllText は、
内部でファイルを開いて読み書きし、処理が終わると自動的に Close / Disposeします。
そのため呼び出し側で Close() を書く必要はありません。
これは「単発で完結する I/O」を簡潔に書ける高水準 API です。
大きなファイルを少しずつ処理したい場合は、StreamReader / StreamWriter を
using と併用します。
using とリソース管理
StreamReader / FileStream などのストリームは OS のハンドルを保持します。
これを確実に解放するのが using です(Java の try-with-resources に近い)。
// using ブロック(古典)
using (var reader = new StreamReader("sample.txt", Encoding.UTF8))
{
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
Console.WriteLine(line);
}
}
// using 宣言(モダン:C#8+)
using var writer = new StreamWriter("log.txt", append: true, Encoding.UTF8);
writer.WriteLine(DateTime.Now + " start");
結論:必須ではありません(この例は「ログ出力は失敗しても続行したい」ケースを想定)。 ただし要件次第で try/catch を付けます。
- ログ・一時ファイル:失敗しても致命的でないことが多い → try/catch を省略 or 失敗を握りつぶす設計もあり
- ユーザーが選んだファイル保存:失敗理由を表示したい → try/catch が必要
重要:using は「Close を保証」するだけで、例外を捕まえる仕組みではありません。
例外が発生しても、using のスコープを抜けると必ず
Dispose() が呼ばれます。
これにより「ファイルが閉じられず次に開けない」事故を防げます。
パス結合・アプリ用フォルダ・一時ファイル
パスは OS ごと・環境ごとに区切り文字が違い得るため、文字列連結は避けます。
必ず Path.Combine を使います。
// 例:ドキュメント配下に config.ini
string configPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
"config.ini"
);
アプリ専用の保存先が必要な場合、ユーザープロファイル配下の AppData を使うことが多いです。
// 例:AppData\Roaming\YourApp\settings.ini
string appDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"YourApp"
);
Directory.CreateDirectory(appDir); // すでに存在してもOK
string settingsPath = Path.Combine(appDir, "settings.ini");
一時ファイルは Path.GetTempFileName() または Path.GetTempPath() を使います。
// 一時ファイルを作成して書き込む
string tempFile = Path.GetTempFileName();
File.WriteAllText(tempFile, "temp data", Encoding.UTF8);
// 一時ディレクトリ配下に任意名で作る
string tempDir = Path.GetTempPath();
string tempPath = Path.Combine(tempDir, "yourapp_" + Guid.NewGuid() + ".tmp");
ディレクトリ操作(作成・列挙・移動・削除)
ファイルだけでなく、ディレクトリ(フォルダ)も同様に失敗し得ます(権限、ロック、存在しない等)。
.NET では主に Directory / DirectoryInfo を使います。
// 作成(既に存在していてもOK)
Directory.CreateDirectory("data");
// 列挙
foreach (var f in Directory.EnumerateFiles("data", "*.txt"))
{
Console.WriteLine(f);
}
// 移動(=名前変更)
Directory.Move("data", "data_old");
// 削除(中身も含めて削除するなら true)
Directory.Delete("data_old", recursive: true);
作業ディレクトリ(Working Directory)は変えるべき?
結論として、アプリ全体の作業ディレクトリを Directory.SetCurrentDirectory で頻繁に変更する運用は
あまり推奨されません。理由は「相対パスの基準が変わって事故りやすい」ためです。
代わりに、ベースディレクトリを明示して Path.Combine で組み立てるのが安全です。
// ベースを明示して組み立てる(推奨)
string baseDir = AppContext.BaseDirectory; // 実行ファイルの場所
string path = Path.Combine(baseDir, "data", "settings.ini");
INIファイルの読み込み(実用例)
INI は「単純で軽い設定ファイル」として今でも現場で使われます。 ここでは WinForms でありがちな「設定を読み込んで画面に反映する」例を示します。
INI例
[app]
baseUrl=https://api.example.com
timeoutMs=5000
[user]
name=Taro
age=20
読み込みコード(セクション対応の簡易パーサ)
public static Dictionary<string, Dictionary<string, string>> LoadIni(string path)
{
var result = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
string section = "default";
result[section] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var raw in File.ReadAllLines(path, Encoding.UTF8))
{
var line = raw.Trim();
if (line.Length == 0 || line.StartsWith(";") || line.StartsWith("#")) continue;
if (line.StartsWith("[") && line.EndsWith("]"))
{
section = line.Substring(1, line.Length - 2).Trim();
if (!result.ContainsKey(section))
result[section] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
continue;
}
int eq = line.IndexOf('=');
if (eq < 0) continue;
string key = line.Substring(0, eq).Trim();
string val = line.Substring(eq + 1).Trim();
result[section][key] = val;
}
return result;
}
WinForms での使用例
try
{
var ini = LoadIni("settings.ini");
string baseUrl = ini["app"]["baseUrl"];
int timeoutMs = int.Parse(ini["app"]["timeoutMs"]);
textBoxBaseUrl.Text = baseUrl;
numericTimeout.Value = timeoutMs;
numericTimeout とは?
これは WinForms の NumericUpDown(数値入力コントロール)を想定した変数名です。
数値は Value プロパティ(型は decimal)で扱います。
numericTimeout.Value = timeoutMs; は「画面の数値入力欄に timeoutMs を表示する」処理です。
(必要なら (decimal)timeoutMs にキャストします)
}
catch (Exception ex)
{
MessageBox.Show("設定読み込みに失敗: " + ex.Message);
}
第9章:REST API 通信
WinForms でも「REST API を叩いて結果を表示する」ケースは非常に多いです。 ここでは 通信 → ステータス判定 → JSON/XML処理 → UI反映 までを一連の流れとして説明します。
HttpClient の基本と設計
.NET の HTTP 通信は基本的に HttpClient を使います。
重要なのは「毎回 new して捨てない」ことです(ソケット枯渇の原因になる)。
WinForms アプリなら、フォームやサービスクラスに 1つ保持して使い回すのが典型です。
// 例:フォームに 1 つ持つ(シンプル)
private readonly HttpClient _http = new HttpClient
{
Timeout = TimeSpan.FromSeconds(10)
};
ステータスコード別の処理
response.IsSuccessStatusCode は HTTP 200〜299(2xx)なら成功として true になります。
そのため、POST 成功の 201 Created、DELETE 成功の 204 No Content も成功扱いです。
- 例:
200 OK,201 Created,202 Accepted,204 No Content→ 成功(true) - 例:
400,401,403,404,500→ 失敗(false)
IsSuccessStatusCode(200〜299)だけで済む場面もありますが、
実務では 400/401/403/404/409/500 などを分岐し、ユーザーに適切に知らせます。
HttpResponseMessage res = await _http.GetAsync(url);
switch ((int)res.StatusCode)
{
case 200:
// OK
break;
case 400:
// BadRequest: 入力が不正
break;
case 401:
case 403:
// 認証・認可
break;
case 404:
// NotFound
break;
default:
// 500系など
break;
}
JSON / XML の扱い
JSON は System.Text.Json(標準)を使うのが基本です。
DTO(受信・送信専用クラス)を作って deserialize すると安全です。
using System.Text.Json;
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
string json = "{\"id\":1,\"name\":\"Taro\"}";
UserDto user = JsonSerializer.Deserialize<UserDto>(json)!;
string outJson = JsonSerializer.Serialize(user);
JSON 通信の DTO には通常不要です。
System.Text.Json / Newtonsoft.Json は、主にプロパティをリフレクションで読み取り
JSON と相互変換するため、[Serializable](バイナリ直列化用)は参照されません。
JSON 側で名前が合わない場合などに使うのは [JsonPropertyName] のような
JSON 専用属性です。
DTO は「通信や保存のためにデータだけを持つクラス」です。
REST API から受け取る JSON の形に合わせてプロパティを定義し、
JsonSerializer.Deserialize<T>() で安全に型へ変換します。
- 利点:キー名の打ち間違いを減らせる/型変換エラーが早期に分かる/IDE補完が効く
- 注意:画面用のクラス(ViewModel)と混ぜない。DTO は「運ぶ」役割に徹する。
XML が必要な場合は XDocument(簡易)や XmlSerializer(DTO化)が選択肢です。
using System.Xml.Linq;
string xml = "<user><id>1</id><name>Taro</name></user>";
var doc = XDocument.Parse(xml);
string name = doc.Root?.Element("name")?.Value ?? "";
GET / POST / PUT / DELETE
REST の基本は「取得(GET)」「作成(POST)」「更新(PUT/PATCH)」「削除(DELETE)」です。
JSON を送る場合は StringContent を使い、Content-Type に application/json を指定します。
using System.Net.Http;
using System.Text;
// GET
string body = await _http.GetStringAsync(url);
// POST
string json = "{\"name\":\"Taro\"}";
var content = new StringContent(json, Encoding.UTF8, "application/json");
HttpResponseMessage postRes = await _http.PostAsync(url, content);
// PUT
HttpResponseMessage putRes = await _http.PutAsync(url, content);
// DELETE
HttpResponseMessage delRes = await _http.DeleteAsync(url);
WinForms:通信→UI反映の完全例(スレッド/例外/ステータス対応)
ここが実務の核心です。ボタンを押す → API を叩く → JSON を DTO に変換 → 画面に表示する。 さらに「通信中はボタンを無効化」「失敗時はメッセージ」「キャンセル可能」まで入れた例です。
private CancellationTokenSource? _apiCts;
private async void btnFetch_Click(object sender, EventArgs e)
{
btnFetch.Enabled = false;
btnCancelApi.Enabled = true;
labelStatus.Text = "通信中...";
_apiCts = new CancellationTokenSource();
try
{
string url = textBoxUrl.Text;
using var req = new HttpRequestMessage(HttpMethod.Get, url);
using HttpResponseMessage res = await _http.SendAsync(req, _apiCts.Token);
if (!res.IsSuccessStatusCode)
{
labelStatus.Text = $"失敗: {(int)res.StatusCode} {res.ReasonPhrase}";
return;
}
string json = await res.Content.ReadAsStringAsync(_apiCts.Token);
// JSON を画面に表示(まずは生文字)
textBoxBody.Text = json;
// DTO に変換したい場合
// var dto = JsonSerializer.Deserialize<UserDto>(json);
labelStatus.Text = "完了";
}
catch (OperationCanceledException)
{
labelStatus.Text = "キャンセルしました";
}
catch (HttpRequestException ex)
{
labelStatus.Text = "通信エラー: " + ex.Message;
}
catch (Exception ex)
{
labelStatus.Text = "予期せぬエラー: " + ex.Message;
}
finally
{
btnFetch.Enabled = true;
btnCancelApi.Enabled = false;
_apiCts = null;
}
}
private void btnCancelApi_Click(object sender, EventArgs e)
{
_apiCts?.Cancel();
}
- UI イベントは
async voidでOK(イベントハンドラに限る) - 通信中はボタン無効化(多重送信防止)
CancellationTokenでキャンセル可能- ステータスコードは
IsSuccessStatusCodeでまず判定 - JSON は「生文字で確認 → DTO化」の順がデバッグしやすい
第10章:DBアクセス(Oracle / MySQL / PostgreSQL / SQLite)
WinForms での DB アクセスは、基本的に ADO.NET(接続・コマンド・リーダー)で行います。 Java でいう JDBC に相当し、考え方は「接続(Connection)→ SQL 実行(Command)→ 結果取得(Reader)」です。
10.1 まず共通:ADO.NET の基本形
DB の種類が変わっても、コードの骨格はほぼ共通です。違いは主に「使うプロバイダ」と「接続文字列」などです。
// 共通の流れ(概念)
// 1) Connection を作る
// 2) Open
// 3) Command に SQL とパラメータをセット
// 4) SELECT なら Reader で行を読む / DDLやINSERT/UPDATEなら ExecuteNonQuery
// 5) Close(using で自動)
10.2 どこが違う?(Oracle / MySQL / PostgreSQL / SQLite)
- プロバイダ(ライブラリ):DBごとに異なる(下記)
- 接続文字列:書式が異なる
- パラメータ記法:
@name/:nameなど(プロバイダ依存) - SQL方言:AUTO INCREMENT、LIMIT/OFFSET、日付関数などが異なる
主要プロバイダ(C#)
- Oracle:
Oracle.ManagedDataAccess - MySQL:
MySqlConnector(推奨) - PostgreSQL:
Npgsql - SQLite:
Microsoft.Data.Sqlite
接続まで行う「完全なサンプルコード」(DB別)
まずは「接続文字列を指定して Open する」までを、DBごとに そのまま貼って動かせる形で示します。 (※ 実行には各プロバイダの NuGet 参照が必要です)
Oracle(Oracle.ManagedDataAccess)
using Oracle.ManagedDataAccess.Client;
public static void Oracle_ConnectSample()
{
// 接続文字列例(環境に合わせて変更)
string cs = "User Id=scott;Password=tiger;Data Source=localhost:1521/ORCLPDB1;";
try
{
using var conn = new OracleConnection(cs);
conn.Open(); // ここで接続
Console.WriteLine("Oracle connected");
}
catch (Exception ex)
{
Console.WriteLine("Oracle connect failed: " + ex.Message);
}
}
MySQL(MySqlConnector)
using MySqlConnector;
public static void MySql_ConnectSample()
{
string cs = "Server=localhost;Port=3306;Database=testdb;User ID=root;Password=pass;SslMode=None;";
try
{
using var conn = new MySqlConnection(cs);
conn.Open();
Console.WriteLine("MySQL connected");
}
catch (Exception ex)
{
Console.WriteLine("MySQL connect failed: " + ex.Message);
}
}
PostgreSQL(Npgsql)
using Npgsql;
public static void Postgres_ConnectSample()
{
string cs = "Host=localhost;Port=5432;Database=testdb;Username=postgres;Password=pass;";
try
{
using var conn = new NpgsqlConnection(cs);
conn.Open();
Console.WriteLine("PostgreSQL connected");
}
catch (Exception ex)
{
Console.WriteLine("PostgreSQL connect failed: " + ex.Message);
}
}
SQLite(Microsoft.Data.Sqlite)
using Microsoft.Data.Sqlite;
public static void Sqlite_ConnectSample()
{
string cs = "Data Source=app.db;";
try
{
using var conn = new SqliteConnection(cs);
conn.Open();
Console.WriteLine("SQLite connected");
}
catch (Exception ex)
{
Console.WriteLine("SQLite connect failed: " + ex.Message);
}
}
DBごとに「どこが違う?」を具体的に
- 接続クラス名:OracleConnection / MySqlConnection / NpgsqlConnection / SqliteConnection のように型が違う
- 接続文字列のキー:Server/Host、User ID/Username、Data Source など書式が違う
- パラメータ記法:
@name(多い)/:name(Oracleでよく見る)などプロバイダ依存 - SQL方言:AUTO INCREMENT、LIMIT/OFFSET、日付関数などがDBごとに違う
- 最後に挿入したID:取得方法が DB / SQL によって違う(例:SQLiteは last_insert_rowid()、Postgresは RETURNING 等)
はい、最終的にやっていること(接続文字列で接続して Open する)は同じです。 違いは「書き方の目的」です。
- DB別サンプル:
OracleConnectionなど “DB専用型” を直接 new して接続する(最短で分かりやすい) - CreateConnection:設定(INI 等)で DB を切り替えたいときに、呼び出し側を
DbConnectionで共通化する
どのタイミングで呼ぶ?
典型的には「検索ボタン押下」や「画面ロード」など、DB操作を開始する直前に
CreateConnection(provider, cs) で Connection を作り、すぐ Open してクエリを実行します。
(このとき using で確実に Close させます)
多くの .NET プロバイダでは
@name が使えますが、Oracle は :name が一般的です。
「記法はプロバイダ依存」と覚えると混乱が減ります。
10.3 DDL(テーブル作成など)を実行する
CREATE TABLE などの DDL は ExecuteNonQuery() を使います(戻り値は影響行数)。
// SQLite の例(Microsoft.Data.Sqlite)
using Microsoft.Data.Sqlite;
using var conn = new SqliteConnection("Data Source=app.db;");
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);";
cmd.ExecuteNonQuery();
10.4 SELECT の実行(fetch ループ)
SELECT は ExecuteReader() を使い、行を 1 行ずつ読みます(JDBC の ResultSet に近い)。
// SQLite の例(他DBでも構造は同じ)
using var cmd2 = conn.CreateCommand();
cmd2.CommandText = "SELECT id, name FROM users WHERE name LIKE @name ORDER BY id;";
cmd2.Parameters.AddWithValue("@name", "T%"); // パラメータで安全に渡す
using var reader = cmd2.ExecuteReader();
while (reader.Read())
{
int id = reader.GetInt32(0); // 0列目
string name = reader.GetString(1); // 1列目
Console.WriteLine($"{id}: {name}");
}
reader.GetInt32(0) のような 列番号(ordinal)アクセスは高速で、サンプルでもよく使われます。
ただし、列順が変わると壊れるため、実務では次の方法もよく使います。
- 列名→番号を一度だけ取得:
int idx = reader.GetOrdinal("id"); - インデクサ:
var v = reader["name"];(objectで返るので型変換が必要)
// 列名で安全に(おすすめ:GetOrdinalを一回だけ)
int idIdx = reader.GetOrdinal("id");
int nameIdx = reader.GetOrdinal("name");
while (reader.Read())
{
int id = reader.GetInt32(idIdx);
string name = reader.GetString(nameIdx);
}
結論として、「列が固定」なら ordinal でOK、 「列順が変わり得る」なら GetOrdinal が安全です。
10.5 INSERT / UPDATE / DELETE の実行
更新系 SQL も ExecuteNonQuery() を使います。
ユーザー入力は必ずパラメータで渡してください(SQLインジェクション対策)。
// INSERT
using var ins = conn.CreateCommand();
ins.CommandText = "INSERT INTO users(name) VALUES(@name);";
ins.Parameters.AddWithValue("@name", "Taro");
int rows = ins.ExecuteNonQuery(); // 影響行数
// UPDATE
using var upd = conn.CreateCommand();
upd.CommandText = "UPDATE users SET name=@name WHERE id=@id;";
upd.Parameters.AddWithValue("@name", "Jiro");
upd.Parameters.AddWithValue("@id", 1);
upd.ExecuteNonQuery();
// DELETE
using var del = conn.CreateCommand();
del.CommandText = "DELETE FROM users WHERE id=@id;";
del.Parameters.AddWithValue("@id", 1);
del.ExecuteNonQuery();
10.6 トランザクション(複数SQLをまとめて確定)
複数の更新を「全部成功したら確定、途中で失敗したら巻き戻し」にしたい場合はトランザクションを使います。
using var tx = conn.BeginTransaction();
try
{
using var c1 = conn.CreateCommand();
c1.Transaction = tx;
c1.CommandText = "INSERT INTO users(name) VALUES(@name);";
c1.Parameters.AddWithValue("@name", "A");
c1.ExecuteNonQuery();
using var c2 = conn.CreateCommand();
c2.Transaction = tx;
c2.CommandText = "INSERT INTO users(name) VALUES(@name);";
c2.Parameters.AddWithValue("@name", "B");
c2.ExecuteNonQuery();
tx.Commit();
}
catch
{
tx.Rollback();
throw;
}
WinForms では DB 通信も I/O なので、重いクエリは
Task.Run や async と併用し、
UI を固めないようにします(第7章のパターンと同じです)。
10.7 接続を使い回す設計(WinFormsでの現実解)
DB 接続(DbConnection)は「開く/閉じる」が比較的高コストです。
とはいえ WinForms の常駐アプリで 常に接続しっぱなしにすると、
ネットワーク切断や DB 再起動などの障害に弱くなります。
実務で多いのは次の設計です:
- 接続文字列とプロバイダを1箇所に集約(DBごとの違いを隠蔽)
- クエリ単位で Open/Close(ただし
usingで確実に閉じる) - 必要に応じて コネクションプール(プロバイダ側)が効くので、毎回 new でも性能は出やすい
プロバイダを抽象化する(DbConnection/DbCommand)
using System.Data.Common;
// 接続を作る関数を1箇所に集約する
public static DbConnection CreateConnection(string provider, string connectionString)
{
// provider 例:
// "Microsoft.Data.Sqlite"
// "Npgsql"
// "MySqlConnector"
// "Oracle.ManagedDataAccess.Client"
var factory = DbProviderFactories.GetFactory(provider);
var conn = factory.CreateConnection()!;
conn.ConnectionString = connectionString;
return conn;
}
「接続を使い回す」と言っても、WinForms では “接続インスタンスを永遠に保持する”より、“作り方を共通化する”方が事故が少ないです。
(※ SQLite のようなファイルDBは「同一接続を使い回す」設計もよくあります)
10.8 非同期(ExecuteReaderAsync など)
DB も I/O なので、UI を固めないために非同期 API を使うのが基本です。
ADO.NET の多くは OpenAsync, ExecuteReaderAsync, ExecuteNonQueryAsync を提供します。
(プロバイダによって対応状況は異なります)
using System.Data.Common;
// SELECT(非同期): reader を while で読み進める
public static async Task<List<(int Id, string Name)>> LoadUsersAsync(
DbConnection conn,
string namePrefix,
CancellationToken token)
{
await conn.OpenAsync(token);
await using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT id, name FROM users WHERE name LIKE @name ORDER BY id;";
var p = cmd.CreateParameter();
p.ParameterName = "@name";
p.Value = namePrefix + "%";
cmd.Parameters.Add(p);
var result = new List<(int, string)>();
await using var reader = await cmd.ExecuteReaderAsync(token);
while (await reader.ReadAsync(token))
{
int id = reader.GetInt32(0);
string name = reader.GetString(1);
result.Add((id, name));
}
return result;
}
非同期でも「UI へ反映する処理」は UI スレッド上で行います。
WinForms のイベントハンドラを
async void にして await するのが定番です(第7章参照)。
10.9 DataGridView に表示する例(実務の最短ルート)
WinForms でテーブル表示に使う代表的なコントロールが DataGridView です。
ここでは「DBから非同期で取得 → DataTable に詰める → DataGridView にバインド」という
現場で最も多い流れを示します。
フォーム側:ボタンで読み込み→表示
using System.Data;
using System.Data.Common;
// 例:フォームに置いたコントロール
// - dataGridView1 : DataGridView
// - btnLoad : Button
// - txtPrefix : TextBox(名前の前方一致)
// - lblStatus : Label
private readonly string _provider = "Microsoft.Data.Sqlite";
private readonly string _connStr = "Data Source=app.db;";
private async void btnLoad_Click(object sender, EventArgs e)
{
btnLoad.Enabled = false;
lblStatus.Text = "読み込み中...";
try
{
string prefix = txtPrefix.Text;
using var conn = CreateConnection(_provider, _connStr);
// 1) DBから取得(非同期)
DataTable table = await LoadUsersAsTableAsync(conn, prefix, CancellationToken.None);
// 2) DataGridView にバインド
dataGridView1.AutoGenerateColumns = true;
dataGridView1.DataSource = table;
lblStatus.Text = $"件数: {table.Rows.Count}";
}
catch (Exception ex)
{
lblStatus.Text = "失敗: " + ex.Message;
}
finally
{
btnLoad.Enabled = true;
}
}
private static async Task<DataTable> LoadUsersAsTableAsync(
DbConnection conn,
string prefix,
CancellationToken token)
{
await conn.OpenAsync(token);
await using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT id, name FROM users WHERE name LIKE @name ORDER BY id;";
var p = cmd.CreateParameter();
p.ParameterName = "@name";
p.Value = prefix + "%";
cmd.Parameters.Add(p);
var table = new DataTable();
await using var reader = await cmd.ExecuteReaderAsync(token);
// DataTable に列定義を作る
for (int i = 0; i < reader.FieldCount; i++)
{
table.Columns.Add(reader.GetName(i), reader.GetFieldType(i));
}
// 行を追加
while (await reader.ReadAsync(token))
{
var row = table.NewRow();
for (int i = 0; i < reader.FieldCount; i++)
{
row[i] = await reader.IsDBNullAsync(i, token) ? DBNull.Value : reader.GetValue(i);
}
table.Rows.Add(row);
}
return table;
}
DataGridView は
DataTable / BindingList<T> などにバインドできます。
DataTable の使いどころと注意点
- 向いている:列が可変/SELECTの列が変わる/汎用ビューア
- メリット:列を動的に作れて最短で表示できる
- デメリット:型が弱い(object中心)/コンパイル時の補完が効きにくい
BindingList<T> の使いどころ
- 向いている:列が固定/DTO(UserDto など)がある/型安全に扱いたい
- メリット:プロパティ名が列になる/型が効く/IDE補完が効く
- デメリット:列定義が固定になる(動的列には不向き)
BindingList<T> を使う例(DTOで型安全に)
// DTO(列が固定のときに強い)
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
// 取得した DTO のリストを BindingList にして DataGridView に表示
var users = new List<UserDto>
{
new UserDto { Id = 1, Name = "Taro" },
new UserDto { Id = 2, Name = "Jiro" }
};
var binding = new BindingList<UserDto>(users);
dataGridView1.AutoGenerateColumns = true;
dataGridView1.DataSource = binding;
- 「ツール的に何でも表示」→ DataTable
- 「業務画面で固定の一覧」→ BindingList<DTO>
列が可変(SELECT の列が変わる)な場合、DataTable が最短で扱いやすいです。
列が固定で DTO があるなら
BindingList<UserDto> の方が型安全になります。
10.10 INIでDB切替:完全フロー(INI → provider/cs → CreateConnection → CRUD)
ここでは「設定ファイル(INI)を切り替えるだけで、接続先DBを変えられる」構成を 1 セットで示します。 目的は、呼び出し側を DbConnection/DbCommand で共通化し、環境(Oracle/MySQL/PostgreSQL/SQLite)を 設定で切り替えられるようにすることです。
この方式と「接続まで行う完全サンプルコード」の違い
- 完全サンプルコード(DB別):学習用に最短で接続できる(DB専用型を直接 new)
- INI切替方式(抽象化):運用でDBを切替可能(呼び出し側は共通コード、設定で差し替え)
機能としては同じ(接続文字列で接続して SQL を実行)ですが、 「書き方の目的」が違うと捉えると理解しやすいです。
NuGet で必要なパッケージ(プロバイダ)
抽象化しても、実際の接続には各DBのプロバイダが必要です。プロジェクトに NuGet で追加します。 (Visual Studio:ソリューションエクスプローラー → プロジェクト右クリック → NuGet パッケージの管理)
- Oracle:
Oracle.ManagedDataAccess - MySQL:
MySqlConnector - PostgreSQL:
Npgsql - SQLite:
Microsoft.Data.Sqlite
① INI ファイル例(db.ini)
[db]
; provider は DbProviderFactories 用のプロバイダ名(例)
provider=Microsoft.Data.Sqlite
; 接続文字列
connectionString=Data Source=app.db;
② INI 読み込み → provider / connectionString を取得
public static Dictionary<string, string> LoadIni(string path)
{
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var raw in File.ReadAllLines(path, Encoding.UTF8))
{
var line = raw.Trim();
if (line.Length == 0 || line.StartsWith(";") || line.StartsWith("#")) continue;
if (line.StartsWith("[") && line.EndsWith("]")) continue; // セクションは今回は無視(簡易)
int eq = line.IndexOf('=');
if (eq <= 0) continue;
string key = line[..eq].Trim();
string val = line[(eq + 1)..].Trim();
dict[key] = val;
}
return dict;
}
③ provider とは何?(DbProviderFactories と抽象化の考え方)
provider は「どのDBのプロバイダ(ドライバ)を使うか」を示す識別子です。
Java の JDBC でいうドライバ名に近く、.NET では DbProviderFactories が
その識別子から Connection/Command を生成します。
DbProviderFactories.GetFactory(provider) が動くためには、該当プロバイダが
“Factory として登録されている”必要があります。環境によっては自動登録されない場合があり、
その場合は下の「手動登録」か、DB別の専用 Connection を直接 new する方式を使います。
④ CreateConnection(抽象化)と、呼び出し側(どのタイミングで呼ぶ?)
「どのタイミングで呼ぶ?」はシンプルです:DB処理を開始する直前です。 例:検索ボタン押下、画面ロード、保存ボタン押下など。
using System.Data.Common;
// Factory を使う方式(provider 文字列から Connection を作る)
public static DbConnection CreateConnection(string provider, string connectionString)
{
var factory = DbProviderFactories.GetFactory(provider);
var conn = factory.CreateConnection()!;
conn.ConnectionString = connectionString;
return conn;
}
// (任意)Factory が自動登録されない場合の“手動登録”例
public static void EnsureFactoriesRegistered()
{
DbProviderFactories.RegisterFactory("Microsoft.Data.Sqlite", Microsoft.Data.Sqlite.SqliteFactory.Instance);
DbProviderFactories.RegisterFactory("Npgsql", Npgsql.NpgsqlFactory.Instance);
DbProviderFactories.RegisterFactory("MySqlConnector", MySqlConnector.MySqlConnectorFactory.Instance);
}
// 呼び出し側(例:ボタン押下など)
public static void RunCrudFromIni()
{
// 0) (必要なら)Factory を登録
// EnsureFactoriesRegistered();
// 1) INI から設定取得
var ini = LoadIni("db.ini");
string provider = ini["provider"];
string cs = ini["connectionString"];
// 2) Connection を作成 → Open(この時点で “接続まで行う完全サンプル” と同じことをしている)
using DbConnection conn = CreateConnection(provider, cs);
conn.Open();
// 3) DDL(テーブル作成)
using (DbCommand ddl = conn.CreateCommand())
{
ddl.CommandText = @"
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);";
ddl.ExecuteNonQuery();
}
// 4) INSERT
using (DbCommand ins = conn.CreateCommand())
{
ins.CommandText = "INSERT INTO users(id, name) VALUES(@id, @name);";
var p1 = ins.CreateParameter(); p1.ParameterName = "@id"; p1.Value = 1;
var p2 = ins.CreateParameter(); p2.ParameterName = "@name"; p2.Value = "Taro";
ins.Parameters.Add(p1); ins.Parameters.Add(p2);
ins.ExecuteNonQuery();
}
// 5) SELECT(列名アクセス:GetOrdinal)
using (DbCommand sel = conn.CreateCommand())
{
sel.CommandText = "SELECT id, name FROM users ORDER BY id;";
using DbDataReader reader = sel.ExecuteReader();
int idIdx = reader.GetOrdinal("id");
int nameIdx = reader.GetOrdinal("name");
while (reader.Read())
{
int id = reader.GetInt32(idIdx);
string name = reader.GetString(nameIdx);
Console.WriteLine($"{id}: {name}");
}
}
// 6) UPDATE
using (DbCommand upd = conn.CreateCommand())
{
upd.CommandText = "UPDATE users SET name=@name WHERE id=@id;";
var p1 = upd.CreateParameter(); p1.ParameterName = "@name"; p1.Value = "Jiro";
var p2 = upd.CreateParameter(); p2.ParameterName = "@id"; p2.Value = 1;
upd.Parameters.Add(p1); upd.Parameters.Add(p2);
upd.ExecuteNonQuery();
}
// 7) DELETE
using (DbCommand del = conn.CreateCommand())
{
del.CommandText = "DELETE FROM users WHERE id=@id;";
var p = del.CreateParameter(); p.ParameterName = "@id"; p.Value = 1;
del.Parameters.Add(p);
del.ExecuteNonQuery();
}
}
補足:依存性の注入(DI: Dependency Injection)
ここでは、「あとで作り方(実装)を差し替えたい」ときに、
直す場所を最小にするための考え方として DI を説明します。
対象は「プログラム経験はあるが、C# の流儀はこれから」という人です。難しい用語は後半の補足でまとめます。
1. まず「なぜ DI を使うのか」(現実のたとえ)
たとえ:社内に「印刷物の作成」を頼む仕事があるとします。 いまは各担当者が、毎回それぞれ勝手に印刷会社を選んで発注している状態です。
- 担当A:いつも印刷会社Aに直接電話して発注
- 担当B:いつも印刷会社Bに直接メールして発注
- 担当C:いつも印刷会社Aに直接電話して発注
ここで「印刷会社を変更する(値上げ・品質問題・契約変更など)」となると、 担当者ごとの発注手順を全部探して直すことになり、影響範囲が広がります。
そこで、「発注先を決める場所」を 1か所にまとめます。
つまり各担当者は『印刷を頼む』と言うだけにして、
どこに発注するかは“1か所で管理”で決めるようにします。
2. DI が目指すこと(影響範囲を小さくする)
DI の狙いはシンプルです:
「どれを使うかを決める場所」を、アプリ起動時の 1か所に集めることです。
そうしておけば、実装を差し替えるときに あちこちの修正が発生しません。
3. ロジックやDBが変わったら何をする?(現実の作業)
DIで作っていれば、やることは次の3つに整理できます。
- 新しいクラスを用意する(新しい計算方法 / 新しいDB方式など)
- 再コンパイルする(.NET はビルドして動かすため)
- Program.cs の「どれを使うか」の1行を変更する
重要なのは「変更点を Program.cs に集める」ことです。 これにより、画面(Form)や他の処理に変更が波及しにくくなります。
4. なぜ差し替えが成立するのか
差し替えが成立する条件は 1つだけです:
差し替え前と差し替え後のクラスが、同一のインターフェースを使用していること。
DI はインターフェースを中心に構成されます。 画面(Form)はインターフェースだけを知っていて、具体クラスは知りません。
5. DI は「インターフェースを軸」にする
例として、業務ロジックを IBusinessLogic として表します。
// 画面(Form)が使用するインターフェースを定義する
public interface IBusinessLogic
{
string Calculate(string input);
}
// 実装A:通常版のロジック
public class BusinessLogic : IBusinessLogic
{
public string Calculate(string input) => $"[A] {input}";
}
// 実装B:別方式のロジック
public class AnotherBusinessLogic : IBusinessLogic
{
public string Calculate(string input) => $"[B] {input}";
}
画面側(Form)は IBusinessLogic だけを使います。
6. Program.cs
// Program.cs(DI を使う最小構成)
// ※ WinForms のテンプレートに DI は標準で入らないことが多いので、NuGet で
// Microsoft.Extensions.DependencyInjection を追加してから使います。
using System;
using System.Windows.Forms;
using Microsoft.Extensions.DependencyInjection;
internal static class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
// ① DI 用の「入れ物(インスタンス)」を作る
// ここに「どれを使うか」を書いていきます。
var services = new ServiceCollection();
// ② どの実装を使うかを 1か所で決める
// ここを書き換えるだけで、IBusinessLogic の実装を差し替えられます。
services.AddSingleton<IBusinessLogic, BusinessLogic>();
// シングルトンが不適切な場合は、次のように差し替える
// services.AddSingleton<IBusinessLogic, AnotherBusinessLogic>();
// ③ Form1 を DI で作れるように登録する
// (あとで provider.GetRequiredService<Form1>() するため)
services.AddTransient<Form1>();
// ④ 上で登録した内容を元にインスタンス生成の仕組みを作る
using var provider = services.BuildServiceProvider();
// ⑤ Form1 のインスタンスを作ってもらう
// Form1 のコンストラクタに IBusinessLogic が必要なら、
// DI が ② の登録を見て自動で用意して渡します。
var mainForm = provider.GetRequiredService<Form1>();
// ⑥ 起動はいつも通り(ここは DI でも変わりません)
Application.Run(mainForm);
}
}
- AddSingleton:アプリ起動中は同じインスタンスを使い回します(設定や共有状態を持つロジックなどで使う)
- AddTransient:必要なたびに新しいインスタンスを作ります(軽量で使い捨てにしたい場合)
7. 変更が起きたとき Program.cs のどこを直す?
差し替えの対象が IBusinessLogic なら、直すのはここだけです。
// Before(通常版)
services.AddSingleton<IBusinessLogic, BusinessLogic>();
// After(別方式)
services.AddSingleton<IBusinessLogic, AnotherBusinessLogic>();
Form1 側が IBusinessLogic だけを使っている限り、Form1 のコードは基本的に触りません。
8. サンプルで再確認
DI を使う Form は、必要なものをコンストラクタで受け取る形になります。
ここで渡されるのは「クラス」ではなく、インスタンスです。
// Form1.cs(DI を前提にした形)
// 「IBusinessLogic を使いたい」だけ宣言し、どの実装かは知らない
public partial class Form1 : Form
{
private readonly IBusinessLogic _logic;
// ここで受け取るのは IBusinessLogic の「インスタンス」です
public Form1(IBusinessLogic logic)
{
InitializeComponent();
_logic = logic;
}
private void btnRun_Click(object sender, EventArgs e)
{
var result = _logic.Calculate(txtInput.Text);
lblResult.Text = result;
}
}
Form1 が
BusinessLogic を直接書いてしまうと、
別方式に変えるたびに Form1 の型名も書き換えることになります。インターフェース(IBusinessLogic)で受け取ることで、Form1 側の修正を避けられます。
9. 補足(用語)
コンストラクタ注入
クラスの コンストラクタ引数として、依存する型を受け取る方法です。
DI はクラスのコンストラクタを見て、必要な引数を自動的に用意して渡します。
ライフタイム
DI が インスタンスをどの範囲・期間で使うかを表す概念です。
Singleton / Transient / Scoped の違いは、すべて ライフタイムの違いです。
登録
ServiceCollection に対して、
- どのインターフェースに
- どのクラスを
- どのライフタイムで使うか
を記述することを指します。
解決(Resolve)
ServiceProvider が登録内容を元に、
- 必要なクラスを判断し
- インスタンスを生成し
- コンストラクタに渡す
一連の処理を行うことを指します。
補足:App.config(.NET Framework の設定ファイル)
App.config は主に .NET Framework 時代の標準的な構成ファイル(XML)です。
Visual Studio ではアプリ設定(Settings)を作ると app.config が作成されます。 citeturn0search3
作成される?どこに置く?
-
.NET Framework の WinForms では、プロジェクト作成時点で
App.configが追加されることがあります。 citeturn0search6 - もし無い場合は、ソリューションエクスプローラ → 追加 → 新しい項目 → アプリケーション構成ファイルで追加できます。 citeturn0search4
-
ビルド後は、出力フォルダに
<アプリ名>.exe.configとしてコピーされ、それが実行時に使われます。 citeturn0search9 -
.NET(Core/5+)でも
ConfigurationManagerを使い続けることは可能ですが、 互換目的の側面が強く、モダンな設定はappsettings.jsonが推奨されます。 citeturn0search1
設定例(App.config)
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!-- 文字列キーの設定(小規模なら手軽) -->
<appSettings>
<add key="TimeoutMs" value="5000" />
<add key="ApiBaseUrl" value="https://example.com/" />
</appSettings>
<!-- 接続文字列(DB) -->
<connectionStrings>
<add name="MainDb"
connectionString="Data Source=pr.db;"
providerName="Microsoft.Data.Sqlite" />
</connectionStrings>
</configuration>
読み取りサンプル(ConfigurationManager)
・
System.Configuration.ConfigurationManager(.NET Framework には標準で存在)
using System;
using System.Configuration;
public static class AppConfigSample
{
public static void Demo()
{
// appSettings(文字列キー)
string timeoutStr = ConfigurationManager.AppSettings["TimeoutMs"] ?? "5000";
int timeoutMs = int.Parse(timeoutStr);
// connectionStrings(DB接続文字列)
string cs = ConfigurationManager.ConnectionStrings["MainDb"]?.ConnectionString ?? "";
Console.WriteLine(timeoutMs);
Console.WriteLine(cs);
}
}
appsettings.json と App.config の使い分け
- .NET Framework:App.config が自然(既存資産とも相性が良い)
- .NET(Core/5+):appsettings.json + IConfiguration が自然(拡張性・合成が強い) citeturn0search2
- WinForms(.NET)で “軽く” やるなら:App.config も可。ただし将来的には appsettings.json へ寄せるのが無難 citeturn0search1
補足:appsettings.json(.NET(Core/5+)の設定ファイル)
appsettings.json は .NET(Core/5+) で一般的に使われる設定ファイル(JSON)です。
WinForms(.NET)でも、NuGet を追加すれば同じ流儀で読み込めます。
作成される?どこに置く?
-
ASP.NET Core のテンプレート(Web API / MVC など)では、作成時にプロジェクト直下(
.csprojと同じ階層)へ作られるのが一般的です。 -
WinForms / Console などではテンプレートによっては自動生成されません。その場合は「プロジェクト直下」に
appsettings.jsonを追加します。 -
実行時に読み込むため、
appsettings.jsonを 出力フォルダ(bin)へコピーする設定が必要です(Visual Studio のプロパティでCopy to Output Directoryを設定)。
確実にするなら.csprojに次を追加します。
<!-- *.csproj の中に追記 -->
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
設定例(appsettings.json)
{
"App": {
"TimeoutMs": 5000,
"ApiBaseUrl": "https://example.com/"
},
"ConnectionStrings": {
"MainDb": "Data Source=pr.db;"
}
}
必要な NuGet(WinForms で読む場合)
Microsoft.Extensions.ConfigurationMicrosoft.Extensions.Configuration.Json
読み取りサンプル(ConfigurationBuilder)
using System;
using Microsoft.Extensions.Configuration;
public static class AppSettingsSample
{
public static IConfigurationRoot Load()
{
// 実行ファイルのある場所(bin/...)を基準に JSON を探す
return new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
.Build();
}
public static void Demo()
{
var cfg = Load();
// "App:TimeoutMs" のように "セクション:キー" で読む
int timeoutMs = cfg.GetValue("App:TimeoutMs");
string baseUrl = cfg["App:ApiBaseUrl"] ?? "";
// ConnectionStrings は GetConnectionString が便利
string cs = cfg.GetConnectionString("MainDb") ?? "";
Console.WriteLine(timeoutMs);
Console.WriteLine(baseUrl);
Console.WriteLine(cs);
}
}
SetBasePath(AppContext.BaseDirectory)により、bin フォルダを基準に読み込みます(なので「bin にコピー」が重要です)。- 値の型は
GetValue<T>で取り出せます(数値や bool など)。
第11章:コードサンプル
サンプル①:REST API 連携(追加・削除)+チェック付きリスト
仕様
- 画面上に
textbox1、buttonAdd、buttonDelete、list1がある list1の各行の先頭にチェックボックスがある(WinForms ではCheckedListBoxを使用)- ユーザーが
textbox1に文字を入力し、buttonAddを押す buttonAddは入力が空白なら MessageBox を出して終了- 空白でなければ
http://example.com/animalへ POST(Body:{"name": "..."}) - POST がエラーなら、エラー内容を MessageBox(モーダル)で表示し終了
- 成功なら list に 1 行追加(チェックボックス付き)
- 削除:ユーザーがチェックを付け、
buttonDeleteを押す buttonDeleteはhttp://example.com/animalへ DELETE(Body:{"name":"..."})- HTTP エラーなら MessageBox で通知
- 成功なら画面中央に「削除完了しました」をトースト表示
WinForms の
ListBox は行ごとのチェックボックスを標準では持ちません。この仕様を素直に満たすには
CheckedListBox を使うのが最短です。
プロジェクト構成(最小)
WinFormsAnimalSample/
Program.cs
Form1.cs
Form1.Designer.cs
Models/
AnimalRequest.cs
全コード
Program.cs
using System;
using System.Windows.Forms;
namespace WinFormsAnimalSample
{
internal static class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
Application.Run(new Form1());
}
}
}
Models/AnimalRequest.cs
namespace WinFormsAnimalSample.Models
{
public class AnimalRequest
{
public string Name { get; set; } = "";
}
}
Form1.cs(ロジック側)
using System;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using WinFormsAnimalSample.Models;
namespace WinFormsAnimalSample
{
public partial class Form1 : Form
{
private static readonly Uri ApiUri = new Uri("http://example.com/animal");
private readonly HttpClient _http = new HttpClient();
private readonly Toast _toast;
private CancellationTokenSource? _cts;
public Form1()
{
InitializeComponent();
_toast = new Toast(this);
list1.CheckOnClick = true;
}
private async void buttonAdd_Click(object sender, EventArgs e)
{
string name = (textbox1.Text ?? "").Trim();
if (string.IsNullOrWhiteSpace(name))
{
MessageBox.Show("名前を入力してください。", "入力エラー", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
buttonAdd.Enabled = false;
buttonDelete.Enabled = false;
_cts = new CancellationTokenSource();
try
{
var body = new AnimalRequest { Name = name };
string json = JsonSerializer.Serialize(body);
using var content = new StringContent(json, Encoding.UTF8, "application/json");
using HttpResponseMessage res = await _http.PostAsync(ApiUri, content, _cts.Token);
if (!res.IsSuccessStatusCode)
{
string err = await res.Content.ReadAsStringAsync(_cts.Token);
MessageBox.Show($"POST 失敗: HTTP {(int)res.StatusCode} {res.ReasonPhrase}\n{err}",
"通信エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
list1.Items.Add(name, false);
textbox1.Clear();
textbox1.Focus();
}
catch (OperationCanceledException)
{
MessageBox.Show("キャンセルしました。", "キャンセル", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "例外", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
buttonAdd.Enabled = true;
buttonDelete.Enabled = true;
_cts = null;
}
}
private async void buttonDelete_Click(object sender, EventArgs e)
{
if (list1.CheckedItems.Count == 0)
{
MessageBox.Show("削除する項目にチェックを付けてください。", "削除", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var names = list1.CheckedItems.Cast<object>()
.Select(x => x.ToString() ?? "")
.Where(s => s.Length > 0)
.ToList();
buttonAdd.Enabled = false;
buttonDelete.Enabled = false;
_cts = new CancellationTokenSource();
try
{
foreach (string name in names)
{
var body = new AnimalRequest { Name = name };
string json = JsonSerializer.Serialize(body);
using var req = new HttpRequestMessage(HttpMethod.Delete, ApiUri)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
using HttpResponseMessage res = await _http.SendAsync(req, _cts.Token);
if (!res.IsSuccessStatusCode)
{
string err = await res.Content.ReadAsStringAsync(_cts.Token);
MessageBox.Show($"DELETE 失敗: HTTP {(int)res.StatusCode} {res.ReasonPhrase}\n{err}",
"通信エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
}
for (int i = list1.Items.Count - 1; i >= 0; i--)
{
if (list1.GetItemChecked(i))
{
list1.Items.RemoveAt(i);
}
}
_toast.ShowCentered("削除完了しました", milliseconds: 1200);
}
catch (OperationCanceledException)
{
MessageBox.Show("キャンセルしました。", "キャンセル", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "例外", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
buttonAdd.Enabled = true;
buttonDelete.Enabled = true;
_cts = null;
}
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_cts?.Cancel();
_http.Dispose();
base.OnFormClosed(e);
}
}
internal class Toast
{
private readonly Form _owner;
private readonly Label _label;
private readonly Timer _timer;
public Toast(Form owner)
{
_owner = owner;
_label = new Label
{
AutoSize = true,
Visible = false,
BackColor = System.Drawing.Color.FromArgb(220, 0, 0, 0),
ForeColor = System.Drawing.Color.White,
Padding = new Padding(12, 8, 12, 8),
Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular)
};
_owner.Controls.Add(_label);
_label.BringToFront();
_timer = new Timer();
_timer.Tick += (s, e) =>
{
_timer.Stop();
_label.Visible = false;
};
}
public void ShowCentered(string text, int milliseconds)
{
_label.Text = text;
_label.Visible = true;
_label.BringToFront();
int x = (_owner.ClientSize.Width - _label.Width) / 2;
int y = (_owner.ClientSize.Height - _label.Height) / 2;
_label.Left = Math.Max(0, x);
_label.Top = Math.Max(0, y);
_timer.Interval = Math.Max(200, milliseconds);
_timer.Stop();
_timer.Start();
}
}
}
Form1.Designer.cs(UI側)
namespace WinFormsAnimalSample
{
partial class Form1
{
private System.ComponentModel.IContainer components = null;
private System.Windows.Forms.TextBox textbox1;
private System.Windows.Forms.Button buttonAdd;
private System.Windows.Forms.Button buttonDelete;
private System.Windows.Forms.CheckedListBox list1;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
private void InitializeComponent()
{
this.textbox1 = new System.Windows.Forms.TextBox();
this.buttonAdd = new System.Windows.Forms.Button();
this.buttonDelete = new System.Windows.Forms.Button();
this.list1 = new System.Windows.Forms.CheckedListBox();
this.SuspendLayout();
//
// textbox1
//
this.textbox1.Location = new System.Drawing.Point(16, 16);
this.textbox1.Name = "textbox1";
this.textbox1.Size = new System.Drawing.Size(280, 23);
this.textbox1.TabIndex = 0;
//
// buttonAdd
//
this.buttonAdd.Location = new System.Drawing.Point(310, 16);
this.buttonAdd.Name = "buttonAdd";
this.buttonAdd.Size = new System.Drawing.Size(90, 23);
this.buttonAdd.TabIndex = 1;
this.buttonAdd.Text = "Add";
this.buttonAdd.UseVisualStyleBackColor = true;
this.buttonAdd.Click += new System.EventHandler(this.buttonAdd_Click);
//
// buttonDelete
//
this.buttonDelete.Location = new System.Drawing.Point(410, 16);
this.buttonDelete.Name = "buttonDelete";
this.buttonDelete.Size = new System.Drawing.Size(90, 23);
this.buttonDelete.TabIndex = 2;
this.buttonDelete.Text = "Delete";
this.buttonDelete.UseVisualStyleBackColor = true;
this.buttonDelete.Click += new System.EventHandler(this.buttonDelete_Click);
//
// list1
//
this.list1.FormattingEnabled = true;
this.list1.Location = new System.Drawing.Point(16, 55);
this.list1.Name = "list1";
this.list1.Size = new System.Drawing.Size(484, 292);
this.list1.TabIndex = 3;
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(520, 370);
this.Controls.Add(this.list1);
this.Controls.Add(this.buttonDelete);
this.Controls.Add(this.buttonAdd);
this.Controls.Add(this.textbox1);
this.Name = "Form1";
this.Text = "Animal Sample";
this.ResumeLayout(false);
this.PerformLayout();
}
}
}
解説(どこが重要?)
- 入力チェック:
string.IsNullOrWhiteSpaceで空白を弾く - HTTP エラー:
IsSuccessStatusCodeを見て MessageBox で通知(MessageBox はデフォルトでモーダル) - UI 更新:イベントハンドラ内で
list1.Items.AddすればOK(UIスレッド) - HttpClient 使い回し:毎回 new しない
- トースト:Label + Timer で簡易実装
response.IsSuccessStatusCode は HTTP 200〜299(2xx)なら成功として true になります。
そのため、POST 成功の 201 Created、DELETE 成功の 204 No Content も成功扱いです。
- 例:
200 OK,201 Created,202 Accepted,204 No Content→ 成功(true) - 例:
400,401,403,404,500→ 失敗(false)
サンプル②:SQLite + DataGridView + 編集ダイアログ
仕様(シンプルPR編集アプリ)
SQLite に保存されたユーザー情報(名前・自己PR)を一覧表示し、行選択で自己PRを編集して更新するデスクトップアプリです。
画面イメージ(参考)
画面構成とコントロール
- MainForm:一覧画面
DataGridView dgvPrList:名前と自己PRを表示。行選択で編集画面へButton btnRefresh:DBから再読み込みして表示更新
- EditForm:編集ダイアログ
TextBox txtSelfPr(Multiline):自己PR入力欄Button btnSave:保存(DialogResult.OK)Button btnCancel:キャンセル(DialogResult.Cancel)
DB仕様(SQLite)
- DBエンジン:SQLite3
- テーブル:
pr - カラム:
name(TEXT / PRIMARY KEY)self_pr(TEXT)
動作フロー(いつトースト/いつ MessageBox?)
- 初期表示(MainForm 起動時)
DB に接続し、SELECT name, self_pr FROM prを実行します。取得結果をdgvPrListに表示します。 ここで例外が出た場合は、起動時点で操作不能になるため MessageBox(エラー)で通知します。 - 編集遷移(一覧の行をクリック)
ユーザーがdgvPrListの行をクリックすると、選択行からNameとSelfPrを取り出し、EditFormをモーダル表示します(ShowDialog)。
EditFormのtxtSelfPrには「現在の自己PR」を初期値として渡します。 - EditForm の結果判定(DialogResult)
- Cancel の場合(ユーザーがキャンセルボタン): DB は更新しません。操作の結果として トーストで「キャンセルされました」を短時間表示します。
- OK の場合(ユーザーが保存ボタン): DB 更新処理に進みます(次の手順)。
- DB 更新(OK の場合のみ)
BeginTransactionでトランザクションを開始し、パラメータ付きでUPDATE pr SET self_pr = @selfPr WHERE name = @nameを実行します。- 成功:
Commit→ トースト「更新しました」 → 一覧を再読み込みして最新表示 - 失敗(例外):
Rollback→ 例外内容を MessageBox(エラー)で表示(ユーザーが OK を押して閉じる)
- 成功:
SQLite の ADO.NET プロバイダを追加します:
Microsoft.Data.Sqlite(Visual Studio:プロジェクト右クリック → NuGet パッケージの管理)
コメント付き:全サンプルコード
プロジェクト構成(例)
PrEditorSample/
Program.cs
MainForm.cs
MainForm.Designer.cs
EditForm.cs
EditForm.Designer.cs
Data/
PrRepository.cs
Models/
PrRow.cs
Program.cs
using System;
using System.Windows.Forms;
namespace PrEditorSample
{
internal static class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
Application.Run(new MainForm());
}
}
}
Models/PrRow.cs
namespace PrEditorSample.Models
{
// DataGridView に表示する 1 行分のデータ(DTO / ViewModel)
public class PrRow
{
public string Name { get; set; } = "";
public string SelfPr { get; set; } = "";
// 楽観ロック(排他制御)用の版数
public long Version { get; set; }
}
}
Data/PrRepository.cs
using System;
using System.Collections.Generic;
using Microsoft.Data.Sqlite;
using PrEditorSample.Models;
namespace PrEditorSample.Data
{
///
/// SQLite に対する DB 操作をまとめるクラス。
/// UI(Form)から SQL を直書きしないための分離層(Repository)。
///
public class PrRepository
{
private readonly string _connectionString;
public PrRepository(string connectionString)
{
_connectionString = connectionString;
}
///
/// テーブルが無ければ作る(サンプル用の初期化)
/// version 列は「楽観ロック(排他制御)」に使う。
///
public void EnsureSchema()
{
using var conn = new SqliteConnection(_connectionString);
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS pr(
name TEXT PRIMARY KEY,
self_pr TEXT,
version INTEGER NOT NULL DEFAULT 0
);";
cmd.ExecuteNonQuery();
}
/// 一覧取得
public List GetAll()
{
var list = new List();
using var conn = new SqliteConnection(_connectionString);
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT name, self_pr, version FROM pr ORDER BY name;";
using var reader = cmd.ExecuteReader();
// GetOrdinal を使うと「列の順番が変わっても壊れにくい」
int idxName = reader.GetOrdinal("name");
int idxPr = reader.GetOrdinal("self_pr");
int idxVer = reader.GetOrdinal("version");
while (reader.Read())
{
list.Add(new PrRow
{
Name = reader.GetString(idxName),
SelfPr = reader.IsDBNull(idxPr) ? "" : reader.GetString(idxPr),
Version = reader.GetInt64(idxVer)
});
}
return list;
}
///
/// 自己PR更新(トランザクション + 楽観ロック)
///
/// 更新キー
/// 更新後の自己PR
///
/// 画面で表示していた時点の version。
/// DB側の version が一致しない場合は「誰かが先に更新した」と判断する。
///
public void UpdateSelfPr(string name, string newSelfPr, long expectedVersion)
{
using var conn = new SqliteConnection(_connectionString);
conn.Open();
using var tx = conn.BeginTransaction();
try
{
using var cmd = conn.CreateCommand();
cmd.Transaction = tx;
// 楽観ロック:WHERE 句に version を入れて一致した時だけ更新する
// 更新成功したら version を +1 する
cmd.CommandText = @"
UPDATE pr
SET self_pr = @selfPr,
version = version + 1
WHERE name = @name
AND version = @version;";
cmd.Parameters.AddWithValue("@selfPr", newSelfPr);
cmd.Parameters.AddWithValue("@name", name);
cmd.Parameters.AddWithValue("@version", expectedVersion);
int affected = cmd.ExecuteNonQuery();
if (affected == 0)
{
// 0件更新 = version 不一致 or name 不存在
throw new InvalidOperationException(
"更新できませんでした。別のユーザーが先に更新した可能性があります。\n" +
"一覧を更新して最新状態を確認してください。"
);
}
tx.Commit();
}
catch
{
tx.Rollback();
throw; // UI 側で MessageBox に出すため再スロー
}
}
}
}
(共通)トースト表示クラス
using System;
using System.Windows.Forms;
namespace PrEditorSample
{
internal class Toast
{
private readonly Form _owner;
private readonly Label _label;
private readonly Timer _timer;
public Toast(Form owner)
{
_owner = owner;
_label = new Label
{
AutoSize = true,
Visible = false,
BackColor = System.Drawing.Color.FromArgb(220, 0, 0, 0),
ForeColor = System.Drawing.Color.White,
Padding = new Padding(12, 8, 12, 8),
Font = new System.Drawing.Font("Segoe UI", 11F)
};
_owner.Controls.Add(_label);
_label.BringToFront();
_timer = new Timer();
_timer.Tick += (s, e) =>
{
_timer.Stop();
_label.Visible = false;
};
}
public void ShowCentered(string text, int milliseconds = 1200)
{
_label.Text = text;
_label.Visible = true;
_label.BringToFront();
int x = (_owner.ClientSize.Width - _label.Width) / 2;
int y = (_owner.ClientSize.Height - _label.Height) / 2;
_label.Left = Math.Max(0, x);
_label.Top = Math.Max(0, y);
_timer.Interval = Math.Max(200, milliseconds);
_timer.Stop();
_timer.Start();
}
}
}
MainForm.cs(一覧 + 編集遷移 + 再読込)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using PrEditorSample.Data;
using PrEditorSample.Models;
namespace PrEditorSample
{
public partial class MainForm : Form
{
// DB ファイルは “作業フォルダ” 基準で置く例(必要に応じてフルパス化)
private const string DbFile = "pr.db";
private readonly PrRepository _repo;
private readonly Toast _toast;
// DataGridView にバインドする(編集しない想定なので読み取り専用)
private BindingList _binding = new BindingList();
// 連打防止やキャンセルに使う(重いDBなら有効)
private CancellationTokenSource? _cts;
public MainForm()
{
InitializeComponent();
string cs = $"Data Source={DbFile};";
_repo = new PrRepository(cs);
_toast = new Toast(this);
// サンプル用:無ければテーブル作成
_repo.EnsureSchema();
// ---- UI 初期設定(「一覧を見るだけ」用途なので編集できないようにする) ----
dgvPrList.AutoGenerateColumns = true;
// セル編集を禁止(最重要)
dgvPrList.ReadOnly = true;
dgvPrList.EditMode = DataGridViewEditMode.EditProgrammatically;
// 選択は行単位、1行のみ
dgvPrList.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgvPrList.MultiSelect = false;
// 行クリックで編集ダイアログへ(仕様どおり)
dgvPrList.CellClick += dgvPrList_CellClick;
// 更新ボタン
btnRefresh.Click += async (s, e) => await LoadGridAsync();
// 初期ロード(非同期)
_ = LoadGridAsync();
}
///
/// 一覧を読み込み、DataGridView に反映する。
/// DB が重い場合に UI を固めないため、Task.Run で別スレッド実行にする。
///
private async Task LoadGridAsync()
{
// 前回のロードが動いていたらキャンセル(例:連打対策)
_cts?.Cancel();
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
btnRefresh.Enabled = false;
// ★ここが Task.Run(UIフリーズ対策)
List rows = await Task.Run(() =>
{
token.ThrowIfCancellationRequested();
return _repo.GetAll();
}, token);
// UI スレッドでバインドし直す
_binding = new BindingList(rows);
dgvPrList.DataSource = _binding;
}
catch (OperationCanceledException)
{
// キャンセルはエラー扱いしない
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "読込エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
btnRefresh.Enabled = true;
}
}
private async void dgvPrList_CellClick(object? sender, DataGridViewCellEventArgs e)
{
// ヘッダ等のクリックは除外
if (e.RowIndex < 0) return;
// 選択行のデータ(表示していた時点の Version も含む)
var row = (PrRow)dgvPrList.Rows[e.RowIndex].DataBoundItem;
// EditForm を開く(モーダル:戻り値は DialogResult)
using var form = new EditForm(row.Name, row.SelfPr);
var result = form.ShowDialog(this);
if (result == DialogResult.Cancel)
{
// キャンセルは「操作をやめた」だけなので軽い通知=トースト
_toast.ShowCentered("キャンセルされました");
return;
}
if (result == DialogResult.OK)
{
// 保存(OK)の場合のみ DB 更新に進む
try
{
// DB 更新も重い可能性があるので Task.Run で UI を固めない
await Task.Run(() =>
{
_repo.UpdateSelfPr(row.Name, form.UpdatedSelfPr, row.Version);
});
// 更新成功 → トースト → 一覧更新
_toast.ShowCentered("更新しました");
await LoadGridAsync();
}
catch (Exception ex)
{
// 失敗(例外)→ MessageBox(ユーザーがOKを押すまで閉じない)
MessageBox.Show(ex.Message, "更新エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}
}
MainForm.Designer.cs(UI定義)
namespace PrEditorSample
{
partial class MainForm
{
private System.ComponentModel.IContainer components = null;
private System.Windows.Forms.DataGridView dgvPrList;
private System.Windows.Forms.Button btnRefresh;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
private void InitializeComponent()
{
this.dgvPrList = new System.Windows.Forms.DataGridView();
this.btnRefresh = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)(this.dgvPrList)).BeginInit();
this.SuspendLayout();
//
// dgvPrList
//
this.dgvPrList.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.dgvPrList.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgvPrList.Location = new System.Drawing.Point(12, 51);
this.dgvPrList.Name = "dgvPrList";
this.dgvPrList.RowTemplate.Height = 25;
this.dgvPrList.Size = new System.Drawing.Size(760, 397);
this.dgvPrList.TabIndex = 0;
//
// btnRefresh
//
this.btnRefresh.Location = new System.Drawing.Point(12, 12);
this.btnRefresh.Name = "btnRefresh";
this.btnRefresh.Size = new System.Drawing.Size(120, 30);
this.btnRefresh.TabIndex = 1;
this.btnRefresh.Text = "更新";
this.btnRefresh.UseVisualStyleBackColor = true;
//
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(784, 461);
this.Controls.Add(this.btnRefresh);
this.Controls.Add(this.dgvPrList);
this.Name = "MainForm";
this.Text = "PR一覧";
((System.ComponentModel.ISupportInitialize)(this.dgvPrList)).EndInit();
this.ResumeLayout(false);
}
}
}
EditForm.cs(編集ダイアログ)
using System;
using System.Windows.Forms;
namespace PrEditorSample
{
public partial class EditForm : Form
{
private readonly string _name;
// MainForm に返す “更新後の自己PR”
public string UpdatedSelfPr => txtSelfPr.Text;
// MainForm に返す “編集対象(誰の行か)”
public string TargetName => _name;
public EditForm(string name, string currentSelfPr)
{
InitializeComponent();
_name = name;
this.Text = $"自己PR編集 - {name}";
// 初期値として現在の自己PRを表示
txtSelfPr.Text = currentSelfPr;
btnSave.Click += btnSave_Click;
btnCancel.Click += (s, e) =>
{
this.DialogResult = DialogResult.Cancel;
this.Close();
};
}
private void btnSave_Click(object? sender, EventArgs e)
{
// ここで入力チェックを入れるならこのタイミング
// 例:空欄禁止なら MessageBox で通知して return する、など。
this.DialogResult = DialogResult.OK;
this.Close();
}
}
}
EditForm.Designer.cs(UI定義)
namespace PrEditorSample
{
partial class EditForm
{
private System.ComponentModel.IContainer components = null;
private System.Windows.Forms.TextBox txtSelfPr;
private System.Windows.Forms.Button btnSave;
private System.Windows.Forms.Button btnCancel;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
private void InitializeComponent()
{
this.txtSelfPr = new System.Windows.Forms.TextBox();
this.btnSave = new System.Windows.Forms.Button();
this.btnCancel = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// txtSelfPr
//
this.txtSelfPr.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.txtSelfPr.Location = new System.Drawing.Point(12, 12);
this.txtSelfPr.Multiline = true;
this.txtSelfPr.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
this.txtSelfPr.Name = "txtSelfPr";
this.txtSelfPr.Size = new System.Drawing.Size(560, 260);
this.txtSelfPr.TabIndex = 0;
//
// btnSave
//
this.btnSave.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnSave.Location = new System.Drawing.Point(376, 284);
this.btnSave.Name = "btnSave";
this.btnSave.Size = new System.Drawing.Size(90, 30);
this.btnSave.TabIndex = 1;
this.btnSave.Text = "保存";
this.btnSave.UseVisualStyleBackColor = true;
//
// btnCancel
//
this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnCancel.Location = new System.Drawing.Point(482, 284);
this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(90, 30);
this.btnCancel.TabIndex = 2;
this.btnCancel.Text = "キャンセル";
this.btnCancel.UseVisualStyleBackColor = true;
//
// EditForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(584, 326);
this.Controls.Add(this.btnCancel);
this.Controls.Add(this.btnSave);
this.Controls.Add(this.txtSelfPr);
this.Name = "EditForm";
this.Text = "自己PR編集";
this.ResumeLayout(false);
this.PerformLayout();
}
}
}
コード説明(理解のポイント)
- Repository 分離:Form(UI)から SQL を直接書かず、
PrRepositoryに集約します。
理由:①UIコードがSQLで汚れない ②DB差し替えやテストがしやすい ③例外処理・トランザクションの責務を一箇所にまとめられる、ためです。
目安:Form は「ボタン/クリックの受付・画面更新」、Repository は「接続・SQL・トランザクション」を担当します。 - DataGridView 表示:
BindingList<PrRow>をDataSourceにセット - 編集遷移:行クリック →
EditForm.ShowDialog→DialogResultで分岐 - トランザクション:
BeginTransaction→ UPDATE →Commit/ 例外時Rollback
注意点(実務で詰まりやすいところ)
- DBファイルの場所:サンプルは作業フォルダ基準。必要なら絶対パス化する
- UIフリーズ:DB処理が重いと UI スレッドが止まります。サンプルでは
Task.Runで別スレッド実行し、完了後に UI を更新しています。 - SQLインジェクション:必ずパラメータ(
@name,@selfPr)を使う - 同時更新(排他制御):サンプルでは
version列を使う楽観ロックを実装しています。
第12章:XMLドキュメントコメント(JavaDoc相当)
C# の XMLドキュメントコメント は、Java の JavaDoc に相当する仕組みです。
/// から始まるコメントに XML 形式のタグ(<summary> など)を書いておくと、
Visual Studio の IntelliSense(補完ツールチップ)に表示され、チーム開発や保守で効きます。
1. 基本:/// と <summary>
もっとも基本は <summary>(要約)です。メソッド・クラス・プロパティなど、公開APIに付けるのが一般的です。
/// <summary>
/// 2つの整数を加算して返します。
/// </summary>
public int Add(int a, int b)
{
return a + b;
}
2. よく使う要素(タグ)一覧
実務で頻出のタグをまとめます(JavaDoc との対比も併記)。
| タグ | 用途 | JavaDoc対比 |
|---|---|---|
<summary> | 要約(最重要) | 説明本文 |
<param name="..."></param> | 引数の説明 | @param |
<returns> | 戻り値の説明 | @return |
<remarks> | 補足(制約・注意点・設計意図など) | 補足本文 |
<exception cref="..."></exception> | 例外の説明 | @throws |
<example> | 使用例(サンプル) | 例示本文 |
<typeparam name="..."></typeparam> | ジェネリクス型引数の説明 | @param <T> |
<value> | プロパティ値の説明 | 説明本文 |
<see cref="..."/> | 参照リンク(インライン) | @link |
<seealso cref="..."/> | 関連項目(一覧) | @see |
<code>...</code> | コード断片(整形) | <pre>相当 |
<para> | 段落 | 段落 |
<list> | 箇条書き(bullet/number/table) | 箇条書き |
<inheritdoc/> | 親のコメント継承 | {@inheritDoc} |
3. フルサンプル(param/returns/remarks/exception/see/example)
実務で使うタグを一通り入れた例です(“何を書くか”の雛形として使えます)。
public class FileLoader
{
/// <summary>
/// テキストファイルを UTF-8 として読み込み、内容を返します。
/// </summary>
/// <param name="path">読み込むファイルパス(相対/絶対)。</param>
/// <returns>ファイルの内容(文字列)。</returns>
/// <remarks>
/// <para>大きなファイルの場合はメモリ使用量に注意してください。</para>
/// <para>例外は呼び出し側でハンドリングできるよう、ここでは握りつぶしません。</para>
/// <list type="bullet">
/// <item><description>入力値は null/空文字チェックを行う</description></item>
/// <item><description>重い処理は UI スレッドで行わない</description></item>
/// </list>
/// </remarks>
/// <exception cref="System.IO.FileNotFoundException">ファイルが存在しない場合。</exception>
/// <exception cref="System.UnauthorizedAccessException">権限不足で読めない場合。</exception>
/// <seealso cref="System.IO.File.ReadAllText(string, System.Text.Encoding)"/>
/// <example>
/// <code>
/// var loader = new FileLoader();
/// string text = loader.LoadUtf8("sample.txt");
/// Console.WriteLine(text);
/// </code>
/// </example>
public string LoadUtf8(string path)
{
return System.IO.File.ReadAllText(path, System.Text.Encoding.UTF8);
}
}
4. Visual Studio の入力支援(ショートカット的なやつ)
-
メンバー直前で
///→ Enter:<summary>や<param>などの “ひな形” が自動生成されます。 -
引数のあるメソッドでは
<param name="..."/>が自動で並ぶことが多いです(環境/設定により差あり)。 -
<see cref="..." />のcrefは型・メソッド名の補完が効きます。 -
併用例:VS のスニペット(例:
propでプロパティ雛形)を出してから、その直前で///を入れる運用も多いです。
5. XML ドキュメントファイルを生成する(プロジェクト設定)
- Visual Studio:プロジェクトのプロパティ → ビルド → XML ドキュメント ファイル を有効化
- 公開APIにコメントが無いと警告にしたい場合:警告番号
1591の運用を検討
まずは public API に
<summary> と <param>/<returns> を必須にするだけでも効果が出ます。仕様や落とし穴がある箇所だけ
<remarks> を厚めにするのが回しやすいです。