はじめに
C#(.NET Framework)を使った、Windows.FormsでのXMLファイルの書き込み(シリアライズ)、読み込み(デシリアライズ)方法について自分なりの実装方法をまとめていきます。
また、読み込み時のテキストボックス上での表示の際、シリアル化時に保持した改行が維持されない問題についての対応策も紹介します。シリアライザーの仕様を把握していなかった僕がハマったところです。
JSON希望の人はブラウザバックだよ!
環境・前提条件
- Windows10
- Visual Studio 2019 V16.6.3(C#)
- .NET Framework 4.8
今回のXMLファイルの入出力は、自作したクラスオブジェクトに対し、読み書きを行っていく方法になります。
以後、書き込み=シリアル化、読み込み=逆シリアル化とします。
XMLファイルへシリアル化する
XMLファイルへのシリアル化に使用する自作クラスを定義する
まずは、サンプルとして、以下の自作クラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | using System; using System.Runtime.Serialization; namespace SimpleNotePadApp { /// <summary> /// メモクラス /// </summary> [DataContract] public class Note { /// <summary> /// タイトル /// </summary> [DataMember] public string Title { get; set; } /// <summary> /// 日付 /// </summary> [DataMember] public DateTime Date { get; set; } /// <summary> /// 内容 /// </summary> [DataMember] public string Content { get; set; } } } |
XMLのシリアル化には、古典的なXMLSerializerクラス 、機能が強化されたDataContractSerializer、JSONが扱えるjsonserializerを使うかのどれかだと思いますが、今回は機能面、パフォーマンスに優れたDataContractSerializerを使っていきます。
ArrayListやDictionary、HashTable、列挙型なんかもシリアル化できるみたいですよ。
DataContractSerializerクラスを使用するには、3行目に記述してあるRuntime.Serialization名前空間が必要です。
シリアル化を行う対象のクラスに、10行目のDataContractAttribute属性を適用します。この型を指定することで、シリアライザーによるシリアル化が可能であることを明示的に示します。
そして、シリアル化を行う対象のメンバそれぞれにDataMenberAttribute属性を適用します。これは、アクセス指定子による影響を受けることは無く、publicでもprivateでも何でもOKです。また、ここでDataMenberを設定していないメンバがある場合の挙動としては、シリアル化の対象になることはなく、逆シリアル化の際にはNullで出力されるようになります。
この後、この自作クラスをリスト化していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /// <summary> /// 登録・修正ボタン /// </summary> private void Register(object sender, EventArgs e) { if (correctFormat()) return; if (OperationType == "登録") { noteList.Add(new Note { Title = title.Text, Date = DateTime.Parse(dateTimePicker.Text), Content = content.Text }); } // 〜 } |
上記のようなフォームを作成し、適当に記入したメモの内容をリストに格納していきます。
DataContractSerializerオブジェクトを生成し、XMLで保存する。
XMLでのファイル保存方法は以下のとおりです。一応ダイアログ操作からの流れを記載しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | //FileIO.cs /// <summary> /// ファイルをXML形式で名前を付けて保存する /// </summary> public void SaveFile(List NoteList) { SaveFileDialog sa = new SaveFileDialog(); sa.Title = "名前を付けて保存"; sa.InitialDirectory = @"C:\Users\sample\Documents\SimpleNotePadApp_Data"; sa.FileName = @"sample.xml"; sa.Filter = "XML ファイル (*.xml)|*.xml"; sa.FilterIndex = 1; sa.OverwritePrompt = false; if (sa.ShowDialog() == DialogResult.OK) { string fileName = sa.FileName; if (System.IO.File.Exists(fileName)) { DialogResult overWrite = MessageBox.Show("同名のファイルが存在します。\n" + "上書きしてもよろしいでしょうか?", "名前を付けて保存の確認", MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button2, MessageBoxOptions.ServiceNotification); if (overWrite == DialogResult.Yes) { Serializer(fileName, NoteList); } } else { Serializer(fileName, NoteList); } } } /// <summary> /// ファイルをXML形式で保存する /// </summary> private void Serializer(string fileName, List NoteList) { // オブジェクトの生成と型指定 var serializer = new DataContractSerializer(typeof(List)); //ファイルを開く XmlWriterSettings settings = new XmlWriterSettings(); settings.Encoding = new System.Text.UTF8Encoding(false); // XMLファイルで保存する using (XmlWriter xw = XmlWriter.Create(fileName, settings)) { serializer.WriteObject(xw, NoteList); } } |
ダイアログで設定したパス、自作クラスを入れたリストを渡しています。ダイアログのフィルター文字列、何回見ても忘れる笑
DataContractSerializerに必要な名前空間は以下の2つです。シリアル化・逆シリアル化どちらも必要です。
- System.Runtime.Serialization;
- System.Xml;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // FileIO.cs /// <summary> /// ファイルをXML形式で保存する /// </summary> private void Serializer(string fileName, List NoteList) { // オブジェクトの生成と型指定 var serializer = new DataContractSerializer(typeof(List)); //ファイルを開く XmlWriterSettings settings = new XmlWriterSettings(); settings.Encoding = new System.Text.UTF8Encoding(false); // XMLファイルで保存する using (XmlWriter xw = XmlWriter.Create(fileName, settings)) { serializer.WriteObject(xw, NoteList); } } |
8行目で、DataContractSerializerオブジェクトを生成し、保存を行うオブジェクトの型を指定してあげます。
10〜11行目で、保存場所として受け取ったファイル(filename)をBOM無しのUTF-8で開きます。BOMとはバイトオーダーマーク(ByteOrderMark)の略で、Unicodeで符号化したテキストに付与される補足データのことです。
「UTF〜」から始まる符号化形式には、UTF-16やUTF-32等いくつか種類があり、符号化形式のどれを用いているのか、BOMで判別できるようにしているわけですね。ここでは、コンパイラに対してBOM無しで、UTF-8を用いるようにと記述しています。
余談ですが、UTF-8はUnicode規格によってBOMは付けるべきではないとされています。これは、符号化形式によって異なりますし、使用するソフトウェアによってもBOMの定義を必須とするか禁止とするか違います。BOMによって文字コードを明確に判別できるといった利点がある反面、付与されるデータによって問題が生じる場合もあるんですね。
UTF-8には付けなくていいBOMが一応許容されています。UTF8Encoding(False)の引数をTrueにすることで、BOM有りになります。
17行目から、XMLファイルへの書き込み・保存を行っています。パスと書き込み時の設定値をパラメータとして渡してあげて、WriteObjectで書き込みをしています。オブジェクトの破棄を忘れないようにusingステートメントで囲ってあげます。
以上が、XMLファイルの保存方法になります。以下のように、ダイアログで指定したパスにXMLファイルが保存されていることが確認できます。
XMLファイルを逆シリアル化する
DataContractSerializerオブジェクトを生成し、XMLで読み込む
逆シリアル化についても、シリアル化の時の手順とほぼ同じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // FileIO.cs /// <summary> /// ファイルを読み込む /// </summary> public List ReadFile(List NoteList) { OpenFileDialog ofd = new OpenFileDialog(); ofd.InitialDirectory = @"C:\Users\sample\Documents\SimpleNotePadApp_Data"; ofd.FileName = "sample.xml"; ofd.Filter = "XML ファイル (*.xml)|*.xml|" + "TXTファイル (*.txt)|*.txt"; ofd.FilterIndex = 1; ofd.Title = "ファイルを開く"; ofd.RestoreDirectory = true; if (ofd.ShowDialog() == DialogResult.OK) { string fileName = ofd.FileName; //オブジェクトの生成と型指定 var serializer = new DataContractSerializer(typeof(List)); using (var reader = XmlReader.Create(fileName)) { //XMLファイルから読み込み、逆シリアル化する NoteList = (List)serializer.ReadObject(reader); } } return NoteList; } |
19行目で、DataContractSerializerオブジェクトの生成と型指定を行い、XMLファイルをパスで指定します。
24行目でXMLファイルの読み込みをしています。ReadObjectメソッドの戻り値はObject型ですので、キャストが必要になります。
XMLファイルの読み込み時に改行が維持されない時は?
先述した読み込み方法では、マルチラインのテキストボックスで保存している場合は、改行が維持されず、空白を詰めて読み込んでしまいます。
この場合には保存時ではなく、読み込み時にXMLドキュメントのあらゆる設定が行えるXML Documentクラスを用意し、設定値をXML NordReaderオブジェクトに渡してあげます。XML NodeReaderクラスはファイルへの高速なアクセスを可能とするリーダーです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //FileIO.cs if (ofd.ShowDialog() == DialogResult.OK) { string fileName = ofd.FileName; XmlDocument doc = new XmlDocument(); doc.PreserveWhitespace = true; doc.Load(fileName); using (XmlNodeReader reader = new XmlNodeReader(doc.DocumentElement)) { var serializer = new DataContractSerializer(typeof(List)); NoteList = (List)serializer.ReadObject(reader); } } return TaskList; |
6行目で、XML Documentクラスのインスタンスを生成し、要素に含まれるスペースを保存するPreserveWhitespaceプロパティをTrueに設定します。次の行でファイルパスを取り込み、設定を完了します。
XMLSerializerやDatacontractSerializerには、同じ機能のプロパティが見つからなかったのでXMLドキュメントクラスを用いた対応方法です。これで、読み込み時に改行が維持されるようになります。
- LF(Line Feed)
カーソルを新しい行に移動する。(UNIX系で用いられる。LinuxやmacOS、AIX等) - CR(Carriage Return)
カーソルを左端の位置に戻す。(Mac OS等) - CR+LF
CRとLFの組み合わせ。左端にカーソルを戻して改行する。(Windows、MS-DOS等)
改行されなくなっていた理由としては、入力時の改行コードの仕様とSerializerの改行コードの仕様が異なっていたからです。
改行コードとは、コンピュータにおける改行を表す制御文字のことで、ASCII文字コードを用いるシステムでは以上の3つの種類があります。Windows環境の.NET Frameworkは、改行コードがCR+LFですが、Serializerの標準改行コードがLFです。テキストボックスがLFを認識しないために、改行が無視されるようになっていたわけですね。
さいごに
改行を維持する方法は、XMLSerializer、DataContractSerializerの機能では対応していないんですね〜。
改行コードなんて全く意識することが無かったよ。