WebSurfer's Home

Filter by APML

参照型と ASP.NET Core の検証

by WebSurfer 23. February 2024 17:49

.NET 6.0 以降の ASP.NET Core Web アプリで、コンテキストクラスとエンティティクラスを使って SQL Server などのデータベースのテーブルの CRUD 操作を行う際、エンティティクラス内の参照型のプロパティを null 許容にしておかないと検証に通らず、Create や Edit 操作に失敗するという話を書きます。特に下の画像のようなナビゲーションプロパティが要注意です。

検証結果が NG

また同じ問題にハマって時間を無駄にしないよう、どういうことかを書いておくことにしました。

先の記事「null 許容参照型と EF Core Code First」で書きましたように、Visual Studio 2022 のテンプレートを使って、ターゲットフレームワークを .NET 6.0 以降の設定にしてプロジェクトを作ると、デフォルトで「Null 許容」オプションが有効になり、Code First の場合は生成されるデータベースのフィールドの NULL 可/不可が、リバースエンジニアリングでデータベースからエンティティクラスを生成する場合はプロパティの型の null 許容/非許容が影響を受けます。

エンティティクラスをビューモデルに使ってブラウザからのデータを MVC アプリのアクションメソッドで受け取る場合、null 許容参照型に注意が必要です。特にナビゲーションプロパティの型による問題は気が付きにくいと思いました。

具体的にどういうことかを下の画像の SQL Server の Blogging データベースの dbo.Blogs, dbo.Posts テーブルを例に使って説明します。

SQL Server データベース

上の dbo.Blogs, dbo.Posts テーブルからリバースエンジニアリングでコンテキストクラスとエンティティクラスを生成します。ターゲットフレームワークは .NET 8.0 で「Null 許容」オプションはプロジェクト全体で有効に設定された状態です。パッケージマネージャコンソールから発行したコマンドを参考に下に載せておきます。

Scaffold-DbContext -Connection "接続文字列" -Provider Microsoft.EntityFrameworkCore.SqlServer -ContextDir Data -OutputDir Models -Tables Posts, Blogs -DataAnnotations

その結果、以下のエンティティクラスが生成されます。(同時にコンテキストクラスも生成されますが省略)

Blog クラス

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPages1.Models;

public partial class Blog
{
    [Key]
    public int BlogId { get; set; }

    public string Name { get; set; } = null!;

    [InverseProperty("Blog")]
    public virtual ICollection<Post> Posts { get; set; } = 
        new List<Post>();
}

Post クラス

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace RazorPages1.Models;

[Index("BlogId", Name = "IX_Posts_BlogId")]
public partial class Post
{
    [Key]
    public int PostId { get; set; }

    public string Title { get; set; } = null!;

    public string? Content { get; set; }

    public int BlogId { get; set; }
    
    [ForeignKey("BlogId")]
    [InverseProperty("Posts")]
    public virtual Blog Blog { get; set; } = null!;   
}

SQL Server の dbo.Blog テーブルの外部キーフィールド BlogId が NULL 不可になっているため、Blog クラスの Posts ナビゲーションプロパティと、Post クラスの Blog ナビゲーションプロパティの型は Null 許容になりません。

上の Post クラスをベースに、Visual Studio 2022 のスキャフォールディング機能を使って dbo.Posts テーブルの CRUD を行う ASP.NET Core MVC アプリのコントローラーとビューを生成し、アプリを実行してレコードの Create 操作をしようとしたところ ModelState.IsValid が false(検証結果 NG)となって失敗したというのがこの記事の一番上の画像です。Create 操作だけでなく、Edit 操作でも同様に検証 NG で失敗します。

検証 NG となった原因は、Post クラスで null 非許容のナビゲーションプロパティ Bolg に、モデルバインディングの際に null が代入されたからです。

なぜ null が代入されるかですが、下のスキャフォールディングで自動生成された Create 画面を見てください。ブラウザから送信されてくるのは Title, Content, BlogId だけということに注目してください。

Create 画面

一方 Create アクションメソッドの引数は、この記事の一番上の画像の通り Post クラスになっています。ブラウザからデータが送信されてこない PostId, Blog プロパティにはモデルバインディングの際その型のデフォルト値(参照型は null)が代入されます。一番上のデバッグ画面のローカル変数の値を見てください。

Blog ナビゲーションプロパティの型は null 非許容なので、ModelState.IsValid が false になり、Create, Edit に失敗します。

解決策は、生成されたコードに手を加えて Null 許容にすることです。

// 以下のように null 許容にすれば検証は通る
public virtual Blog? Blog { get; set; }

ちなみに、PostId もブラウザからは送信されてきませんのでモデルバインディングでデフォルト値が代入されますが、int 型のデフォルト値は null ではなくて 0 (ゼロ) なので問題は出ません。一番上の画像のローカル変数の青枠を見てください。

また、Title も null 非許容ですが、こちらは検証結果のエラーメッセージがブラウザの画面上の当該テキストボックスの下に表示されるので、すぐ気が付きます。Blog の方はエラーメッセージは出ませんので気づき難いです。

Tags: , , ,

CORE

整数型プロパティの検証、エラーメッセージ (CORE)

by WebSurfer 26. January 2023 11:49

.NET 7.0 の ASP.NET Core MVC で、モデルのプロパティが整数型(int, short, long 等)の場合の検証とエラーメッセージに関する注意点を書きます。

検証結果

.NET Framework の MVC5 の場合については先の記事「int 型プロパティの検証、エラーメッセージ (MVC5)」に書きましたのでそちらを見てください。

検証に使ったのは Visual Studio 2022 v17.4.4 のテンプレートで作成した .NET 7.0 の ASP.NET Core MVC アプリです。View には Html ヘルパーではなく入力 Tag ヘルパーを使いました。

Model のプロパティが整数型の場合 (下に載せた Model のコード例では ContractID, Price2 プロパティ)、Required 属性を付与しなくても、View の入力 Tag ヘルパーから生成される html の input 要素には data-val-required="The xxx field is required." (xxx はプロパティ名) という属性が付与されます。

(プロパティが int? 型のような null 許容型の場合は data-val-required 属性は付与されません。.NET Framework MVC5 の場合に付与された data-val-number という属性は .NET 7.0 ASP.NET Core MVC では付与されません)

View で検証用の JavaScript ファイルが取り込まれるように設定されていると (下に載せた View のコード例では @section Scripts の中のコード)、未入力の場合はクライアント側での検証により data-val-required 属性に設定された "The xxx field is required." というメッセージが表示されます。

これを任意の文字列に書き換えるには、Model の���該プロパティに Required 属性を付与し ErrorMessage に書き換えたい文字列を設定します。そうすると、html の input 要素の data-val-required 属性に設定される文字列が ErrorMessage に置き換わり、検証 NG の場合はそれが表示されます。

未入力の検証については上記の対応だけで特に不都合はないと思います。

しかし、未入力の検証に加えて、数字か否かの検証を行う場合はいろいろ問題があります。それを以下に説明します。

入力 Tag ヘルパーが以下のようになっている場合、html の input 要素の type 属性が "number" となり、ブラウザによる入力の制約と、ASP.NET による検証がかかります。

<input asp-for="Price2" class="form-control"/>

入力の制約はブラウザ依存です。自分が試した限りですが、Edge 109.0.1518.61, Chrome 109.0.5414.120, Opera 94.0.4606.76 では数字と + - . 以外の文字は受け付けなくなります。Firefox 109.0 では何でも入力できてしまいます。

クライアント側での JavaScript による検証はフレームワークに組み込みの jquery.validate.min.js により行われます。数字以外の文字を入力して送信しようとすると、"Please enter a valid number." というエラーメッセージが表示され送信はキャンセルされます。("Please enter a valid number." というエラーメッセージは jquery.validate.min.js にハードコーディングされています。それを書き換えれば日本語化はできます)

問題は、(1) 文字 + - はブラウザは受け付けるが jquery.validate.min.js による検証で NG になるという不整合、(2) エラーメッセージが英語になること、(3) さらにそれを解決するためプロパティに RegularExpression 属性を追加しても無視されることです。

上記 (1) ~ (3) を解決するには、html の input 要素が type="number" ではなく type="text" になるようにします。具体的には、View の当該入力 Tag ヘルパーに以下のように type="text" を追加します。

<input asp-for="Price2" class="form-control" type="text"/>

それにより RegularExpression 属性に設定した正規表現が期待通り動くようになり、検証結果 NG の場合は ErrorMessage に設定したエラーメッセージが表示されるようになります。

以上をまとめると:

  1. 未入力の検証には、Model の当該プロパティに Required 属性を付与し ErrorMessage にエラーメッセージを設定。
  2. 数字か否かの検証には、当該入力 Tag ヘルパーに type="text" を追加して html の input 要素の type 属性が "text" となるようにし、さらに
  3. RegularExpression 属性をプロパティに付与して正規表現を使っての検証を行う。ErrorMessage にエラーメッセージを設定する。

 

以上はクライアント側での検証の話です。

サーバー側での検証の問題は、未入力または整数型にパースできない文字列を送信した場合、Model のプロパティに付与した Required 属性、RegularExpression 属性が働かないことです。上の画像の「価格2 (int)」のエラーメッセージを見てください。

上の画像の例では、クライアント側での検証を無効にして "2000x" という文字列を送信していますが「The value '2000x' is not valid for 価格2 (int).」というエラーメッセージが出ています。RegularExpression 属性に設定したエラーメッセージとは異なっています。

未入力の場合は「The value '' is invalid.」というエラーメッセージが表示されます。やはり Required 属性に設定したエラーメッセージとは異なります。

これは、"2000x" という文字列や空白は int 型にパースできないので、検証属性による検証が行われる前にエラーとなって、そのエラーメッセージが出ているようです。(想像です)

検証属性の ErrorMessage に設定したメッセージが表示されて欲しいのですが、int 型にパースできない文字列が送信された場合は何ともならないようです。ただし、このエラーメッセージを書き換える方法はあります。

ModelStateDictionary に含まれる ModelStateEntry は同じ Key でマージしすると上書きされます。具体例は下の Controller コードの通りです。上の画像の ID (int) のテキストボックスの下のエラーメッセージを書き換えています。

Model

using System.ComponentModel.DataAnnotations;

namespace MvcNet7App.Models
{
    public class Contract
    {
        [Display(Name = "ID (int)")]
        [Required(ErrorMessage = "{0} は必須")]
        [RegularExpression(@"^\d+$", ErrorMessage = "数字のみ")]
        public int ContractID { get; set; }

        [Display(Name = "契約日 (DateTime)")]
        [Required(ErrorMessage = "{0} は必須")]
        [RegularExpression(@"^\d{4}/\d{2}/\d{2}( \d{1,2}:\d{2}:\d{2})?$",
            ErrorMessage = "yyyy/MM/dd 形式")]
        public DateTime ContractDate { get; set; }

        [Display(Name = "価格 (decimal)")]
        [Required(ErrorMessage = "{0} は必須")]
        [RegularExpression(@"^\d{1,5}$", 
            ErrorMessage = "数字 1 ~ 5 文字")]
        public decimal Price { get; set; }

        [Display(Name = "価格2 (int)")]
        [Required(ErrorMessage = "{0} は必須")]
        [RegularExpression(@"^\d+$", ErrorMessage = "数字のみ")]
        [Range(100, 10000, 
            ErrorMessage = "{0}は{1}~{2}の間で入力してください。")]
        public int Price2 { get; set; }
    }
}

View

@model MvcNet7App.Models.Contract

@{
    ViewData["Title"] = "ContractEdit";
}

<div class="row">
    <div class="col-md-4">
        <form asp-action="ContractEdit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="ContractID" class="control-label"></label>
                <input asp-for="ContractID" class="form-control"/>
                <span asp-validation-for="ContractID" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ContractDate" class="control-label"></label>
                <input asp-for="ContractDate" class="form-control" type="text"/>
                <span asp-validation-for="ContractDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" type="text"/>
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price2" class="control-label"></label>
                <input asp-for="Price2" class="form-control" type="text"/>
                <span asp-validation-for="Price2" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

@section Scripts {
    @{
        // コメントアウトするとクライアント側での検証がかからない
        await Html.RenderPartialAsync("_ValidationScriptsPartial");
    }
}

Controller

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using MvcNet7App.Models;
using System.Diagnostics;

namespace MvcNet7App.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }

        public IActionResult ContractEdit()
        {
            var model = new Contract 
            { 
                ContractID = 1, 
                ContractDate = DateTime.Now, 
                Price = 1000m, 
                Price2 = 2000
            };

            return View(model);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult ContractEdit(Contract model)
        {
            if (ModelState.IsValid)
            {
                return RedirectToAction("Index");
            }

            // div asp-validation-summary に表示するために追加
            var newDictionary = new ModelStateDictionary();
            newDictionary.AddModelError("", 
                "ValidationSummary に表示するために追加。");
            ModelState.Merge(newDictionary);

            // エラーメッセージを書き換えることはできる。
            // 同じ Key でマージした方に上書きされる
            ModelStateEntry? state = ModelState["ContractID"];

            if (state != null && state.Errors.Count > 0)
            {
                string msg = state.Errors[0].ErrorMessage;
                if (msg.StartsWith("The value"))
                {
                    // マージすると RawValue が null になるので書き戻すた
                    // めに取得しておく
                    object? value = state.RawValue;

                    var newDictionary2 = new ModelStateDictionary();
                    newDictionary2.AddModelError("ContractID",
                        "書き換え。元は The value '1.0' is not valid for ID (int).");
                    ModelState.Merge(newDictionary2);

                    // RawValue を書き戻す。そうしないと再描画されたとき
                    // 元のユーザー入力が表示されず 0 になってしまう
                    state.RawValue = value;
                }
            }

            return View(model);
        }
    }
}

上に書いた「The value '2000x' is not valid for 価格2 (int).」とか「The value '' is invalid.」は Model Binding Error Messages というそうで、ローカライズする方法があるようです。上のように書き換えるよりローカライズする方が現実的かもしれません。

Stackoverflow の記事 ASP.NET Core Model Binding Error Messages Localization に方法が書いてあるのを見つけました。別途検証してみます。 ⇒ 2022/1/27 追記: 別の記事「Model Binding Error Messages の差替 (CORE)」に書きましたのでそちらを見てください。

Tags: , , , , ,

Validation

DataGridView 新規追加行の未入力検証

by WebSurfer 9. October 2022 13:25

Windows Forms アプリの GataGridView の新規追加行に未入力のままフォーカスを移動したり[データの保存]ボタンをクリックした時にスローされる NoNullAllowedException に対処する方法を書きます。

NoNullAllowedException

NoNullAllowedException というのは、DataColumn.AllowDBNull プロパティが false に設定されている列に null 値を挿入しようとした場合にスローされる例外です。Visual Studio のデータソース構成ウィザードを使って型付 DataSet / DataTable を作ると、元の DB の列が NULL 不可になっている場合に false に設定されます。

DataGridView 上で当該列を空白のままにしてフォーカスを移動したり[データの保存]ボタンをクリックしたりすると DataTable の当該 Column に null 値を挿入しようとするので NoNullAllowedException がスローされます。

その際、エラーメッセージを表示するダイアログが、上の画像のものとは違って、下の画像のような「DataGridView の既定のエラーダイアログ」であれば、DataGridView.DataError イベントを処理することで対応できます。

DataGridView の既定のエラーダイアログ

具体例は Microsoft のドキュメント「DataGridView.DataError イベント」や「ユーザーがDataGridViewのセルに正しくない値を入力した時に発生するエラーを捕捉する」を見てください。下に載せたサンプルコードにも実装してあります。

しかしながら、DataError イベントをハンドルしても NoNullAllowedException を補足できないケースがあるのが問題です。

その補足できないケースというのは、自分が調べた限りですが、(1) BindingNavigator 上の[新規追加]ボタンを続けて 2 回クリック、(2) 新規追加行に未入力のまま BindingNavigator 上の[最初に移動][前に戻る][データの保存]ボタンをクリックした場合です。(他にもあるかも)

その場合は、この記事の一番上の画像のような、例のおなじみのエラーメッセージのダイアログが表示されます。

これを何とかしようとすると try - catch 句を追加して NoNullAllowedException を補足するということになると思います。

(他にもっとスマートな手段があるかもしれませんが分かりませんでした。DataGridView.RowValidating イベントをハンドルして対応できないかと考えたのですが、上に書いた「補足できないケース」ではイベントが発生しません)

どこに try - catch 句を追加するかですが、データソース構成ウィザードを使って型付 DataSet / DataTable を作り、それをデザイン画面で「データソース」ウィンドウから Form にドラッグ&ドロップして作ったアプリのコードにはその場所がありません。(Program.cs の Application.Run(new Form1()); で try - catch するのはやりすぎと思うので対象外)

そこで考えたのが、BindingNavigator の AddNewItem, DeleteItem, MoveFirstItem, MoveLastItem, MoveNextItem, MoveProviousItem プロパティの設定を以下のように (なし) に変更し(デフォルトでは当該 ToolStripButton が設定されている)、

BindingNavigator の設定変更

AddNewItem, DeleteItem, MoveFirstItem, MoveLastItem, MoveNextItem, MoveProviousItem に設定されていた各 ToolStripButton の Click イベントのハンドラを追加し、

Click イベントのハンドラ設定

その各ハンドラの中で BindingSource の AddNew, RemoveCurrent, MoveFirst, MoveLast, MoveNext, MoveProvious を実行するようにして try - catch 句を使うということです。

BindingNavigation の操作やマウスやキーボードを使っての BindingSource のポジションの変更に伴って BindingNavigator 上の ToolStripButton の Enabled プロパティの true/false の設定を変更する必要がありますが、それは BindingSource.PositionChanged イベントのハンドラで行います。

ただし、ここまでの設定だけでは[データの保存]ボタンをクリックした場合に対応できません。そこは、自動生成されたクリックイベントのハンドラの中の BindingSource.EndEdit(); というコードに try - catch を追加して NoNullAllowedException を補足して対処します。

以下にサンプルコードを書きます。例として、下の画像の SQL Server データベースを使います。

SQL Server データベース

まず、上の Product テーブルのレコード一覧表示・編集を行う Windows Forms + DataGridView アプリを作ります。それは Microsoft のドキュメント「新しいデータ ソースの追加」のように「データソース構成ウィザード」を使って型付 DataSet / DataTable を作り、それをデザイン画面で「データソース」ウィンドウから Form にドラッグ&ドロップすれば、自力ではコードを一行も書くことなく簡単に作成できます。

その後、上に述べたように、自動生成されたコードに手を加えてたのが下のサンプルコードです。

using System;
using System.Data;
using System.Data.SqlClient;
using System.Windows.Forms;

namespace WindowsFormsPkUnique
{
    public partial class Form3 : Form
    {
        public Form3()
        {
            InitializeComponent();

            // DataGridView.DataError イベントのハンドラ設定
            this.productDataGridView.DataError += 
                                  ProductDataGridView_DataError;

            // BindingSource.PositionChanged イベントのハンドラ設定
            this.productBindingSource.PositionChanged += 
                                  ProductBindingSource_PositionChanged;
        }

        private void Form3_Load(object sender, EventArgs e)
        {
            this.productTableAdapter.Fill(this.productDataSet.Product);
        }

        // DataGridView.DataError イベントを処理。これで補足できない
        // ケースがあるのが問題
        private void ProductDataGridView_DataError(object sender, 
            DataGridViewDataErrorEventArgs e)
        {
            if (e.Exception != null)
            {
                MessageBox.Show(this,
                    $"Index {e.RowIndex} の行でエラーが発生しました。\n\n" +
                    $"説明: {e.Exception.Message}",

                    "エラーが発生しました",
                    MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
        }

        // BindingSource の Position の変更に伴って、BindingNavigator の
        // 各ボタンの Enabled プロパティの true/false 設定を変更する
        private void ProductBindingSource_PositionChanged(object sender, 
            EventArgs e)
        {
            int count = productBindingSource.Count;
            int position = productBindingSource.Position;
            if (count <= 1)
            {
                bindingNavigatorMoveFirstItem.Enabled = false;
                bindingNavigatorMovePreviousItem.Enabled = false;
                bindingNavigatorMoveNextItem.Enabled = false;
                bindingNavigatorMoveLastItem.Enabled = false;
                if (count == 0)
                    bindingNavigatorDeleteItem.Enabled = false;
                else
                    bindingNavigatorDeleteItem.Enabled = true;
            }
            else
            {
                bindingNavigatorDeleteItem.Enabled = true;

                if (position == 0)
                {
                    bindingNavigatorMoveFirstItem.Enabled = false;
                    bindingNavigatorMovePreviousItem.Enabled = false;
                    bindingNavigatorMoveNextItem.Enabled = true;
                    bindingNavigatorMoveLastItem.Enabled = true;
                }
                else if (position == count - 1)
                {
                    bindingNavigatorMoveFirstItem.Enabled = true;
                    bindingNavigatorMovePreviousItem.Enabled = true;
                    bindingNavigatorMoveNextItem.Enabled = false;
                    bindingNavigatorMoveLastItem.Enabled = false;
                }
                else
                {
                    bindingNavigatorMoveFirstItem.Enabled = true;
                    bindingNavigatorMovePreviousItem.Enabled = true;
                    bindingNavigatorMoveNextItem.Enabled = true;
                    bindingNavigatorMoveLastItem.Enabled = true;
                }
            }
        }

        // BindingNavigator の[データの保存]ボタンクリックのハンドラ
        private void productBindingNavigatorSaveItem_Click(object sender, 
            EventArgs e)
        {
            this.Validate();            

            try
            {
                // EndEdit を try に含めないと NoNullAllowedException が
                // catch できない
                this.productBindingSource.EndEdit();

                this.tableAdapterManager.UpdateAll(this.productDataSet);
            }
            catch (NoNullAllowedException ex)
            {
                MessageBox.Show(this,
                    ex.Message + "\n\nデータを修正してください。",
                    "「データの保存」ボタン操作エラー",
                    MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
            catch (SqlException sqlEx)  // オマケの重複チェック
            {
                if (sqlEx.Number == 2601)
                {
                    MessageBox.Show(this,
                        $"Unique 制約違反です。\n\n説明: {sqlEx.Message}",
                        "エラーが発生しました",
                        MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
                else
                {
                    throw;
                }               
            }            
        }

        // BindingNavigator の[最初に移動]ボタンクリックのハンドラ
        private void bindingNavigatorMoveFirstItem_Click(object sender, 
            EventArgs e)
        {
            try
            {
                productBindingSource.MoveFirst();
            }
            catch (NoNullAllowedException ex)
            {
                MessageBox.Show(this,
                    ex.Message + "\n\nデータを修正してください。",
                    "「最初に移動」ボタン操作エラー",
                    MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
        }

        // BindingNavigator の「前に戻る」ボタンクリックのハンドラ
        private void bindingNavigatorMovePreviousItem_Click(object sender, 
            EventArgs e)
        {
            try
            {
                productBindingSource.MovePrevious();
            }
            catch (NoNullAllowedException ex)
            {
                MessageBox.Show(this,
                    ex.Message + "\n\nデータを修正してください。",
                    "「前に戻る」ボタン操作エラー",
                    MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
        }

        // BindingNavigator の「次に移動」ボタンクリックのハンドラ
        private void bindingNavigatorMoveNextItem_Click(object sender, 
            EventArgs e)
        {
            try
            {
                productBindingSource.MoveNext();
            }
            catch (NoNullAllowedException ex)
            {
                MessageBox.Show(this,
                    ex.Message + "\n\nデータを修正してください。",
                    "「次に移動」ボタン操作エラー",
                    MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
        }

        // BindingNavigator の「最後に移動」ボタンクリックのハンドラ
        private void bindingNavigatorMoveLastItem_Click(object sender, 
            EventArgs e)
        {
            try
            {
                productBindingSource.MoveLast();
            }
            catch (NoNullAllowedException ex)
            {
                MessageBox.Show(this,
                    ex.Message + "\n\nデータを修正してください。",
                    "「最後に移動」ボタン操作エラー",
                    MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
        }

        // BindingNavigator の[新規追加]ボタンクリックのハンドラ
        private void bindingNavigatorAddNewItem_Click(object sender, 
            EventArgs e)
        {
            try
            {
                productBindingSource.AddNew();
            }
            catch (NoNullAllowedException ex)
            {
                MessageBox.Show(this,
                    ex.Message + "\n\nデータを修正してください。",
                    "「新規追加」ボタン操作エラー",
                    MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
        }

        // BindingNavigator の[削除]ボタンクリックのハンドラ
        private void bindingNavigatorDeleteItem_Click(object sender, 
            EventArgs e)
        {
            productBindingSource.RemoveCurrent();
        }
    }
}

上のコードには一つオマケで重複入力の検証を追加しています。上の SQL Server のデータベースの画像にあるように ProductName には Unique 制約がかかっていますが、それも検証してエラーメッセージを MessageBox を使って出すようにしています。

SQL Server ですから複数のユーザーが同時にアクセスするのが前提です。なので、重複しているか否かは実際に INSERT する時でないと分かりません。それは、INSERT 操作を行ったとき SqlException がスローされ、エラーの種類を表す番号が 2601 であることで判断できます。

その具体的な例は上のコードの productBindingNavigatorSaveItem_Click メソッドを見てください。


以上ですが、厳しく検証したわけではありませんので、上のコードはあくまで参考程度にお願いします。ひょっとしたら検証漏れがあるかもしれません。

Tags: , , ,

Validation

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  December 2024  >>
MoTuWeThFrSaSu
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

View posts in large calendar