WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

CSV ファイルを DataGridView に表示

by WebSurfer 2020年9月11日 15:05

CSV ファイルからデータを取得して Windows Forms アプリの DataGridView に表示し、それをユーザーが編集して結果を CSV ファイルに書き出すサンプルを書きます。先の記事「XML ファイルを DataGridView に表示」の CSV 版です。

CSV ファイルを DataGridView に表示

CSV ファイルのサンプルは、Microsoft のサンプルデータベース Northwind の Products テーブルから SQL Server Management Studio を使ってエクスポートしたものを使います。CSV ファイルは 1 行目がヘッダであることが条件で、Products テーブルのフィールド名がヘッダになります。

CVS ファイルを DataGridView に表示する基本的な構成は以下の通りです。

CSV ファイル => CSV パーサー => DataTable => BindingSource => DataGridView

編集結果の CSV ファイルへの書き出しは以下の通りです。ユーザーが DataGridView を操作して編集した結果は自動的に DataTable に反映されるので、自作メソッドで DataTable からデータを読んで CSV ファイルに書き出すようにしています。

DataTable => 自作メソッド => CSV ファイル

CSV パーサーには、(1) Microsoft が提供している Visual Basic .NET 用のクラスライブラリ TextFieldParser と、(2) ADO.NET + OleDb + JET (ACE も使用可) を使ったもの 2 種類を試してみました。

そのサンプルコードはこの記事の下の方にアップしておきます。

TextFieldParser 版は、CSV ファイルのカラム数とその型は不定という前提で、各カラムの型指定はせず全カラムを string 型として扱っています。

上にも書きましたが、CSV ファイルは 1 行目がヘッダであることが条件で、ヘッダのフィールドを DataTable の列の名前に設定しています。そうしておけば DataTable を DataGridView にバインドした際、 DataTable の列名が DataGridView のヘッダに自動的に設定されます。

TextFieldParser は CSV ファイルに BOM が付与されていればそれから文字エンコーディングを自動判別し、BOM 無しの場合はコンストラクタに設定した文字エンコーディングの指定に従います。この記事のサンプル CSV ファイルは BOM 無しの UTF-8 で作ったので、文字エンコーディングの指定は UTF-8 としています。

ADO.NET + OleDb + JET 版は、CSV ファイルの各列の型を Schema.ini ファイルを使用して指定し、OleDbDataAdapter を利用して DataTable を生成した際 DataColumn に型が反映されるようにしました。

schema.ini の内容は以下の通りです。CSV ファイルと同じディレクトリに置けば自動的に情報を取得して DataTable を作ってくれます。各列の型だけでなく文字コードも指定できます。

schema.ini

作成された DataTable の各列には schema.ini によって .NET の型 Int32, string, decimal, Int16, bool が設定されます。結果、DataGridView の表示も違ってきて以下のように��ります。bool 型の Discontinued 列に表示されているのはチェックボックスになっているのが分かるでしょうか。

CSV ファイルを DataGridView に表示(その2)

上の DataGridView の画像を表示したサンプルコードを以下に書いておきます。TextFieldParser 版と ADO.NET + OleDb + JET 版の両方のコードが含まれています(後者はコメントアウトしてます)。

using System;
using System.ComponentModel;
using System.Data;
using System.IO;
using System.Text;
using System.Windows.Forms;
using Microsoft.VisualBasic.FileIO;
using System.Data.OleDb;

namespace WinFormsApp1
{
    public partial class Form8 : Form
    {
        private BindingSource bindingSource1;
        private DataTable table;

        public Form8()
        {
            InitializeComponent();

            this.components = new Container();
            this.bindingSource1 = 
                new BindingSource(this.components);
            this.dataGridView1.DataSource = this.bindingSource1;
        }

        private void Form8_Load(object sender, EventArgs e)
        {
            // CSV ファイルのあるフォルダ、ファイル名の指定
            // CSV ファイルは 1 行目がヘッダであることが条件
            string filePath = @"CSV ファイルのあるフォルダ";
            string fileName = "products.csv";

            // CSV ファイルを読む際の文字エンコーディングを指定
            Encoding encoding = new UTF8Encoding();

            // CSV ファイルから DataTable を作成し DataGridView
            // に表示

            // TextFieldParser 版
            this.table =
                CreateDataTable(filePath + fileName, encoding);

            // ADO.NET + OleDb + JET 版
            //this.table =
            //    CreateDataTableByJet(filePath, fileName);

            this.bindingSource1.DataSource = this.table;
        }

        // テキストボックス入力により ProductName をあいまい検索
        private void button1_Click(object sender, EventArgs e)
        {
            if (!String.IsNullOrEmpty(this.textBox1.Text))
            {
                this.table.DefaultView.RowFilter =
                    "ProductName LIKE '%" +
                    this.textBox1.Text + "%'";

                //this.bindingSource1.DataSource = this.table;
                // 上のコードよりこちらの方が良さそう
                this.bindingSource1.ResetBindings(false);
            }
            else
            {
                // 元に戻す
                this.table.DefaultView.RowFilter = "";
            }
        }


        // DataGirdView で選択した行を非表示にする。DELETE 操作
        // はこれにより DataTable の当該行に Deleted マークを付
        // けることにより可能になる
        private void button2_Click(object sender, EventArgs e)
        {
            this.bindingSource1.RemoveCurrent();
        }


        // DataGridView を編集した結果を CSV ファイルに書き出す
        private void button3_Click(object sender, EventArgs e)
        {
            // CSV ファイルの保存先のフォルダ、ファイル名の指定
            string filePath = @"CSV ファイルの保存先のフォルダ";
            string fileName = "productsRevised.csv";

            // 保存する CSV ファイルの文字エンコーディングを指定
            Encoding encoding = new UTF8Encoding();

            // DataTable を CSV ファイルに書き出し
            SaveDataTableAsCsv(this.table,
                               filePath + fileName,
                               encoding);

            // 上のメソッドで書き出した CSV ファイルを読んで
            // DataTable を作成し、それを DataGridView に表示

            // TextFieldParser 版
            this.table =
                CreateDataTable(filePath + fileName, encoding);

            // ADO.NET + OleDb + JET 版
            //this.table =
            //    CreateDataTableByJet(filePath, fileName);

            this.bindingSource1.DataSource = this.table;
        }

        // ================ 以下ヘルパメソッド ================

        // TextFieldParser 版
        // 指定されたパスから CSV ファイルを読んできて DataTable
        // を作成。CSV ファイルのカラム数とその型は不定という前提
        // なので、全カラムを string 型として扱わざるを得ない。
        // TextFieldParser は CSV ファイルに BOM が付与されていれ
        // ばそれからエンコーディングを自動判別。BOM 無しの場合は
        // 引数の encoding の指定に従う
        protected DataTable CreateDataTable(string path, 
                                            Encoding encoding)
        {
            // TextFieldParser は Microsoft が提供している Visual
            // Basic .NET 用のクラスライブラリ。C# のアプリでも
            // Microsoft.VisualBasic.dll を参照に追加すれば利用可
            using (TextFieldParser tfp = 
                            new TextFieldParser(path, encoding))
            {
                //フィールドがデリミタで区切られている
                tfp.TextFieldType = FieldType.Delimited;

                // デリミタを , とする
                tfp.Delimiters = new string[] { "," };

                // フィールドを " で囲み、フィールド内に改行文字、
                // デリミタを含めることができるか
                tfp.HasFieldsEnclosedInQuotes = true;

                // 空白文字をトリム
                tfp.TrimWhiteSpace = true;

                // CSV ファイルは 1 行目がヘッダであることが条件
                string[] headers = tfp.ReadFields();
                int fieldCount = headers.Length;

                DataTable dt = new DataTable();
                DataRow dr;
                DataColumn dc;

                // DataTable の列の設定
                for (int i = 0; i < fieldCount; i++)
                {
                    dc = new DataColumn(headers[i], typeof(String));
                    dt.Columns.Add(dc);
                }

                // CSV のデータから DataRow を作り DataTable
                // に追加していく
                while (!tfp.EndOfData)
                {
                    string[] fields = tfp.ReadFields();

                    dr = dt.NewRow();
                    for (int i = 0; i < fieldCount; i++)
                    {
                        dr[headers[i]] = fields[i];
                    }
                    dt.Rows.Add(dr);
                }

                return dt;
            }
        }

        // ADO.NET + OleDb + JET 版 
        // 指定されたパスから CSV ファイルを読んできて DataTable
        // を作成。
        protected DataTable CreateDataTableByJet(string path, 
                                                 string fileName)
        {
            string conString =
              "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" +
              path +
              ";Extended Properties=\"text;HDR=Yes;FMT=Delimited\"";

            OleDbConnection con = new OleDbConnection(conString);
            string commText = "SELECT * FROM [" + fileName + "]";
            OleDbDataAdapter da = new OleDbDataAdapter(commText, con);

            DataTable dt = new DataTable();
            da.Fill(dt);

            return dt;
        }

        // DataTable から CSV ファイルを指定したパスに作成。
        // デフォルトで CSV ファイルの 1 行目がヘッダとなる
        private void SaveDataTableAsCsv(DataTable dt, 
                                        string path,
                                        Encoding encoding,
                                        bool isHeaderRequired = true)
        {
            int colCount = dt.Columns.Count;
            int lastColIndex = colCount - 1;

            using (var sr = new StreamWriter(path, false, encoding))
            {
                // デフォルトで true
                if (isHeaderRequired)
                {
                    for (int i = 0; i < colCount; i++)
                    {
                        // DataTable の DataColumn.Caption を CSV の
                        // ヘッダとしている
                        string header = dt.Columns[i].Caption;

                        // フィールド中にカンマ等があった場合の処理
                        header = EncloseByDoubleQuote(header);

                        sr.Write(header);
                        if (lastColIndex > i)
                        {
                            sr.Write(',');
                        }
                    }
                    sr.Write("\r\n");
                }

                foreach (DataRow row in dt.Rows)
                {
                    // OleDb + JET で作った DataTable は DataRow
                    // の RowState が Deleted のものも dt.Rows
                    // に含まれてしまう。結果、row[i].ToString() で
                    // DeletedRowInaccessibleException がスローされる
                    // TextFieldParser で作った DataTable は RowState
                    // が Deleted のものは含まれない。理由不明
                    // 前者は列に int, string, decimal, bool などの
                    // 型が schema.ini に従い指定されている、後者は
                    // 全列が string 型。それぐらいしか違いはないが?

                    // RowState を判定して対応するコードを追加
                    if (row.RowState == DataRowState.Unchanged ||
                        row.RowState == DataRowState.Added ||
                        row.RowState == DataRowState.Modified)
                    {

                        for (int i = 0; i < colCount; i++)
                        {
                            // DataRowVersion.Current のデータを取得
                            // row[i] としても同じはずだが念のため
                            string field = 
                                row[i, DataRowVersion.Current].ToString();

                            // フィールド中にカンマ等があった場合の処理
                            field = EncloseByDoubleQuote(field);

                            sr.Write(field);
                            if (lastColIndex > i)
                            {
                                sr.Write(',');
                            }

                        }
                        sr.Write("\r\n");
                    }
                }
            }
        }

        // フィールドの中にダブルクォーテーション、カンマ、改行が
        // 含まれていた場合の処理。フィールド前後の空白文字はトリム
        // してしまうことにした
        private string EncloseByDoubleQuote(string field)
        {
            field = field.Trim();

            // フィールドに " が含まれている
            if (field.IndexOf('"') > -1)
            {
                // " を "" に置き換え
                field = field.Replace("\"", "\"\"");
                return $"\"{field}\"";
            }

            // フィールドにカンマ、改行が含まれている
            if (field.IndexOf(',') > -1 ||
                field.IndexOf('\r') > -1 ||
                field.IndexOf('\n') > -1)
            {
                return $"\"{field}\"";
            }

            return field;
        }
    }
}

Tags: , , ,

.NET Framework

Dispose パターン (その 2)

by WebSurfer 2020年6月20日 13:32

先の記事「Dispose パターン」で、Visual Studio による Dispose パターンの自動生成機能の説明と、その機能を利用して自作カスタムクラスに Dispose パターンを実装する例を書きました。

この記事では Dispose パターンを実装済のクラスを継承してカスタムクラスを作成する際に、それに Dispose が必要な .NET のクラスのインスタンスを保持する場合、どのようなコードを書けばよいかについて述べます。

説明に ASP.NET MVC のプロジェクトを作成するテンプレートで ASP.NET Identity を実装した時に生成される AccountController の例を挙げます。自分が書いたコードで説明するより説得力があると思いますので。(笑)

Visual Studio が自動生成する Controllers/AccountController.cs のコードは以下のようになっています。この記事の説明に不要な部分は省略しています。

namespace Mvc5App2.Controllers
{
    [Authorize]
    public class AccountController : Controller
    {
        private ApplicationSignInManager _signInManager;
        private ApplicationUserManager _userManager;

        // ・・・中略・・・

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_userManager != null)
                {
                    _userManager.Dispose();
                    _userManager = null;
                }

                if (_signInManager != null)
                {
                    _signInManager.Dispose();
                    _signInManager = null;
                }
            }

            base.Dispose(disposing);
        }

        // ・・・中略・・・
    }
}

AccountController の継承元 Controller クラスは IDisposable インターフェイスを継承しており Dispose パターンを実装しています。

フィールド _signInManager と _userManager には、ApplicationSignInManager と ApplicationUserManager が初期化されてそれらのオブジェクトへの参照が代入されるようにコーディングされています。

ApplicationSignInManager と ApplicationUserManager は継承元が IDisposable インターフェイスを継承しており Dispose パターンを実装しています。

AccountController には上に紹介した「Dispose パターン」の記事のような Dispose パターンのコードは実装できませんが、AccountController が Dispose されるときには _signInManager と _userManager も Dispose する必要があります。

というわけで、上のコードのように、Controller クラスが実装している Dispose(bool) メソッドをオーバーライドして _signInManager と _userManager が Dispose されるようにしています。

ASP.NET が要求の処理を終えてコントローラーをアンロードする際、自動的にコントローラーの Dispose() メソッドが実行されます。デバッガで上の Dispose(bool) メソッド内にブレークポイントを置いて実行し、AccountController のアクションメソッドを呼び出してみてください。Dispose(bool) メソッドに制御が飛んでくることで Dispose されるのが分かると思います。

(自分が試した限りですが、上の Dispose(bool) メソッドに制御が飛んでくる前のどこかで _signInManager と _userManager は Dispose されるようで、デバッガで値を調べると null になっていました。それゆえ null をチェックするコードが入っているようです)

次に、Windows Forms アプリの例を紹介します。下のコードは Visual Studio で自動生成されたフォームの .Designer.cs のコードです。(この記事の説明に不要な部分は省略しています)

namespace WindowsFormsApplication1
{
    partial class Form3
    {
        private System.ComponentModel.IContainer components = null;

        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        private void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();
            this.bindingSource1 = new BindingSource(this.components);
            this.bindingNavigator1 = new BindingNavigator(this.components);
            // ・・・中略・・・
        }

        // ・・・中略・・・
}

上のコードで Form3 は Form クラスを継承しており、Form クラスは Dispose パターンを実装しています。なので、上の AccountController の場合と同様に、Form クラスが実装している Dispose(bool) メソッドをオーバーライドし、内部で使用された Dispose が必要な .NET のクラスを Dispose するようになっています。

AccountController の場合とは違って .NET のクラスへの参照を直接 Dispose するのではなく、Container クラス(コンポーネントをカプセル化し追跡するコンテナ)にまとめて Dispose しています。

デザイナ画面で Dispose が必要なコントロールを Form にドラッグ&ドロップすると、.Designer.cs の InitializeComponent メソッド内に Container クラスを初期化し、そのコントロールを Container オブジェクトに追加するコードが自動生成されます。上のコード例では、BindingSource と BindingNavigator が Container オブジェクトに追加されています。

そのあたりの詳しい説明は @IT の記事「第4回 Visual Studio 2010のひな型コードを理解する (3/4)」が参考になると思います。

なお、デザイナ画面を使わないで自分でコードを書いた場合は、InitializeComponent メソッド内には上のようなコードは自動生成されませんので、.cs ファイルの方に自力でコードを書くことになります。具体例は別の記事「XML ファイルを DataGridView に表示」のコードを見てください。

また、IDisposable を継承しているクラスでも、全てが自動的に Container オブジェクトに追加されることはないです。例えば DataGridView クラスとか DataSet クラスがそうです。ホントに Dispose が必要かという疑問はありますが、IDisposable を継承しているクラスは使い終わったら Dispose するのが基本のようですので、上に紹介した記事のように自分でコードを書いて Container オブジェクトに追加した方がよさそうです。(初期化する前に Add しても無効のようですので注意してください)

Container オブジェクトに登録しておけば、フォームの右上の × 印アイコンをクリックするなどしてフォームを閉じる際に、自動的にフォームの Dispose() メソッドが実行され、上のコードの Dispose(bool) メソッドも実行されます。(これもブレークポイントを置いて実行してみれば、そこに制御が飛んでくることで Dispose されるのが分かると思います)

Tags: ,

.NET Framework

コレクションを表現した JSON のデシリアライズ

by WebSurfer 2020年6月3日 15:49

以下のようなコレクション(配列)を表現した JSON 文字列を C# のオブジェクトにデシリアライズするときにハマった話を書きます。

こんなことにハマるのは無知だからだと言われそうですが、また無駄な時間を費やさないように備忘録として書いておくことにしました。

[
  {
    "id":1,
    "text":"project #1",
    "start_date":"2020-05-06",
    "end_date":"2020-05-26",
    "user":3,
    "duration":20,
    "parent":0,
    "progress":0
  },
  {
    "id":2,
    "text":"Task #1",
    "start_date":"2020-05-06",
    "end_date":"2020-05-16",
    "user":0,
    "duration":10,
    "parent":1,
    "progress":0
  },
  {
    "id":3,
    "text":"Task #2",
    "start_date":"2020-05-16",
    "end_date":"2020-05-26",
    "user":0,
    "duration":10,
    "parent":1,
    "progress":0
  }
]

先の記事「JSON 文字列から C# のクラス定義生成」で書きましたように、Visual Studio を使って JSON 文字列から C# のオブジェクトのクラス定義を生成することができます。

上の JSON 文字列から Visual Studio で C# のクラス定義を生成すると以下の通りとなります。

public class Rootobject
{
    public Class1[] Property1 { get; set; }
}

public class Class1
{
    public int id { get; set; }
    public string text { get; set; }
    public string start_date { get; set; }
    public string end_date { get; set; }
    public int user { get; set; }
    public int duration { get; set; }
    public int parent { get; set; }
    public int progress { get; set; }
}

Newtonsoft.Json と JavaScriptSerializer で上に書いた JSON 文字列を Rootobject にデシリアライズしてみます。コード例は以下の通りです。

// Newtonsoft.Json
var result1 = JsonConvert.DeserializeObject<Rootobject>(jsonText);

// JavaScriptSerializer
var serializer = new JavaScriptSerializer();
var result2 = serializer.Deserialize<Rootobject>(jsonText);

そうすると、Newtonsoft.Json では JsonSerializationException 例外が、JavaScriptSerializer では InvalidOperationException 例外がスローされます。Newtonsoft.Json が表示するエラーメッセージは以下の通りでした。

Newtonsoft.Json.JsonSerializationException: 'Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'ConsoleAppJson.Rootobject' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly. To fix this error either change the JSON to a JSON object (e.g. {"name":"value"}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List<T> that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array. Path '', line 1, position 1.'

これによると Rootobject ではダメで、List<Class1> にデシリアライズしろということのようです。やってみたら確かに List<Class1> にはデシリアライズできました。

なお、上のような配列だけの JSON 文字列ではなくて、例えば { "data": [ { ... },{ ... },{ ... } ] } のような形の JSON 文字列にした場合は、Visual Studio により生成される C# のクラス定義はほぼ同じになりますが、Rootobject にデシリアライズできます。

[ { ... },{ ... },{ ... } ] という形の JSON 文字列だけが要注意のようです。

.NET Framework 版の ASP.NET MVC は JavaScriptSerializer を、ASP.NET Web API は Newtonsoft.Json を使っているので、上のような JSON 文字列を送信してアクションメソッドに渡す場合が自分的に要注意だと思いました。

Tags: , ,

.NET Framework

About this blog

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

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar