C# WinFormsでTextBoxの値を渡すとユーザー登録/更新がコンパイルエラーになる原因と解決策(Identity主キー・外部キー・SQL Server)

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のタグに直し、エラー全文と該当コードを添える。これだけで、回答が届く確率が一段上がります。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次