アプリケーションの設定の保存

設定保存の基本

アプリケーション設定を保存するための手軽な手順はVisual Studioでの設定ですが、使い勝手など微妙な面もあります。

Visual Studioでアプリケーションの設定を保存する: .NET Tips: C#, VB.NET
http://dobon.net/vb/dotnet/programing/mysettings.html

自前で作る場合には、設定データをクラス化するのが妥当でしょう。アプリケーションにもよりますが、シングルトン化した方が楽な場合も多いと思います(一般に設定は書き込み頻度が低いので、マルチスレッドの競合もあまりないはず)。

C# での Singleton についてまとめ - やこ~ん SuperNova2
http://d.hatena.ne.jp/saiya_moebius/20091017/1255799846

さて、自前で設定クラスを作る場合、永続化が必要です。保存形式としては、一部web業界はともかく、バイナリはきついので、テキストで保存したいところ。候補はXMLJSONでしょう。

XML保存

最も簡単に処理する場合はXmlSerializerを用います。

オブジェクトの内容をXMLファイルに保存、復元する: .NET Tips: C#, VB.NET
http://dobon.net/vb/dotnet/file/xmlserializer.html

例外発生を考慮すると、こんな感じかと思います。 なお、using指定は過剰な項目を含んでいると思います。ご了承ください。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Drawing;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Xml.Serialization;
using System.ComponentModel;

ここまでがXMLJSON両方を含めたusing指定(たぶんやや過剰)。

あとは設定クラス内に保存と読み込みのメソッドを配置。

         public static bool SaveXml(Config c)
         {
             var result = true;

             var s = new XmlSerializer(typeof(Config));
             try
             {
                 using (var f = new FileStream(XmlFilename, FileMode.Create))
                 {
                     try
                     {
                         s.Serialize(f, c);
                     }
                     catch (Exception)
                     {
                         System.Diagnostics.Debug.WriteLine(ex.Message);
                         result = false;
                     }
                 }
             }
             catch (Exception ex)
             {
                 System.Diagnostics.Debug.WriteLine(ex.Message);
                 result = false;
             }

             return result;
         }

         public static Config LoadXml()
         {
             var s = new XmlSerializer(typeof(Config));
             Config c = null;

             try
             {
                 using (var f = new FileStream(XmlFilename, FileMode.Open))
                 {
                     try
                     {
                         c = (Config)s.Deserialize(f);
                     }
                     catch (Exception ex)
                     {
                         System.Diagnostics.Debug.WriteLine(ex.Message);
                         c = null;
                     }
                 }
             }
             catch (Exception ex)
             {
                 System.Diagnostics.Debug.WriteLine(ex.Message);
                 c = null;
             }

             return c;
         }
}

JSON保存

外部ライブラリなしなら、DataContractJsonSerializerが使えます。

C#でお手軽ハイブリッドアプリケーション (2) - ごった日記
http://mokake.hatenablog.com/entry/2016/01/09/013149

保存/読み込みメソッドのみ。

         public static bool SaveJson(Config c)
         {
             var result = true;

             var s = new DataContractJsonSerializer(typeof(Config));
             try
             {
                 using (var f = new FileStream(JsonFilename, FileMode.Create))
                 {
                     try
                     {
                         s.WriteObject(f, c);
                     }
                     catch (Exception ex)
                     {
                         System.Diagnostics.Debug.WriteLine(ex.Message);
                         result = false;
                     }
                 }
             }
             catch (Exception ex)
             {
                 System.Diagnostics.Debug.WriteLine(ex.Message);
                 result = false;
             }

             return result;
         }

         public static Config LoadJson()
         {
             var s = new DataContractJsonSerializer(typeof(Config));
             Config c = null;

             try
             {
                 using (var f = new FileStream(JsonFilename, FileMode.Open))
                 {
                     try
                     {
                         c = (Config)s.ReadObject(f);
                     }
                     catch (Exception ex)
                     {
                         System.Diagnostics.Debug.WriteLine(ex.Message);
                         c = null;
                     }
                 }
             }
             catch (Exception ex)
             {
                 System.Diagnostics.Debug.WriteLine(ex.Message);
                 c = null;
             }

             return c;
         }

クラスや構造体を含む場合

上記を設定クラスに含めた状態で呼び出しても、設定クラスによっては実行時例外になります。 理由は簡単で、XML/JSON処理できない要素が含まれるからです。 設定での定番はフォントじゃないでしょうか。

ちなみに、色構造体(System.Drawing.Color)を使っている場合は、例外は出ないが再現できないという悪夢になる場合もあるようです(自分ではなった)。

XmlSerializerもDataContractJsonSerializerも、残念ながらFontやColorをうまく処理できません(Fontは保存時例外、Colorは保存は通るものの保存ファイルのColor指定には中身がない状態)。

この問題を解決するには、TypeDescriptorを明示的に指定する方法が使えます。 クラス内にprivateでメソッドを追加してみます。

         private static string ConvertToString<T>(T value)
         {
             try
             {
                 return TypeDescriptor.GetConverter(typeof(T)).ConvertToString(value);
             }
             catch (Exception ex)
             {
                 System.Diagnostics.Debug.WriteLine(ex.Message);
                 return null;
             }
         }

         private static T ConvertFromString<T>(string s)
         {
             try
             {
                 return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromString(s);
             }
             catch (Exception ex)
             {
                 System.Diagnostics.Debug.WriteLine(ex.Message);
                 return default(T);
             }
         }

設定情報の定義側では、本来のクラスや構造体は保存対象から外して、代わりに文字列プロパティをXML/JSON処理対象とします。

XMLでは無視項目にXmlIgnore属性を指定し、JSONでは設定クラス自体にDataContract属性をつけ、JSON処理対象項目全てにDataMember属性を指定します。

ここではフォントを例に、XMLJSONの両方に対応する例を挙げます。

         [XmlIgnore]
         public Font UIFont { get; set; }
         [DataMember]
         public string UIFontText
         {
             get { return ConvertToString(UIFont); }
             set
             {
                 var f = ConvertFromString<Font>(value);
                 if (f != null) { UIFont = f; }
             }
         }

構造体の読み込みエラー判定

フォントの場合は、読み込んだ時に不正な文字列ならnullが返るのでエラー判定が可能ですが、色など構造体では、先のコードだと(まさにnullがとれない構造体対策として)default(T)を返しているため、判定が不可能(その値が正しく記述されていたのかエラーなのか判断できない)です。

それならTryParseのように、メソッド内で代入してしまえば……

         private static bool TryConvertFromString<T>(string s,ref T target)
         {
             try
             {
                 var v = (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromString(s);
                 target = v;
                 return true;
             }
             catch (Exception ex)
             {
                 System.Diagnostics.Debug.WriteLine(ex.Message);
                 return false;
             }
         }

しかし、この方法はビルドできません。 ref引数にプロパティはとれないため、メソッド呼び出し側でエラーになります。

ということで、簡単な対応として、null許容型にしてみます。

         [XmlIgnore]
         public Color UIColor { get; set; }
         [DataMember]
         public string UIColorText
         {
             get { return ConvertToString(UIColor); }
             set
             {
                 var c = ConvertFromString<Color?>(value);
                 if (c != null) { UIColor = (Color)c; }
             }
         }

設定項目の中のクラスや構造体すべてについて、このような代替項目を記述することになります。 やはりXmlSerializerやDataContractJsonSerializerに対応してほしかったところですね。

設定項目のクラスや構造体自体をXML/JSON対応にしてしまう

もしクラス定義を変えてもいいのであれば、クラス自体にXML/JSON入出力機能をつけておくのも一手です。

c# - How to XmlSerialize System.Drawing.Font class - Stack Overflow
http://stackoverflow.com/questions/1940127/how-to-xmlserialize-system-drawing-font-class

保存先

設定データの保存先としては、Windowsだと現在はアプリケーションデータ(%APPDATA%)が良い印象です。UACの影響も受けず、実利用例も多く(自分のAPPDATAを見るとわかると思います)、比較的容易にアクセスできます。

Environment.SpecialFolder on Windows, Linux and OS X – Jaroslav IMRICH
https://www.jimrich.sk/2015/01/18/environment-specialfolder-on-windows-linux-and-os-x/

Windows以外を含めても、上記のように、System.Environment.SpecialFolder.ApplicationDataは良好です。

  • Windows 8.1 + .NET 4.5 → C:\Users\(ユーザ名)\AppData\Roaming
  • Ubuntu 14.04 + Mono 3.2.8 → /home/(ユーザ名)/.config
  • Mac OS X 10.10.1 + Mono 3.10.0 → /Users/(ユーザ名)/.config

もし、USBメモリで持ち運べるポータブルアプリケーションにしたいなら、実行ファイル自身のいるフォルダが起点になるでしょう。

自分のアプリケーションの実行ファイルのパスを取得する、VB6のApp.Pathと同じ事を行うには?
http://dobon.net/vb/dotnet/vb6/apppath.html

いくつか候補がありますが、アプリケーションに組み込むのであれば、

  • 通常なら、System.Reflection.Assembly.GetExecutingAssembly().Location
  • もし特殊な仮想化環境も想定するなら、System.Reflection.Assembly.GetEntryAssembly().Locationを試して、取得できなければ前者

ぐらいがいいのではないでしょうか。