WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

.NET の多次元配列の JSON シリアル化

by WebSurfer 14. December 2022 18:06

Newtonsoft.Json のシリアライザなら .NET の多次元配列を JSON 文字列にシリアライズできるという話を書きます。何を今さらと言われそうですが。(汗)

Newtonsoft.Json

System.Text.Json 名前空間のシリアライザは多次元配列の JSON シリアル化をサポートしていないようで、試してみましたが System.NotSupportedException がスローされ "The type 'System.String[,,]' is not supported." というエラーメッセージが出ます。(ジャグ配列ならサポートしていることは確認しました)

なので、調べもしないで Newtonsoft.Json も同じだと思っていたのですが、Json.NET 4.5 Release 8 – Multidimensional Array Support, Unicode Improvements によると 10 年も前から多次元配列のシリアライズをサポートしているとのことです。

Microsoft のドキュメント「Newtonsoft.Json と System.Text.Json を比較して、System.Text.Json に移行する」にもいろいろ書いてありますが、多次元配列のサポートについては言及してないです。「さまざまな型のサポート ⚠」に含むのでしょうか?

実際にコードを書いて、Newtonsoft.Json が多次元配列とジャグ配列の両方のシリアライズ/デシリアライズをサポートしていることは確認しました。以下に検証に使ったコードを載せておきます。.NET Framework 4.8.1 のコンソールアプリです。

using Newtonsoft.Json;
using System;

namespace ConsoleAppArrayJson
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // 多次元配列
            var mdArray = new string[30, 12, 31];
            for (int i = 0; i < 30; i++)
            {
                for (int j = 0; j < 12; j++)
                {
                    for (int k = 0; k < 31; k++)
                    {
                        mdArray[i, j, k] = $"e-{i}-{j}-{k}";
                    }
                }
            }

            // ジャグ配列
            var jArray = new string[30][][];
            for (int i = 0; i < 30; i++)
            {
                jArray[i] = new string[12][];
                for (int j = 0; j < 12; j++)
                {
                    jArray[i][j] = new string[31];
                    for (int k = 0; k < 31; k++)
                    {
                        jArray[i][j][k] = $"e-{i}-{j}-{k}";
                    }
                }
            }

            // 多次元配列を JSON 文字列にシリアライズ
            var json1 = JsonConvert.SerializeObject(mdArray);

            // ジャグ配列を JSON 文字列にシリアライズ
            var json2 = JsonConvert.SerializeObject(jArray);

            // json1 == json2 は true になる
            Console.WriteLine($"json1 == json2: {json1 == json2}");

            // JSON 文字列を多次元配列にデシリアライズ
            var array1 = JsonConvert.DeserializeObject<string[,,]>(json1);
            Console.WriteLine($"array1[0,1,2]: {array1[0, 1, 2]}");

            // JSON 文字列をジャグ配列にデシリアライズ
            var array2 = JsonConvert.DeserializeObject<string[][][]>(json2);
            Console.WriteLine($"array2[0][1][2]: {array2[0][1][2]}");
        }
    }
}

上のコードの実行結果は以下のようになります。

実行結果

Tags: , , ,

.NET Framework

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: , , ,

.NET Framework

PostgreSQL とデータソース構成ウィザード

by WebSurfer 5. October 2022 16:14

PostgreSQL の既存のデータベースから Visual Studio 2022 のデータソース構成ウィザードを利用して型付 DataSet / DataTable + TableAdapter を作ることができます。それを使って、DB のレコードの一覧表示・編集ができる Windows Forms + GataGridView アプリを容易に作成できます。

Windows Forms + GataGridView アプリ

SQL Server の場合は当たり前の話なので何をいまさらと言われるかもしれませんが、先日初めて PostgreSQL をインストールしたので試してみた次第です。

(ASP.NET MVC5 アプリで Entity Framework 6 を使って PostgreSQL の既存の DB の CRUD を行う場合については、先の記事「PostgreSQL で EF6 DB First」に書きましたので、興味があれば見てください)

この記事を書いた時の環境は以下の通りです。

  • PostgreSQL 14.4
  • Visual Studio Community 2022 17.3.5
  • Npgsql PostgreSQL Integration 4.1.12
  • Npgsql 6.0.7
  • .NET Framework 4.8
  • Windows フォームアプリケーション (.NET Framework)

Visual Studio で PostgreSQL に対してデータソース構成ウィザードが使えるようにするには Npgsql PostgreSQL Integration という拡張機能の追加が必要です。下の画像のバージョン 4.1.12 は Visual Studio 2022 用にリリースされたものだそうです。

Npgsql PostgreSQL Integration

拡張機能の追加後、Microsoft のドキュメント「新しいデータ ソースの追加」のようにデータソース構成ウィザードを起動すれば、「データ接続の選択」で PostgreSQL Database を選択できるようになります。

データ接続の選択

以下のように PostgreSQL への接続情報を設定して接続できれば、

接続情報を設定

そのあとは SQL Server とほぼ同様な手順で DataSet(xsd ファイル)を作成できます。

DataSet(xsd ファイル)

xsd ファイルが作成できると、デザイン画面でデータソースウィンドウに DataSet / DataTable が表示されるので (下の画像で MovieDataSet / Movie)、DataTable (Movie) の DataGridView を選択してから Form にドラッグ&ドロップします。

データソースウィンドウの DataSet / DataTable

上の画像はドラッグ&ドロップして必要なコードがすべて生成された後、DataGridView.Dock プロパティを Fill に設定したものです。

次に NuGet から Npgsql をインストールします。

NuGet から Npgsql をインストール

その後ソリューションをビルドして実行すればこの記事の一番上の画像のようなアプリが動きます。DataGridView を編集した結果も期待通り DB に反映されます。

ただ、微妙なところがあって、なぜか Npgsql のインストールのタイミングが問題で、自分の環境で試した時は以下の順番で行う必要がありましたので注意してください。

  1. データソース構成ウィザードを起動し型付 DataSet / DataTable + TableAdapter を生成
  2. 「データソース」ウィンドウから作成した DataTable を Form にドラッグ&ドロップ
  3. NuGet から Npgsql をインストール
  4. ソリューションをビルド

想像ですが、データソース構成ウィザードが使う Npgsql と NuGet からインストールする Npgsql やその他の関係 dll とのバージョンの不整合が問題ではないかと思われます。

Tags: , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。その後ブログ2を追加し、ここはプログラミング関係、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  February 2023  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
272812345
6789101112

View posts in large calendar