CSV ファイルからデータを取得して Windows Forms アプリの DataGridView に表示し、それをユーザーが編集して結果を CSV ファイルに書き出すサンプルを書きます。先の記事「XML ファイルを DataGridView に表示」の CSV 版です。
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 を作ってくれます。各列の型だけでなく文字コードも指定できます。
作成された DataTable の各列には schema.ini によって .NET の型 Int32, string, decimal, Int16, bool が設定されます。結果、DataGridView の表示も違ってきて以下のように��ります。bool 型の Discontinued 列に表示されているのはチェックボックスになっているのが分かるでしょうか。
上の 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;
}
}
}