第25章 Windowsフォーム社員管理アプリ:編集・更新
この章の目的
Section titled “この章の目的”この章では、第 23・24 章で作った社員管理アプリに 編集機能 を追加し、第 23 章から枠だけ用意していた 新規登録・削除 も完成させます。
ここまでで一覧表示(Read)と検索ができるようになっていますが、業務アプリとしての CRUD(Create / Read / Update / Delete)はまだ Read だけです。この章で残りの 3 つを実装し、Windows フォーム社員管理アプリを 業務アプリの最小完成形 にします。
| 操作 | SQL | 章 |
|---|---|---|
| Create(新規登録) | INSERT | この章 |
| Read(一覧・検索) | SELECT | 第 23・24 章 |
| Update(更新) | UPDATE | この章 |
| Delete(削除) | DELETE | この章 |
加えて、この章で初めて 複数のフォーム間の画面遷移 を扱います。一覧画面で行を選んで「編集」を押すと 別のフォーム(編集ダイアログ) が開き、入力して保存すると一覧に戻って自動で再読み込みされる、という現場で頻出のパターンです。
この章でできるようになること
Section titled “この章でできるようになること”この章を終えると、次のことができるようになります。
INSERT/UPDATE/DELETE文をパラメータ化クエリで書けるIDENTITYで採番された ID を取得できる(OUTPUT INSERTED.column_name)SqlCommand.ExecuteNonQueryの戻り値(影響行数)の意味を説明できる- 別のフォームを
ShowDialogでモーダル表示できる - フォーム間でデータを受け渡せる(コンストラクタ + プロパティ)
DialogResultで OK / キャンセルを判定できる- 入力チェックを書ける(必須項目、数値、日付)
- 削除前の確認ダイアログを出せる
- 一覧画面に戻ったときに 自動で再読み込み する処理を書ける
本章で使用する環境
Section titled “本章で使用する環境”| 項目 | 内容 |
|---|---|
| 開発環境 | Visual Studio 2022 |
| プロジェクト種類 | Windows フォーム アプリ |
| 対象フレームワーク | .NET 8 |
| ソリューション名 | KadaiWinFormsApp(第 23 章で作成済み・続けて使う) |
| プロジェクト名 | EmployeeApp(第 23・24 章の続き・作り直さない) |
| ベースとなる章 | 第 24 章 |
| データベース | SQLServer 2022(TrainingDB) |
| 認証方式 | Windows 統合認証 |
| NuGet パッケージ | Microsoft.Data.SqlClient |
この章も第 23・24 章の
EmployeeAppを続けて使います新しいプロジェクトは作りません。
KadaiWinFormsAppソリューションのEmployeeAppを開き、編集ダイアログ(別フォーム)と CRUD のイベントを足していきます。
Server=localhostで接続できないとき第 23 章と同じく、SQLServer のインスタンス名によっては
Server=localhost\SQLEXPRESS等の指定が必要です。詳しくは第 23 章「23-4 接続文字列を設定する」の補足を参照してください。
作業前チェック
Section titled “作業前チェック”- 第 24 章を Git に提出済み、または手元にコードがある
-
TrainingDBのemployees/departmentsが動作している - 第 24 章の
EmployeeRepository.Searchが動くことを確認した
25-1 この章で完成させる機能
Section titled “25-1 この章で完成させる機能”第 23・24 章のアプリに、次の機能を追加します。
┌──────────────────────────────────────────────────────────────┐│ 社員一覧 [再読込]│├──────────────────────────────────────────────────────────────┤│ 名前:[ ] 部署:[ (すべての部署) ▼ ] [検索] [クリア]│├──────────────────────────────────────────────────────────────┤│ EmployeeId │ LastName │ FirstName │ Email │ Salary │├────────────┼──────────┼───────────┼─────────────────┼────────┤│ 1001 │ 山田 │ 二郎 │ yamada.jiro@... │ 500000 │ ← 行をダブルクリックで編集│ ... │├──────────────────────────────────────────────────────────────┤│ [新規登録] [編集] [削除] │ ← 全て有効化└──────────────────────────────────────────────────────────────┘機能の追加点:
| 機能 | 操作 |
|---|---|
| 新規登録 | 「新規登録」ボタン → 編集ダイアログが空欄で開く → 保存で INSERT |
| 編集 | 行を選んで「編集」ボタン(または行をダブルクリック) → ダイアログに値が入って開く → 保存で UPDATE |
| 削除 | 行を選んで「削除」ボタン → 確認ダイアログ → OK で DELETE |
| 保存後の更新 | ダイアログを閉じたら、一覧画面が自動で再読み込み |
25-2 EmployeeRepository に Insert / Update / Delete を実装する
Section titled “25-2 EmployeeRepository に Insert / Update / Delete を実装する”第 23 章で NotImplementedException を返していた Insert と Delete を実装し、新しく Update を追加します。
namespace EmployeeApp;
using Microsoft.Data.SqlClient;using System.Collections.Generic;
public class EmployeeRepository{ private readonly string _connectionString;
public EmployeeRepository(string connectionString) { _connectionString = connectionString; }
public List<Employee> GetAll() { const string sql = @" SELECT e.employee_id, e.last_name, e.first_name, e.email, e.hire_date, e.salary, e.department_id, d.department_name FROM employees e LEFT JOIN departments d ON e.department_id = d.department_id ORDER BY e.employee_id";
using SqlConnection connection = new SqlConnection(_connectionString); connection.Open();
using SqlCommand command = new SqlCommand(sql, connection); return ReadEmployees(command); }
public List<Employee> Search(string keyword, int departmentId) { const string sql = @" SELECT e.employee_id, e.last_name, e.first_name, e.email, e.hire_date, e.salary, e.department_id, d.department_name FROM employees e LEFT JOIN departments d ON e.department_id = d.department_id WHERE (@keyword = '' OR e.last_name LIKE '%' + @keyword + '%' OR e.first_name LIKE '%' + @keyword + '%') AND (@departmentId = -1 OR e.department_id = @departmentId) ORDER BY e.employee_id";
using SqlConnection connection = new SqlConnection(_connectionString); connection.Open();
using SqlCommand command = new SqlCommand(sql, connection); command.Parameters.AddWithValue("@keyword", keyword ?? string.Empty); command.Parameters.AddWithValue("@departmentId", departmentId);
return ReadEmployees(command); }
public int Insert(Employee employee) { const string sql = @" INSERT INTO employees (last_name, first_name, email, hire_date, salary, department_id) OUTPUT INSERTED.employee_id VALUES (@lastName, @firstName, @email, @hireDate, @salary, @departmentId)";
using SqlConnection connection = new SqlConnection(_connectionString); connection.Open();
using SqlCommand command = new SqlCommand(sql, connection); command.Parameters.AddWithValue("@lastName", employee.LastName); command.Parameters.AddWithValue("@firstName", employee.FirstName); command.Parameters.AddWithValue("@email", string.IsNullOrEmpty(employee.Email) ? (object)DBNull.Value : employee.Email); command.Parameters.AddWithValue("@hireDate", employee.HireDate); command.Parameters.AddWithValue("@salary", employee.Salary); command.Parameters.AddWithValue("@departmentId", employee.DepartmentId);
int newId = (int)command.ExecuteScalar(); return newId; }
public int Update(Employee employee) { const string sql = @" UPDATE employees SET last_name = @lastName, first_name = @firstName, email = @email, hire_date = @hireDate, salary = @salary, department_id = @departmentId WHERE employee_id = @employeeId";
using SqlConnection connection = new SqlConnection(_connectionString); connection.Open();
using SqlCommand command = new SqlCommand(sql, connection); command.Parameters.AddWithValue("@employeeId", employee.EmployeeId); command.Parameters.AddWithValue("@lastName", employee.LastName); command.Parameters.AddWithValue("@firstName", employee.FirstName); command.Parameters.AddWithValue("@email", string.IsNullOrEmpty(employee.Email) ? (object)DBNull.Value : employee.Email); command.Parameters.AddWithValue("@hireDate", employee.HireDate); command.Parameters.AddWithValue("@salary", employee.Salary); command.Parameters.AddWithValue("@departmentId", employee.DepartmentId);
int affected = command.ExecuteNonQuery(); return affected; }
public int Delete(int employeeId) { const string sql = "DELETE FROM employees WHERE employee_id = @employeeId";
using SqlConnection connection = new SqlConnection(_connectionString); connection.Open();
using SqlCommand command = new SqlCommand(sql, connection); command.Parameters.AddWithValue("@employeeId", employeeId);
int affected = command.ExecuteNonQuery(); return affected; }
private static List<Employee> ReadEmployees(SqlCommand command) { List<Employee> list = new List<Employee>();
using SqlDataReader reader = command.ExecuteReader();
int idxId = reader.GetOrdinal("employee_id"); int idxLast = reader.GetOrdinal("last_name"); int idxFirst = reader.GetOrdinal("first_name"); int idxEmail = reader.GetOrdinal("email"); int idxHire = reader.GetOrdinal("hire_date"); int idxSalary = reader.GetOrdinal("salary"); int idxDeptId = reader.GetOrdinal("department_id"); int idxDeptName = reader.GetOrdinal("department_name");
while (reader.Read()) { list.Add(new Employee { EmployeeId = reader.GetInt32(idxId), LastName = reader.GetString(idxLast), FirstName = reader.GetString(idxFirst), Email = reader.IsDBNull(idxEmail) ? string.Empty : reader.GetString(idxEmail), HireDate = reader.GetDateTime(idxHire), Salary = reader.IsDBNull(idxSalary) ? 0m : reader.GetDecimal(idxSalary), DepartmentId = reader.IsDBNull(idxDeptId) ? 0 : reader.GetInt32(idxDeptId), DepartmentName = reader.IsDBNull(idxDeptName) ? string.Empty : reader.GetString(idxDeptName) }); }
return list; }}各メソッドのポイント
Section titled “各メソッドのポイント”| メソッド | 戻り値 | ポイント |
|---|---|---|
Insert | 採番された employee_id | OUTPUT INSERTED.employee_id で、INSERT 直後に IDENTITY 値を取得できる |
Update | 影響行数(0 か 1) | ExecuteNonQuery は INSERT/UPDATE/DELETE で使う。戻り値は処理された行数 |
Delete | 影響行数(0 か 1) | WHERE employee_id = @employeeId を 必ず付ける(付け忘れると全件削除) |
ExecuteScalar / ExecuteReader / ExecuteNonQuery の使い分け
Section titled “ExecuteScalar / ExecuteReader / ExecuteNonQuery の使い分け”ここまでで 3 種類のメソッドが出てきました。
| メソッド | 想定する SQL | 戻り値 |
|---|---|---|
ExecuteScalar | 1 行 1 列が返る SELECT(COUNT、MAX 等)、OUTPUT 付き INSERT | 先頭セルの値(object) |
ExecuteReader | 複数行が返る SELECT | SqlDataReader |
ExecuteNonQuery | INSERT / UPDATE / DELETE などデータ変更系 | 影響行数(int) |
Insert で ExecuteScalar を使っているのは、OUTPUT INSERTED.employee_id で 1 つの値(採番された ID)を返してもらっているからです。
DBNull.Value の扱い
Section titled “DBNull.Value の扱い”email 列は NULL を許容します。C# の string で null を AddWithValue に渡すと、内部で パラメータが省略された ように扱われてしまうことがあるため、明示的に DBNull.Value を渡します。
command.Parameters.AddWithValue("@email", string.IsNullOrEmpty(employee.Email) ? (object)DBNull.Value : employee.Email);(object) キャストは、三項演算子の戻り値の型を object に揃えるためです(DBNull.Value と string を直接並べると型推論で混乱します)。
25-3 編集ダイアログ(EmployeeEditForm)を作る
Section titled “25-3 編集ダイアログ(EmployeeEditForm)を作る”社員 1 件分の入力フォームを 別フォーム として作ります。 新規登録と編集の両方で使い回します(モードはコンストラクタで切り替え)。
フォームの追加
Section titled “フォームの追加”ソリューションエクスプローラーで EmployeeApp プロジェクトを右クリック → 追加 → 新しい項目 → 左ペインで「Windows フォーム」を選択 → 「フォーム(Windows フォーム)」 → 名前を EmployeeEditForm.cs にして追加します。
追加すると、EmployeeEditForm.cs、EmployeeEditForm.Designer.cs、EmployeeEditForm.resx の 3 つが自動生成されます(Form1 と同じ仕組み)。
コントロールの配置
Section titled “コントロールの配置”ラベルと入力欄を縦に並べ、いちばん下に「保存」「キャンセル」ボタンを置きます。完成イメージは次のとおりです。
┌─ 社員 編集 ──────────────────────────┐│ ││ 姓 [___________________________]││ 名 [___________________________]││ メール [___________________________]││ 入社日 [______________________ ▼] ││ 給与 [_______________] ││ 部署 [______________________ ▼] ││ ││ [ 保存 ] [キャンセル]││ │└───────────────────────────────────────┘ツールボックスから次のコントロールをフォームに配置し、プロパティ ウィンドウで下表のとおり設定します。Location(左上からの位置)と Size(大きさ)は座標(X, Y)で指定 すると、上のイメージのきれいに揃った配置になります。ラベルの (Name) は既定のままで構いません(表では分かりやすいように名前を付けています)。
| コントロール | (Name) | Text | Location | Size | その他のプロパティ |
|---|---|---|---|---|---|
Form | EmployeeEditForm | 社員 編集 | — | 420, 380 | FormBorderStyle = FixedDialog、MaximizeBox = false、MinimizeBox = false、StartPosition = CenterParent |
Label | labelLastName | 姓 | 24, 24 | 72, 23 | |
TextBox | textBoxLastName | (空) | 110, 21 | 270, 23 | |
Label | labelFirstName | 名 | 24, 60 | 72, 23 | |
TextBox | textBoxFirstName | (空) | 110, 57 | 270, 23 | |
Label | labelEmail | メール | 24, 96 | 72, 23 | |
TextBox | textBoxEmail | (空) | 110, 93 | 270, 23 | |
Label | labelHireDate | 入社日 | 24, 132 | 72, 23 | |
DateTimePicker | dateTimePickerHireDate | — | 110, 129 | 270, 23 | Format = Short |
Label | labelSalary | 給与 | 24, 168 | 72, 23 | |
NumericUpDown | numericUpDownSalary | — | 110, 165 | 150, 23 | Maximum = 99999999、Minimum = 0、DecimalPlaces = 0、ThousandsSeparator = true |
Label | labelDepartment | 部署 | 24, 204 | 72, 23 | |
ComboBox | comboBoxDepartment | — | 110, 201 | 270, 24 | DropDownStyle = DropDownList |
Button | buttonSave | 保存 | 196, 300 | 88, 30 | DialogResult = OK |
Button | buttonCancel | キャンセル | 292, 300 | 88, 30 | DialogResult = Cancel |
座標はあくまで目安です。 多少ずれても動作には影響しません。まずは上の値で置いてみて、見た目が気になれば後から微調整してください。
Labelは文字がはみ出すようならSizeの幅を少し広げます。
buttonCancel.DialogResult を Cancel にしておくと、ボタンを押すだけで ShowDialog() が DialogResult.Cancel を返してフォームが閉じます(コードは不要)。同様に buttonSave.DialogResult を OK にしておくと、保存処理が通ったときに OK が返ります(入力チェックで止めたいときは、後述のコードで DialogResult を一時的に打ち消します)。
EmployeeEditForm.cs を組み立てる
Section titled “EmployeeEditForm.cs を組み立てる”このフォームも、ヘルパー(自分で呼ぶ部品)を先に用意 → イベントを紐付けて中身を書く の順で組み立てます。
ソースを丸ごと貼り付けるのではなく、ステップごとに自分の手で組み立てます。完成形(答え合わせ用)は 25-7 にあります。先に見ず、まず自分で積み上げてみてください。
ステップ1 フィールドとコンストラクタ(新規/編集の切り替え)
Section titled “ステップ1 フィールドとコンストラクタ(新規/編集の切り替え)”このダイアログは 新規登録と編集の両方 で使い回します。編集対象 _target が null なら新規モード、そうでなければ編集モードです。
namespace EmployeeApp;
using Microsoft.Data.SqlClient;
public partial class EmployeeEditForm : Form{ private readonly EmployeeRepository _employeeRepository; private readonly DepartmentRepository _departmentRepository;
// 編集対象。新規モードのときは null。 private readonly Employee _target;
// _target が null かどうかで「新規モード」を判定する private bool IsNewMode => _target == null;
public EmployeeEditForm( EmployeeRepository employeeRepository, DepartmentRepository departmentRepository, Employee target) { InitializeComponent(); _employeeRepository = employeeRepository; _departmentRepository = departmentRepository; _target = target; }}| 書いたもの | 意図 |
|---|---|
_target フィールド | 編集対象の社員。null なら新規登録モード |
IsNewMode プロパティ | _target == null で新規/編集を 1 か所で判定 |
| Repository をコンストラクタで受け取る | ダイアログが直接 DB を触らず、親と同じ Repository を使い回す |
ビルドの状態:通ります。
ステップ2 入力チェックのヘルパー(ValidateInput)
Section titled “ステップ2 入力チェックのヘルパー(ValidateInput)”保存前に、必須項目やメール形式をチェックする部品を先に用意します。bool と out string error で「OK か」+「NG の理由」を一緒に返すパターンです。
private bool ValidateInput(out string error){ if (string.IsNullOrWhiteSpace(textBoxLastName.Text)) { error = "「姓」は必須です。"; return false; } if (string.IsNullOrWhiteSpace(textBoxFirstName.Text)) { error = "「名」は必須です。"; return false; }
string email = textBoxEmail.Text.Trim(); if (!string.IsNullOrEmpty(email) && !email.Contains('@')) { error = "「メール」は @ を含む形式で入力してください。"; return false; }
if (comboBoxDepartment.SelectedValue == null) { error = "「部署」を選択してください。"; return false; }
error = string.Empty; return true;}メール形式は
@が含まれるかだけの「ざっくり」チェックで十分です(厳密には正規表現が必要)。
ステップ3 ダイアログを開いたときの初期化(Load イベント)
Section titled “ステップ3 ダイアログを開いたときの初期化(Load イベント)”イベントの紐付け:デザイナでフォームの空白部分をクリック → 稲妻アイコン → Load をダブルクリック。
EmployeeEditForm_Load の中で、部署プルダウンを用意し、編集モードなら既存の値を画面に流し込みます。
private void EmployeeEditForm_Load(object sender, EventArgs e){ Text = IsNewMode ? "社員 新規登録" : $"社員 編集 (ID:{_target.EmployeeId})";
// 部署プルダウンを初期化 List<Department> departments = _departmentRepository.GetAll(); comboBoxDepartment.DataSource = departments; comboBoxDepartment.DisplayMember = nameof(Department.DepartmentName); comboBoxDepartment.ValueMember = nameof(Department.DepartmentId);
// 既存値を画面に流し込む(編集モード) if (!IsNewMode) { textBoxLastName.Text = _target.LastName; textBoxFirstName.Text = _target.FirstName; textBoxEmail.Text = _target.Email; dateTimePickerHireDate.Value = _target.HireDate; numericUpDownSalary.Value = _target.Salary; comboBoxDepartment.SelectedValue = _target.DepartmentId; } else { // 新規モード:今日を入社日のデフォルトに dateTimePickerHireDate.Value = DateTime.Today; }}ステップ4 保存ボタン(buttonSave の Click)
Section titled “ステップ4 保存ボタン(buttonSave の Click)”イベントの紐付け:デザイナで buttonSave を ダブルクリック。buttonCancel は コードを書きません(DialogResult プロパティを Cancel に設定するだけで閉じます)。
保存ボタンでは、①入力チェック → ②画面の値から Employee を組み立てる → ③新規なら Insert、編集なら Update を呼びます。新規と編集はこの保存処理で 分かれます。流れを図にすると次のとおりです。
入力エラーや更新失敗のときは
DialogResult = Noneにして フォームを閉じない(入力をやり直せる)のがポイントです。Insert/UpdateでSqlExceptionが起きた場合も、catchで DB エラーを表示してDialogResult = Noneにします(図では省略)。
private void buttonSave_Click(object sender, EventArgs e){ if (!ValidateInput(out string error)) { MessageBox.Show(error, "入力チェック", MessageBoxButtons.OK, MessageBoxIcon.Warning); DialogResult = DialogResult.None; // フォームを閉じない return; }
Employee employee = new Employee { EmployeeId = IsNewMode ? 0 : _target.EmployeeId, LastName = textBoxLastName.Text.Trim(), FirstName = textBoxFirstName.Text.Trim(), Email = textBoxEmail.Text.Trim(), HireDate = dateTimePickerHireDate.Value.Date, Salary = numericUpDownSalary.Value, DepartmentId = (int)comboBoxDepartment.SelectedValue };
try { if (IsNewMode) { int newId = _employeeRepository.Insert(employee); MessageBox.Show($"新規登録しました(ID:{newId})。", "完了", MessageBoxButtons.OK, MessageBoxIcon.Information); } else { int affected = _employeeRepository.Update(employee); if (affected == 0) { MessageBox.Show("更新対象の社員が見つかりませんでした。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning); DialogResult = DialogResult.None; return; } MessageBox.Show("更新しました。", "完了", MessageBoxButtons.OK, MessageBoxIcon.Information); }
// 入力チェックを通り保存できたら、DialogResult = OK のままフォームが閉じる } catch (SqlException ex) { MessageBox.Show( $"SQLServer 関連のエラーが発生しました。\nエラー番号:{ex.Number}\nメッセージ:{ex.Message}", "DB エラー", MessageBoxButtons.OK, MessageBoxIcon.Error); DialogResult = DialogResult.None; }}| ポイント | 説明 |
|---|---|
_target == null で新規モードを判定 | 1 つのフォームで新規/編集を兼ねる(コードが半分で済む) |
DialogResult = DialogResult.None | 入力エラー時にフォームを 閉じさせない テクニック |
EmployeeId = IsNewMode ? 0 : _target.EmployeeId | 新規は 0(採番は DB 任せ)、編集は既存 ID |
NumericUpDown / DateTimePicker | 給与・入社日は専用部品で誤入力を防ぐ |
25-4 Form1 を仕上げる
Section titled “25-4 Form1 を仕上げる”第 24 章で作った検索つきの Form1 を 土台 にして、新規登録・編集・削除のイベントを足していきます。土台がない場合は、第 24 章の Form1.cs(24-9 の完成形)を用意してから始めてください。
ソースを丸ごと貼り付けるのではなく、ステップごとに自分の手で組み立てます。完成形(答え合わせ用)は 25-7 にあります。先に見ず、まず自分で積み上げてみてください。
まず画面(デザイナ)を整える
Section titled “まず画面(デザイナ)を整える”| 追加点 | 操作 |
|---|---|
buttonNew を有効化 | デザイナで Enabled を true に変更 |
buttonEdit を新設 | Button を 1 つ追加。Name = buttonEdit、Text = 編集、Location = 580, 462、Size = 90, 28、Anchor = Bottom, Right(buttonNew の左隣) |
buttonDelete を有効化 | デザイナで Enabled を true に変更 |
イベントの紐付けは、次の各ステップで機能を書くのと一緒に行います。
ステップ1 選択中の社員を取り出すヘルパー(GetSelectedEmployee)
Section titled “ステップ1 選択中の社員を取り出すヘルパー(GetSelectedEmployee)”編集・削除では「いま一覧で選ばれている社員」が必要です。これを取り出す部品を先に用意します。
private Employee GetSelectedEmployee(){ if (dataGridViewEmployees.CurrentRow == null) { return null; } return dataGridViewEmployees.CurrentRow.DataBoundItem as Employee;}
DataBoundItemの便利さ
CurrentRow.DataBoundItem as Employeeは「選択中の行に紐づいた 元データ(Employee) を取り出す」書き方です。DataSourceにList<Employee>をバインドしているので、セルを 1 つずつ読み直さなくてもEmployeeをそのまま取り出せます。
ステップ2 新規登録ボタン(buttonNew の Click)
Section titled “ステップ2 新規登録ボタン(buttonNew の Click)”イベントの紐付け:デザイナで buttonNew を ダブルクリック。
編集ダイアログを 新規モード(target: null)で開き、保存(DialogResult.OK)されたら一覧を再読み込みします。
新規登録と編集は ほとんど同じ流れ です(違いは「開く前に行の選択が要るか」と、target に渡す相手だけ)。次の図でまとめて見ておきましょう。
ダイアログを開いたあとの 保存処理の中身(入力チェック →
Insert/Update)は、25-3 ステップ4 の「保存ボタンの流れ」の図を参照してください。ここ(Form1側)は「ダイアログを開く → OK なら再読み込み」までが担当です。
private void buttonNew_Click(object sender, EventArgs e){ using EmployeeEditForm dialog = new EmployeeEditForm(_employeeRepository, _departmentRepository, target: null); if (dialog.ShowDialog(this) == DialogResult.OK) { LoadEmployees(); }}
ShowDialogのポイント
ShowDialog(this)は モーダル表示(編集中は親画面を操作できない)。usingを付けると、閉じたときに自動でDispose(リソース解放)されます。== DialogResult.OKのときだけ一覧を再読み込みします。
ステップ3 編集ボタン(buttonEdit の Click)
Section titled “ステップ3 編集ボタン(buttonEdit の Click)”イベントの紐付け:デザイナで buttonEdit を ダブルクリック。
選択中の社員を取り出し、編集モード(target: selected)でダイアログを開きます。未選択なら案内を出して終了します。流れはステップ2の「新規登録・編集の流れ」の図の 編集側 にあたります。
private void buttonEdit_Click(object sender, EventArgs e){ Employee selected = GetSelectedEmployee(); if (selected == null) { MessageBox.Show("編集する社員を選んでください。", "情報", MessageBoxButtons.OK, MessageBoxIcon.Information); return; }
using EmployeeEditForm dialog = new EmployeeEditForm(_employeeRepository, _departmentRepository, target: selected); if (dialog.ShowDialog(this) == DialogResult.OK) { LoadEmployees(); }}ステップ4 削除ボタン(buttonDelete の Click)
Section titled “ステップ4 削除ボタン(buttonDelete の Click)”イベントの紐付け:デザイナで buttonDelete を ダブルクリック。
選択チェック → 確認ダイアログ → OK なら Delete を呼び、一覧を再読み込みします。
削除は事故が起きやすい操作なので、コードを書く前に 処理の流れ(どこで止めて、どこで分岐するか) を図で押さえておきましょう。
図の ひし形(◇)が分岐 です。「未選択なら止める」「確認でキャンセルなら止める」「
WHERE付きのDELETE」「影響行数で成否を判断」という、削除を安全に行うための要所が分岐になっています。なおDeleteの呼び出しでSqlExceptionが起きた場合は、catchでまとめてShowErrorに渡します(図では省略)。
private void buttonDelete_Click(object sender, EventArgs e){ Employee selected = GetSelectedEmployee(); if (selected == null) { MessageBox.Show("削除する社員を選んでください。", "情報", MessageBoxButtons.OK, MessageBoxIcon.Information); return; }
DialogResult result = MessageBox.Show( $"社員番号 {selected.EmployeeId}({selected.FullName})を削除します。よろしいですか?", "削除確認", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning);
if (result != DialogResult.OK) { return; }
try { int affected = _employeeRepository.Delete(selected.EmployeeId); if (affected == 0) { MessageBox.Show("削除対象の社員が見つかりませんでした。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning); } else { MessageBox.Show("削除しました。", "完了", MessageBoxButtons.OK, MessageBoxIcon.Information); } LoadEmployees(); } catch (SqlException ex) { ShowError(ex); }}
WHERE句を絶対に忘れない
Deleteの SQL(25-2)にはWHERE employee_id = @employeeIdが必ず付いています。これを書き忘れると 全件削除 になります。削除前に確認ダイアログを出しているのも、事故を防ぐためです。
ステップ5 行のダブルクリックでも編集(CellDoubleClick)
Section titled “ステップ5 行のダブルクリックでも編集(CellDoubleClick)”イベントの紐付け:デザイナで dataGridViewEmployees をクリック → 稲妻アイコン → CellDoubleClick をダブルクリック。
行をダブルクリックしたら、編集ボタンと同じ処理を呼びます。列ヘッダー(e.RowIndex < 0)は無視します。
private void dataGridViewEmployees_CellDoubleClick(object sender, DataGridViewCellEventArgs e){ if (e.RowIndex < 0) { // 列ヘッダーがダブルクリックされた場合 return; } buttonEdit_Click(sender, e);}ここまでで CRUD がそろいます。実行(F5)して、次の 25-5 のシナリオで確かめてください。
25-5 動作確認
Section titled “25-5 動作確認”次のシナリオで動作を確認します。
| シナリオ | 期待する結果 |
|---|---|
| 新規登録ボタン → 全項目入力 → 保存 | 一覧に追加され、新規 ID が採番されて表示 |
| 新規登録ボタン → 姓を空のまま保存 | 入力エラーで閉じられない、警告メッセージ |
| 行をダブルクリック → 値を変更 → 保存 | 一覧に変更が反映される |
| 編集中にキャンセル | 変更されない |
| 削除ボタン → OK | 一覧から消える |
| 削除ボタン → キャンセル | 何も起きない |
| 検索結果から編集 → 保存 | 一覧が再読み込みされる(検索条件はクリア) |
| メールにシングルクォートを入れて保存 | エラーにならず保存される(パラメータ化クエリの確認) |
検索条件を保ったまま再読み込みしたい場合
上の表では、編集後は
LoadEmployees()(全件)で再読み込みしています。 検索条件を維持したい場合は、SearchEmployees()を呼ぶようにbuttonNew_Click/buttonEdit_Click/buttonDelete_Clickの最後で分岐します。発展課題で扱います。
25-6 トランザクション(参考)
Section titled “25-6 トランザクション(参考)”この章では扱いませんが、複数の INSERT / UPDATE / DELETE を 「すべて成功するか、すべて失敗するか」 という形にまとめたい場面があります。
using SqlConnection connection = new SqlConnection(_connectionString);connection.Open();
using SqlTransaction transaction = connection.BeginTransaction();try{ // 複数のコマンドを実行... transaction.Commit();}catch{ transaction.Rollback(); throw;}これは トランザクション という機能で、たとえば「社員を異動するときに、旧部署の担当を外して新部署の担当を増やす」など、複数の更新を 1 つの単位で扱う場合に使います。 現場では頻出ですが、まずは単発の CRUD を確実にできるようになることが優先です。本研修ではこの章では深入りしません。
25-7 完成形の確認(答え合わせ用)
Section titled “25-7 完成形の確認(答え合わせ用)”まずは 25-3・25-4 のステップを自分で積み上げてから見てください
25-3(編集ダイアログ)・25-4(Form1)のステップをすべて終えたあとの完成形です。詰まったときの 答え合わせ に使ってください。
EmployeeEditForm.cs(完成形)
Section titled “EmployeeEditForm.cs(完成形)”namespace EmployeeApp;
using Microsoft.Data.SqlClient;
public partial class EmployeeEditForm : Form{ private readonly EmployeeRepository _employeeRepository; private readonly DepartmentRepository _departmentRepository;
// 編集対象。新規モードのときは null。 private readonly Employee _target;
// _target が null かどうかで「新規モード」を判定する private bool IsNewMode => _target == null;
public EmployeeEditForm( EmployeeRepository employeeRepository, DepartmentRepository departmentRepository, Employee target) { InitializeComponent(); _employeeRepository = employeeRepository; _departmentRepository = departmentRepository; _target = target; }
private void EmployeeEditForm_Load(object sender, EventArgs e) { Text = IsNewMode ? "社員 新規登録" : $"社員 編集 (ID:{_target.EmployeeId})";
// 部署プルダウンを初期化 List<Department> departments = _departmentRepository.GetAll(); comboBoxDepartment.DataSource = departments; comboBoxDepartment.DisplayMember = nameof(Department.DepartmentName); comboBoxDepartment.ValueMember = nameof(Department.DepartmentId);
// 既存値を画面に流し込む(編集モード) if (!IsNewMode) { textBoxLastName.Text = _target.LastName; textBoxFirstName.Text = _target.FirstName; textBoxEmail.Text = _target.Email; dateTimePickerHireDate.Value = _target.HireDate; numericUpDownSalary.Value = _target.Salary; comboBoxDepartment.SelectedValue = _target.DepartmentId; } else { // 新規モード:今日を入社日のデフォルトに dateTimePickerHireDate.Value = DateTime.Today; } }
private void buttonSave_Click(object sender, EventArgs e) { if (!ValidateInput(out string error)) { MessageBox.Show(error, "入力チェック", MessageBoxButtons.OK, MessageBoxIcon.Warning); DialogResult = DialogResult.None; // フォームを閉じない return; }
Employee employee = new Employee { EmployeeId = IsNewMode ? 0 : _target.EmployeeId, LastName = textBoxLastName.Text.Trim(), FirstName = textBoxFirstName.Text.Trim(), Email = textBoxEmail.Text.Trim(), HireDate = dateTimePickerHireDate.Value.Date, Salary = numericUpDownSalary.Value, DepartmentId = (int)comboBoxDepartment.SelectedValue };
try { if (IsNewMode) { int newId = _employeeRepository.Insert(employee); MessageBox.Show($"新規登録しました(ID:{newId})。", "完了", MessageBoxButtons.OK, MessageBoxIcon.Information); } else { int affected = _employeeRepository.Update(employee); if (affected == 0) { MessageBox.Show("更新対象の社員が見つかりませんでした。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning); DialogResult = DialogResult.None; return; } MessageBox.Show("更新しました。", "完了", MessageBoxButtons.OK, MessageBoxIcon.Information); } } catch (SqlException ex) { MessageBox.Show( $"SQLServer 関連のエラーが発生しました。\nエラー番号:{ex.Number}\nメッセージ:{ex.Message}", "DB エラー", MessageBoxButtons.OK, MessageBoxIcon.Error); DialogResult = DialogResult.None; } }
private bool ValidateInput(out string error) { if (string.IsNullOrWhiteSpace(textBoxLastName.Text)) { error = "「姓」は必須です。"; return false; } if (string.IsNullOrWhiteSpace(textBoxFirstName.Text)) { error = "「名」は必須です。"; return false; }
string email = textBoxEmail.Text.Trim(); if (!string.IsNullOrEmpty(email) && !email.Contains('@')) { error = "「メール」は @ を含む形式で入力してください。"; return false; }
if (comboBoxDepartment.SelectedValue == null) { error = "「部署」を選択してください。"; return false; }
error = string.Empty; return true; }}Form1.cs(完成形)
Section titled “Form1.cs(完成形)”namespace EmployeeApp;
using Microsoft.Data.SqlClient;
public partial class Form1 : Form{ private const string ConnectionString = "Server=localhost;Database=TrainingDB;Integrated Security=true;TrustServerCertificate=true;";
private const int AllDepartments = -1;
private readonly EmployeeRepository _employeeRepository; private readonly DepartmentRepository _departmentRepository;
public Form1() { InitializeComponent(); _employeeRepository = new EmployeeRepository(ConnectionString); _departmentRepository = new DepartmentRepository(ConnectionString); }
private void Form1_Load(object sender, EventArgs e) { InitializeDepartmentCombo(); LoadEmployees(); }
private void buttonReload_Click(object sender, EventArgs e) { LoadEmployees(); }
private void buttonSearch_Click(object sender, EventArgs e) { SearchEmployees(); }
private void buttonClear_Click(object sender, EventArgs e) { textBoxKeyword.Text = string.Empty; comboBoxDepartment.SelectedValue = AllDepartments; LoadEmployees(); }
private void buttonNew_Click(object sender, EventArgs e) { using EmployeeEditForm dialog = new EmployeeEditForm(_employeeRepository, _departmentRepository, target: null); if (dialog.ShowDialog(this) == DialogResult.OK) { LoadEmployees(); } }
private void buttonEdit_Click(object sender, EventArgs e) { Employee selected = GetSelectedEmployee(); if (selected == null) { MessageBox.Show("編集する社員を選んでください。", "情報", MessageBoxButtons.OK, MessageBoxIcon.Information); return; }
using EmployeeEditForm dialog = new EmployeeEditForm(_employeeRepository, _departmentRepository, target: selected); if (dialog.ShowDialog(this) == DialogResult.OK) { LoadEmployees(); } }
private void buttonDelete_Click(object sender, EventArgs e) { Employee selected = GetSelectedEmployee(); if (selected == null) { MessageBox.Show("削除する社員を選んでください。", "情報", MessageBoxButtons.OK, MessageBoxIcon.Information); return; }
DialogResult result = MessageBox.Show( $"社員番号 {selected.EmployeeId}({selected.FullName})を削除します。よろしいですか?", "削除確認", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning);
if (result != DialogResult.OK) { return; }
try { int affected = _employeeRepository.Delete(selected.EmployeeId); if (affected == 0) { MessageBox.Show("削除対象の社員が見つかりませんでした。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning); } else { MessageBox.Show("削除しました。", "完了", MessageBoxButtons.OK, MessageBoxIcon.Information); } LoadEmployees(); } catch (SqlException ex) { ShowError(ex); } }
private void dataGridViewEmployees_CellDoubleClick(object sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0) { // 列ヘッダーがダブルクリックされた場合 return; } buttonEdit_Click(sender, e); }
private Employee GetSelectedEmployee() { if (dataGridViewEmployees.CurrentRow == null) { return null; } return dataGridViewEmployees.CurrentRow.DataBoundItem as Employee; }
private void InitializeDepartmentCombo() { try { List<Department> departments = _departmentRepository.GetAll(); departments.Insert(0, new Department { DepartmentId = AllDepartments, DepartmentName = "(すべての部署)" });
comboBoxDepartment.DataSource = departments; comboBoxDepartment.DisplayMember = nameof(Department.DepartmentName); comboBoxDepartment.ValueMember = nameof(Department.DepartmentId); comboBoxDepartment.SelectedValue = AllDepartments; } catch (Exception ex) { ShowError(ex); } }
private void LoadEmployees() { try { List<Employee> list = _employeeRepository.GetAll(); dataGridViewEmployees.DataSource = list; } catch (Exception ex) { ShowError(ex); } }
private void SearchEmployees() { try { string keyword = textBoxKeyword.Text.Trim(); int departmentId = (int)comboBoxDepartment.SelectedValue; List<Employee> list = _employeeRepository.Search(keyword, departmentId); dataGridViewEmployees.DataSource = list; } catch (Exception ex) { ShowError(ex); } }
private static void ShowError(Exception ex) { string title = ex is SqlException ? "DB エラー" : "エラー"; string message = ex is SqlException sqlEx ? $"SQLServer 関連のエラーが発生しました。\nエラー番号:{sqlEx.Number}\nメッセージ:{sqlEx.Message}" : $"予期しないエラーが発生しました。\n内容:{ex.Message}";
MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Error); }}よくあるつまずき
Section titled “よくあるつまずき”| 症状 | 原因 | 対処 |
|---|---|---|
Insert で SqlException (Number=2627) | 重複した email(UNIQUE 制約違反) | 別のメールに変える |
Insert で SqlException (Number=547) | 存在しない department_id を指定 | 部署プルダウンの値が正しいか確認 |
| 保存しても一覧が変わらない | 戻った後に LoadEmployees を呼んでいない | if (dialog.ShowDialog() == DialogResult.OK) の中で呼ぶ |
| 入力エラーでもダイアログが閉じる | DialogResult = DialogResult.None を設定していない | Save の検証失敗時に明示的に None を代入 |
| 削除ボタンが押せたが効かない | WHERE 句の typo | SQL を SSMS でコピペ実行して確認 |
comboBoxDepartment.SelectedValue が null で例外 | DataSource 設定前に値を読んでいる | Load イベントで先に DataSource を設定 |
| 編集後に検索条件が消える | 編集後の再読み込みで GetAll を呼んでいる | 検索条件を保持したいなら SearchEmployees を呼ぶ(発展) |
| 行をダブルクリックすると編集と削除が両方走る | CellDoubleClick 内で buttonDelete_Click まで呼んでいる | buttonEdit_Click だけ呼ぶ |
学んだことチェック
Section titled “学んだことチェック”-
INSERT/UPDATE/DELETEをパラメータ化クエリで書ける -
OUTPUT INSERTED.列名でIDENTITY採番後の値を取得できる -
ExecuteNonQueryの戻り値の意味を説明できる -
ShowDialogでモーダルな別フォームを開ける - フォーム間でデータをコンストラクタ + プロパティで受け渡せる
-
DialogResultで OK / Cancel を判定できる - 入力チェックを書ける(必須項目・形式・選択チェック)
- 削除前に確認ダイアログを出せる
- 戻った後の再読み込みパターン(
if (dialog.ShowDialog() == DialogResult.OK) ...)を書ける -
DataGridView.CurrentRow.DataBoundItemで選択行の元データを取り出せる
ExecuteScalar/ExecuteReader/ExecuteNonQueryの使い分けを 1 つずつ例で説明してください。OUTPUT INSERTED.employee_idがない場合、Insertの戻り値はどうなりますか?ShowDialogとShowの違いを説明してください。- 編集ダイアログで「保存」を押したのに閉じないことがあります。なぜですか?
DialogResult = DialogResult.Noneはどんな場面で使いますか?- 削除前に確認ダイアログを出す理由を 2 つ挙げてください。
- CRUD の各操作で 影響行数 を確認することの意味は何ですか?
第 25 章も、第 23・24 章と同じ チームで自走するハンズオン形式 で進めます。 チームの役割分担(リーダー / 技術部長 / タイムキーパー)と「自走のすすめ」は 第 17 章「ここからはチームで進める」、ミニ発表の進め方は 第 23 章「演習課題」 を参照してください。本文を手順書として、ペア・チームで確認し合いながら、自分たちのペースでアプリを組み上げます(提供ソースを使うので、できあがる CRUD アプリは全員ほぼ同じ動作になります)。
この章で社員管理アプリは CRUD がそろった、一応の完成形 になります。まずは動かして「自分のアプリができた」を実感してください。そのうえで、第 23・24 章と同じく 必須課題は、できあがったソースを読み解いて「なぜそう書くのか」をコメントとして書き残す 作業です。CRUD・画面遷移・入力チェックという盛りだくさんのコードを、1 つずつ意味を確かめながら読み解きましょう。
この章の進め方
Section titled “この章の進め方”- チームで本文 25-2〜25-7 の 実装ステップ に沿って、CRUD アプリを組み上げる(= 追加・編集・削除ができる)。25-3・25-4 のステップは自分の手で。詰まったら 25-7 の完成形で答え合わせ
- 「25-5 動作確認」のシナリオで、新規登録・編集・削除・入力チェックがひととおり動くことを確かめる
- 【必須課題 25-1】 できあがったソースに「なぜ」コメントを付ける
- 【発展課題 25-2 / 25-3】 余裕があれば機能を追加する
- タイムキーパーの合図で手を止め、チーム内で ミニ発表(下記)を行う
必須課題は、第 23・24 章から続けている EmployeeApp プロジェクト にそのまま書き込みます(「なぜ」コメントも同じプロジェクト)。これで社員管理アプリ(EmployeeApp)が一応の完成形になります。発展課題だけは、同じ KadaiWinFormsApp ソリューション内に 別プロジェクト として作ります。
| 課題 | プロジェクト | 内容 |
|---|---|---|
| 課題 25-1(必須) | EmployeeApp(本文の続き=完成) | CRUD アプリ本体 + 「なぜ」コメント |
| 課題 25-2(発展) | Ext_KeepSearch(新規) | 編集後も検索条件を保つ |
| 課題 25-3(発展) | Ext_BulkSalaryUp(新規) | 給与の一括引き上げ |
ミニ発表(成果の共有)
Section titled “ミニ発表(成果の共有)”社員管理アプリが完成したら、チーム内で一人ずつ ごく簡単に発表 します。最後の仕上げの章なので、完成したアプリを動かして見せる のがメインです。
- デモ:社員の 追加 → 編集 → 削除 を実際に動かして見せる(一連の CRUD が動くこと)
- 1 問説明:自分が付けた「なぜ」コメントから 1 つ、または上の「ペア確認」から 1 つを選び、自分の言葉で説明する(
ShowDialog/DialogResult、OUTPUT INSERTEDでの採番、ExecuteNonQueryの影響行数 など) - 一言:コメントを書いていて一番「なるほど」と思った点、または詰まったポイントを一言
第 23〜25 章をやりきった自分をねぎらう
一覧 → 検索 → 編集と 3 章かけて、DB とつながる業務アプリを 1 つ作り上げました。ミニ発表は、その成果を言葉にして確かめる場です。うまく説明できなかったところは、チームで埋め合わせましょう。
課題 25-1 ソースを読み解いて「なぜ」コメントを付ける
Section titled “課題 25-1 ソースを読み解いて「なぜ」コメントを付ける”本文 25-2〜25-7 の実装ステップで組み上げた CRUD アプリ(EmployeeApp)の Form1.cs・EmployeeEditForm.cs・EmployeeRepository.cs を読み返し、第 23 章の課題 23-1 と同じ要領 で、次の 2 種類のコメントを書き込んでください。
- (A) メソッドの役割:各メソッドの 上の行 に、何をするメソッドかを 1 行(
//)で書く - (B) 難所の「なぜ」:下の表の各箇所に、「なぜそう書くのか」「何のためか」 を 前の行 に自分の言葉で書く(言い換えコメントは NG。→ 第 23 章「課題 23-1」・第 7 章「コラム:コメントの書き方」)
CRUD・画面遷移・入力チェックと盛りだくさんなので、全部を完璧に書ききる必要はありません。表の中から、チームで分担したり、自分が「なるほど」と思えた箇所を選んで、確実に自分の言葉にしましょう(目安は 各ファイル 3 箇所以上)。
(B) 「なぜ」コメントを付ける箇所
| ファイル | 箇所 | 説明する観点(= ここに「なぜ」を書く) |
|---|---|---|
EmployeeRepository.cs | OUTPUT INSERTED.employee_id | なぜこれを書くと採番された ID が取れるのか |
EmployeeRepository.cs | Insert で ExecuteScalar を使う | なぜ ExecuteNonQuery ではなく ExecuteScalar か |
EmployeeRepository.cs | Update/Delete の ExecuteNonQuery の戻り値 | 戻り値(影響行数)は何を意味するか |
EmployeeRepository.cs | DELETE ... WHERE employee_id = @employeeId の WHERE | なぜ WHERE が必須か(無いとどうなるか) |
EmployeeRepository.cs | (object)DBNull.Value の三項演算子 | なぜ null ではなく DBNull.Value を渡すのか |
EmployeeEditForm.cs | IsNewMode => _target == null | なぜ 1 つのフォームで新規と編集を兼ねられるのか |
EmployeeEditForm.cs | DialogResult = DialogResult.None | なぜこれでフォームが閉じなくなるのか |
EmployeeEditForm.cs | EmployeeId = IsNewMode ? 0 : _target.EmployeeId | なぜ新規のとき 0 でよいのか |
Form1.cs | dialog.ShowDialog(this) == DialogResult.OK | なぜ「保存されたとき」だけ再読み込みするのか |
Form1.cs | using EmployeeEditForm dialog = ... | なぜ using を付けるのか |
Form1.cs | CurrentRow.DataBoundItem as Employee | なぜこれで選択行の Employee が取れるのか |
Form1.cs | 削除前の MessageBox(確認ダイアログ) | なぜ削除の前に確認を挟むのか |
確認すること
- (A) 各メソッドの上に「役割」を 1 行コメントした
- (B) 表から各ファイル 3 箇所以上に「なぜ」コメントを前行で書いた
- とくに
WHEREを忘れると全件削除になる 理由を自分の言葉で書けた - 言い換えコメントになっていない/本文の解説の丸写しになっていない
- CRUD アプリが動く(追加・編集・削除・入力チェック)
課題 25-2 編集後も検索条件を保つ
Section titled “課題 25-2 編集後も検索条件を保つ”KadaiWinFormsApp ソリューションに新しいプロジェクト Ext_KeepSearch を作成し、EmployeeApp のコードをコピーした上で、編集後・削除後・新規登録後に検索条件を保ったまま 一覧を再読み込みするように改修してください。
仕様
- 編集後、検索キーワード・部署プルダウンが空でなければ、
SearchEmployeesを呼ぶ - 編集後、検索条件がクリア状態なら
LoadEmployees(全件)を呼ぶ - 新規登録した社員が検索条件に合わないと一覧に出てこないことがある。その場合のヒントを
MessageBoxで出してもよい(任意)
課題 25-3 給与の一括引き上げ
Section titled “課題 25-3 給与の一括引き上げ”KadaiWinFormsApp ソリューションに新しいプロジェクト Ext_BulkSalaryUp を作成し(EmployeeApp のコードをコピー)、選択した部署の全社員の給与を 1 万円上げる ボタンを追加してください。
仕様
- 画面に「[部署一括 1 万円アップ]」ボタンを追加
- 押すと「
comboBoxDepartmentで選択された部署」の全社員のsalaryを 10000 増やす - 確認ダイアログ → OK で実行 → 完了メッセージ(影響行数を表示)
- 「(すべての部署)」が選ばれていたら、確認ダイアログで強く警告(「全社員に適用されます」)
- パラメータ化クエリで
UPDATE employees SET salary = salary + 10000 WHERE ...
ヒント:
EmployeeRepositoryにBulkSalaryUp(int departmentId)メソッドを追加し、ExecuteNonQueryの戻り値を表示する
提出前チェックリスト
Section titled “提出前チェックリスト”- 全プロジェクトが
KadaiWinFormsAppソリューションに入っている - 各プロジェクトで Nullable を disable にしている
-
Microsoft.Data.SqlClientを NuGet で追加した - パラメータ化クエリで書いている
- 入力チェックで必須項目が空のときに保存できないことを確認した
- 各メソッドの上に「役割」を 1 行コメントした
- 各ファイル 3 箇所以上に「なぜ」コメントを前行で書いた(
WHERE必須の理由を含む) -
SqlExceptionをキャッチしてMessageBoxでエラーを表示する - 削除前の確認ダイアログを出している
-
bin・obj・.vsフォルダが Git 管理に入っていない - チーム内でミニ発表(CRUD のデモ + 1 問説明 + 一言)を行った
Git への提出
Section titled “Git への提出”完成したところまでを保存して提出します(タイマーはありません。自分のペースで区切りのよいところまで)。
git statusgit add .git commit -m "Chapter25: CRUDアプリ完成+なぜコメント / <一番なるほどと思った点>"git push origin main提出方法:Git が使えないときはサーバへコピー
Git の調子が悪いときは、講師の指示で
pushの代わりにKadaiWinFormsAppフォルダをサーバ上の自分のフォルダへコピーして提出します。 その場合は、コミットメッセージの代わりに、提出先へエクスプローラーの右クリック →「新規作成」→「テキスト ドキュメント」で提出メモ.txtを作り、「どこまで完成したか」「詰まったポイント」を書いておいてください。
Git の詳しい操作は、付録 C「Git のインストールと提出ルール」 を参照してください。
この章のまとめ
Section titled “この章のまとめ”INSERT/UPDATE/DELETEも パラメータ化クエリ で書く(検索と同じ)OUTPUT INSERTED.列名で IDENTITY 採番後の値を取得できるExecuteNonQueryの戻り値は影響行数。0 行のときは「対象が見つからなかった」と判断できる- 別フォームは
ShowDialogでモーダル表示、DialogResultで OK / Cancel を判定する - フォーム間のデータ受け渡しは コンストラクタで渡す のが基本
- 入力チェックに失敗したら
DialogResult.Noneでフォームを閉じない - 削除は 必ず確認ダイアログ、影響行数を確認、
WHERE句を絶対に忘れない DataGridView.CurrentRow.DataBoundItemで選択行の元データに直接アクセスできる
これで Windows フォーム社員管理アプリは CRUD が一通り揃った業務アプリの最小完成形 になりました。
ここまで第 18〜25 章で Windows フォームの基本から CRUD まで一気に作りました。 次章からは Web アプリケーション の世界に入ります。
第 26 章 「ASP.NET Core MVC 入門(占いアプリ)」 では、DB を使わない簡単な占いアプリを作りながら MVC(Model-View-Controller)の流れを身に付けます。 名前と生年月日を入れると、ラッキーカラー・ラッキーナンバー・今日の一言が返ってくる、楽しめる題材です。
Windows フォームでは「画面が常に表示されていて、ボタンを押すとイベントが走る」流れでしたが、Web では「ブラウザがリクエストを送ると、サーバーが HTML を返す」流れに切り替わります。 この イベント駆動 vs リクエスト駆動 の違いを意識しながら進めてください。