WinFormsでユーザー登録フォームを作っていると、TextBoxから値を渡すだけのつもりが、IDやカテゴリなど数値項目でコンパイルエラーに遭遇することがあります。多くはTextBox.Textがstringであること、そしてIdentity主キーや外部キーの扱いが設計と噛み合っていないことが原因です。本記事では3層構成を前提に、登録/更新が安定する設計と実装パターン、さらにMicrosoft Learn Q&Aでタグを誤ると起きる問題まで整理します。
症状は「DBのエラー」ではなく「型の不一致でビルドが止まる」
フォームの「保存」処理で InsertarUsuario(...) や「編集」処理で EditarUsuario(IDUsuario) を呼んだ瞬間にエラーが出ると、SQL Serverやストアドプロシージャ側を疑いがちです。しかし、今回のようにコンパイルエラーで止まっている場合、原因はほぼC#側にあります。つまり、まだDBには到達していません。
よくある根本原因は次の2系統です。
- TextBoxの
.Text(string)を、intを要求する引数にそのまま渡している - メソッドの引数の「個数」「順番」「型」が、呼び出し側と一致していない(オーバーロード増殖・設計の混在など)
| 典型的なコンパイルエラー | 意味 | 対処の方向性 |
|---|---|---|
| CS1503: 引数 1: ‘string’ から ‘int’ へ変換できません | 型が合っていない | 呼び出し前に int.TryParse で変換し、失敗時は入力エラーとして弾く |
| CS7036: 必須パラメーター ‘id’ に対応する引数が指定されていません | 引数の数が足りない | メソッド定義と呼び出しを一致させる(Insert/Updateの責務分離が有効) |
| CS1501: メソッド ‘InsertarUsuario’ のオーバーロードは引数 ‘x’ を取りません | 引数の個数が合わない | 意図したメソッドに合わせて、引数リストを整理する(DTO化・メソッド名分離) |
ここからは、3層構成(プレゼンテーション/ドメイン/データアクセス)を崩さずに、登録と更新を安定させるための実装に落とし込みます。
最初に確認するべきことは「メソッド定義」と「呼び出し」の一致
フォーム(プレゼンテーション層)からサービス(ドメイン層)を呼び、さらにリポジトリ(データアクセス層)でDBを叩く、という3層は定番です。ただし、フォーム側が次のようにTextBoxをそのまま渡すと、数値項目でほぼ確実に詰まります。
// 悪い例:TextBox.Text(string)をそのまま渡す
usuarioService.InsertarUsuario(
TxtIDUsuario.Text, // 本来Identity PK(int)
TxtNombre.Text,
TxtCategoria.Text, // 本来カテゴリID(int)
TxtEdad.Text // 本来年齢(int)
);
一方、サービス層が次のような定義だとコンパイルエラーになります。
// 例:サービス層(ドメイン層)
public void InsertarUsuario(int idUsuario, string nombre, int idCategoria, int edad)
{
// ...
}
このズレは「変換すればOK」に見えますが、Identity主キーや外部キーが絡むとそもそも引数に含めるべきかが変わります。次章で設計の整理から進めるのが近道です。
TextBox.Textは常にstringなので、数値は必ずパースして検証する
WinFormsのTextBoxはユーザーが自由に入力できるため、int.Parse のように例外を投げる変換をそのまま使うと、入力が少しでも崩れた瞬間にアプリが落ちやすくなります。保存処理では int.TryParse を使い、「変換できない=入力が不正」としてUIで明確にフィードバックするのが定石です。
private bool TryGetInt(TextBox textBox, string displayName, out int value)
{
if (!int.TryParse(textBox.Text.Trim(), out value))
{
MessageBox.Show($"{displayName} は数値で入力してください。", "入力エラー",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
textBox.Focus();
textBox.SelectAll();
return false;
}
return true;
}
保存イベントでは、数値項目は必ずこの関門を通します。
private void BtnGuardar_Click(object sender, EventArgs e)
{
if (!TryGetInt(TxtEdad, "年齢", out var edad)) return;
// カテゴリは後述のComboBoxを使うのが安全だが、
// もしTextBox運用なら同様にTryParseする
if (!TryGetInt(TxtCategoria, "カテゴリID", out var idCategoria)) return;
// ここまで来たら「型としては」正しい
}
さらに再発防止を狙うなら、そもそも「数値しか入れない項目」にTextBoxを使わないのが効果的です。年齢、数量、回数、金額などは NumericUpDown を採用すると、入力ミス・TryParse失敗が激減します。
| 項目の性質 | おすすめコントロール | 理由 |
|---|---|---|
| 年齢・数量などの整数 | NumericUpDown | 入力値の範囲をUIで制限でき、検証コストが下がる |
| Identity主キー | 内部保持(フィールド/Tag)+表示は任意 | 編集させない。更新時の識別子として保持するだけ |
| 外部キー(カテゴリなど) | ComboBox(SelectedValue) | 表示名とIDを分離でき、DBに渡すのはID(int)だけにできる |
| 書式がある文字列(メール等) | 検証付きTextBox/MaskedTextBox | 形式チェックとユーザー誘導がしやすい |
Identity主キーはInsertで渡さないのが基本
IDUsuario がIdentity(自動採番)主キーの場合、画面からIDを渡してInsertする設計にすると次の問題が起きやすくなります。
- 「新規登録なのにID入力が必要」というUI矛盾が生まれる
- DBが採番した値と画面入力が衝突する(通常、Identity列へ明示的に値を入れない運用が多い)
- InsertとUpdateの境界が曖昧になり、引数やロジックが肥大化する
そこで、Insert用メソッドからはIDUsuario引数を外すのが王道です。更新だけがIDを必要とします。
| 処理 | 画面で扱うIDUsuario | サービス層の引数設計例 | 備考 |
|---|---|---|---|
| 新規登録(Insert) | 渡さない(空/未設定) | InsertarUsuario(nombre, idCategoria, edad, ...) | IDはDBが採番し、必要なら戻り値で受け取って画面に反映 |
| 更新(Update) | 渡す(選択行のID) | EditarUsuario(idUsuario, nombre, idCategoria, edad, ...) | IDは編集不可。選択行から取得して保持する |
「Insert後に採番されたIDを画面に反映したい」場合は、SQL側で採番値を返し、C#で受け取ります。よく使う方法は次の2つです。
方法A: OUTPUT INSERTED でIDを返す
CREATE PROCEDURE dbo.InsertarUsuario
@Nombre NVARCHAR(100),
@IdCategoria INT,
@Edad INT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO dbo.Usuario (Nombre, IdCategoria, Edad)
OUTPUT INSERTED.IdUsuario
VALUES (@Nombre, @IdCategoria, @Edad);
END
public int InsertarUsuario(UsuarioCreateDto dto)
{
using var con = new SqlConnection(_connectionString);
using var cmd = new SqlCommand("dbo.InsertarUsuario", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@Nombre", SqlDbType.NVarChar, 100).Value = dto.Nombre;
cmd.Parameters.Add("@IdCategoria", SqlDbType.Int).Value = dto.IdCategoria;
cmd.Parameters.Add("@Edad", SqlDbType.Int).Value = dto.Edad;
con.Open();
var result = cmd.ExecuteScalar(); // OUTPUT INSERTED.IdUsuario を受け取る
return Convert.ToInt32(result);
}
方法B: SCOPE_IDENTITY を使う
SCOPE_IDENTITYは通常 numeric(38,0) として返るため、C#側で Convert.ToInt32 等の変換が必要です。OUTPUT INSERTEDの方が直感的で拡張にも強いので、迷ったら方法Aに寄せると安定します。
外部キーはTextBox入力ではなく選択値を渡す
TxtCategoria.Text に「カテゴリ名」や「カテゴリID」を手入力させる設計は、短期的には動いても運用でミスが増えます。外部キーは別テーブルの主キー参照なので、画面では表示名とIDを分離するのが鉄則です。
WinFormsならComboBoxを次のようにバインドし、保存時は SelectedValue(int)を渡します。
private void LoadCategorias()
{
// 例:List<Categoria> を想定(DataTableでもOK)
var categorias = categoriaService.ListarCategorias();
CmbCategoria.DataSource = categorias;
CmbCategoria.DisplayMember = "Nombre"; // 画面に表示する文字
CmbCategoria.ValueMember = "IdCategoria"; // 実際に保存する数値ID
CmbCategoria.SelectedIndex = -1; // 未選択
}
private bool TryGetCategoriaId(out int idCategoria)
{
idCategoria = 0;
if (CmbCategoria.SelectedValue == null)
{
MessageBox.Show("カテゴリを選択してください。", "入力エラー",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
CmbCategoria.Focus();
return false;
}
try
{
idCategoria = Convert.ToInt32(CmbCategoria.SelectedValue);
return true;
}
catch
{
MessageBox.Show("カテゴリIDの取得に失敗しました。カテゴリ一覧の設定を確認してください。", "入力エラー",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
}
DataGridViewの選択行からTextBoxに値を渡して編集する運用でも、カテゴリは「表示名」と「ID」の取り違えが起きやすいポイントです。ComboBox化は体感でミスが激減します。
InsertとUpdateでDTOを分けると、型ズレと引数ズレが一気に減る
フォームからサービスへ引数を羅列する形は、項目が増えた瞬間に破綻しがちです。3層構成なら、プレゼンテーション層からはDTO(データ転送用オブジェクト)を渡し、サービス層はDTOを受け取って処理し、リポジトリに渡す設計が読みやすくなります。
特に重要なのは、InsertとUpdateで「必要な項目が違う」ことを型で表現することです。
public sealed class UsuarioCreateDto
{
public required string Nombre { get; init; }
public required int IdCategoria { get; init; }
public required int Edad { get; init; }
}
public sealed class UsuarioUpdateDto
{
public required int IdUsuario { get; init; } // Updateだけ必須
public required string Nombre { get; init; }
public required int IdCategoria { get; init; }
public required int Edad { get; init; }
}
フォーム側は「新規か更新か」を判断し、適切なDTOを作ります。ここでのコツは、IDをTextBoxの文字列に依存しないことです(表示していても、更新に使うIDは内部のintで持つ)。
private int? _selectedUserId; // 選択中ユーザーID(更新用)
private void BtnGuardar_Click(object sender, EventArgs e)
{
if (!TryGetCategoriaId(out var idCategoria)) return;
if (!TryGetInt(TxtEdad, "年齢", out var edad)) return;
var nombre = TxtNombre.Text.Trim();
if (string.IsNullOrWhiteSpace(nombre))
{
MessageBox.Show("名前を入力してください。", "入力エラー",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
TxtNombre.Focus();
return;
}
if (_selectedUserId == null)
{
// 新規登録
var newId = usuarioService.InsertarUsuario(new UsuarioCreateDto
{
Nombre = nombre,
IdCategoria = idCategoria,
Edad = edad
});
_selectedUserId = newId;
TxtIDUsuario.Text = newId.ToString();
MessageBox.Show("ユーザーを登録しました。", "完了",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
// 更新
usuarioService.EditarUsuario(new UsuarioUpdateDto
{
IdUsuario = _selectedUserId.Value,
Nombre = nombre,
IdCategoria = idCategoria,
Edad = edad
});
MessageBox.Show("ユーザー情報を更新しました。", "完了",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
ReloadGrid();
}
この形にすると、EditarUsuario(IDUsuario) のような「intが絡む箇所」でのコンパイルエラーはほぼ消えます。Update用DTOの IdUsuario が int で固定され、フォーム側で int を準備できない設計は早期に発見できるためです。
DataGridViewが表示専用でも、IDは「表示」より「保持」へ寄せる
DataGridViewは表示専用、選択した行の内容をTextBoxに流し込む方式はよくあります。このときID(PK)をTextBoxに表示してReadOnlyにするのは「どのレコードを触っているか」が分かるメリットがありますが、更新処理がIDのTextBox依存になると、空白混入やコピー貼り付けなどで再びトラブルになります。
おすすめは次のどちらかです。
- IDは表示してもよいが、更新に使うIDは
intのフィールド(例:_selectedUserId)にも保持する - IDは画面に出さず、内部保持だけにする(Labelや
Control.Tagでも可)
選択行からIDとカテゴリIDを取り出して保持し、画面に反映する例です。
private void DgvUsuarios_SelectionChanged(object sender, EventArgs e)
{
var row = DgvUsuarios.CurrentRow;
if (row == null) return;
// 主キー(IdUsuario)
var idObj = row.Cells["IdUsuario"].Value;
_selectedUserId = idObj == null ? null : Convert.ToInt32(idObj);
TxtIDUsuario.Text = _selectedUserId?.ToString() ?? string.Empty;
TxtIDUsuario.ReadOnly = true;
// 表示項目
TxtNombre.Text = Convert.ToString(row.Cells["Nombre"].Value) ?? string.Empty;
TxtEdad.Text = Convert.ToString(row.Cells["Edad"].Value) ?? string.Empty;
// 外部キー(IdCategoria)→ ComboBoxのSelectedValueへ
var catObj = row.Cells["IdCategoria"].Value;
if (catObj != null)
{
CmbCategoria.SelectedValue = Convert.ToInt32(catObj);
}
else
{
CmbCategoria.SelectedIndex = -1;
}
}
この設計なら、TextBoxの表示がどうであれ、更新に必要なIDは常に int として保持されます。
ストアドプロシージャ連携で「型ズレ」を増やさないコツ
今回のテーマはコンパイルエラーですが、修正後に次で詰まるケースとして「ストアド実行時の型ズレ」があります。層間で型を厳密に揃えるために、最低限次を意識すると安定します。
- int項目は
SqlDbType.Intを明示し、文字列で渡して暗黙変換に頼らない - NVARCHAR/VARCHARは長さも指定し、画面入力はTrimしてから渡す
- NULL許容の列は
DBNull.Valueを渡すルールを統一する(空文字をNULLにするか等)
Update用ストアドと、データアクセス層(リポジトリ)の最小例です。
CREATE PROCEDURE dbo.EditarUsuario
@IdUsuario INT,
@Nombre NVARCHAR(100),
@IdCategoria INT,
@Edad INT
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.Usuario
SET Nombre = @Nombre,
IdCategoria = @IdCategoria,
Edad = @Edad
WHERE IdUsuario = @IdUsuario;
END
public void EditarUsuario(UsuarioUpdateDto dto)
{
using var con = new SqlConnection(_connectionString);
using var cmd = new SqlCommand("dbo.EditarUsuario", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@IdUsuario", SqlDbType.Int).Value = dto.IdUsuario;
cmd.Parameters.Add("@Nombre", SqlDbType.NVarChar, 100).Value = dto.Nombre;
cmd.Parameters.Add("@IdCategoria", SqlDbType.Int).Value = dto.IdCategoria;
cmd.Parameters.Add("@Edad", SqlDbType.Int).Value = dto.Edad;
con.Open();
cmd.ExecuteNonQuery();
}
引数の数が合わない系のエラーを根絶するための設計パターン
「引数の数が違う」エラーは、次の状態で起きやすくなります。
- Insert/Update/Deleteで同名メソッドをオーバーロードし、似た引数が増殖している
- フォームからサービスへ値を羅列して渡している(項目追加のたびに呼び出し側が壊れる)
- UIのTextBoxが増えるたびにメソッドの引数が増える
| 対策 | 何が良くなるか | 実装のヒント |
|---|---|---|
| DTOでまとめる | 引数の個数が固定され、項目追加に強くなる | Create/UpdateでDTOを分けるとIdentityの扱いも自然に整理できる |
| メソッド名を分ける | オーバーロードの混乱が減り、意図が明確になる | CreateUser/UpdateUser のように処理単位で分ける |
| 入力とビジネス判断を分離する | フォームが肥大化しにくく、責務が明確になる | フォームは入力検証、サービスは業務ルール、リポジトリはDB型対応に集中 |
3層構成の価値は「責務が分かれること」です。フォームが型変換も業務判断も全部抱えると、結局メンテナンスが辛くなります。フォームは入力を集めて検証し、サービスは処理のルールを持ち、リポジトリはDBとの型対応を厳密にする、という分担が最も事故が少ないです。
最終チェックリスト
- Insert用メソッドにIdentity主キー(IDUsuario)を渡していない
- Update用メソッドは
intのIDを受け取り、フォーム側でもintとして保持している - 外部キー(カテゴリ等)はComboBoxの
SelectedValueで取得しており、表示名とIDを混ぜていない - TextBoxから数値を取る場合は
TryParseで検証し、失敗時はユーザーに分かるエラーメッセージを出している - SqlParameterで型とサイズを明示し、暗黙変換に依存していない
Microsoft Learn Q&AでSmall Basicタグになって内容と合わない問題
もうひとつの論点が、Microsoft Learn Q&A上でタグがsmall-basicになっていて、質問内容(C#/.NET+SQL Server)と合っていない点です。タグは「その質問がどの技術領域に属するか」を示し、回答者が検索・フィルタで拾うための重要な手掛かりです。タグがズレると、次のような損が起きます。
- 本来の専門家の目に届きにくく、回答が付きにくい
- 内容と無関係な誘導回答が増え、解決まで遠回りになる
- オフトピックと判断されやすくなる
Small Basicは別言語の領域なので、.NET/C#の質問であれば、採択回答のとおり適切なタグ(例:dotnet-csharp)に変更して投稿し直す/タグを付け直すのが正攻法です。タグ修正が可能なら編集で直し、難しい場合は新規に正しいタグで投稿し直す方が早く解決するケースもあります。
タグ修正と同時に「通る質問」に整えるポイント
タグを直したうえで、質問本文の情報を揃えると解決速度が上がります。特にコンパイルエラーは、エラーメッセージと該当行のコードが揃っていれば、原因がその場で確定しやすい分野です。
| 書くべき情報 | 例 | 理由 |
|---|---|---|
| エラーコードと全文 | CS1503: ‘string’ から ‘int’ へ変換できません | 原因を即特定できる。画像よりテキストが望ましい |
| 該当行のコード | InsertarUsuario(TxtCategoria.Text, ...) | 型・順番・個数のズレが見える |
| メソッド定義 | InsertarUsuario(int idCategoria, ...) | 呼び出し側との不一致を判断できる |
| 前提(環境・設計) | SQL Server、ストアド利用予定、3層構成、DataGridViewは表示専用 | Identity・外部キーの扱いが解答に直結する |
まとめ
TextBoxの値を渡してユーザー登録/更新すると「int(Identity PK/外部キー)絡みでコンパイルエラー」になる問題は、単に string を int に変換するだけでなく、Identity主キーはInsertで渡さない、外部キーは選択値(int)で渡す、InsertとUpdateの形を分けるという設計に整えることで根本的に安定します。DTOで引数を整理し、IDは内部で int として保持するだけでも、コンパイルエラーも実行時の型ズレも大きく減ります。
また、外部に質問する際はタグが解決速度を左右します。Small BasicではなくC#/.NETのタグに直し、エラー全文と該当コードを添える。これだけで、回答が届く確率が一段上がります。

コメント