銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

.NETのアプリケーション設定を編集可能なXMLに保存する

f:id:takamints:20170221171853p:plain

.NETのプロジェクトでは「アプリケーション構成ファイル」というファイルに設定情報を書いておけます。 でもあれは実行時に書き換えるような利用方法は想定されていません。 インストーラーでセットアップすると、アプリケーションフォルダに置かれるため、読み取り専用で、編集するにも管理者権限が必要です(また、書き換えたつもりでも、何かのタイミングで元に戻るということも・・・どういう機構なのか知らないのですが・・・)

そこで、ここでは、編集可能なXMLファイルをアプリケーションの動的な設定情報として利用するためのサンプルコードをご紹介。 基本クラスは、お持ち帰り可能です。ご自由にご利用ください。

■■■ 目次 ■■■

  1. はじめに - 機能仕様
  2. XML設定クラス定義例:SampleSettingsクラス
  3. 設定ファイルの読み込み処理
  4. XMLファイルの例:SampleSettings.xml
  5. XML設定ファイルの基本クラス:XmlSettingFileクラス
  6. 設定ファイルはWindowsの特殊フォルダ以下へ
  7. 参考サイト
基礎からわかる C#
基礎からわかる C#
posted with amazlet at 17.02.21
シーアンドアール研究所 (2017-02-16)
売り上げランキング: 16,889

1. はじめに - 機能仕様

ここでやりたいことをザックリ以下に箇条書き。

  1. アプリ起動時に特定のフォルダに保存されたXMLの設定ファイルを読み込む。
  2. ファイルがない場合は、アプリ起動時に既定の内容で設定ファイルを作成する。
  3. 設定情報はクラスのインスタンスとしてプログラムからアクセスできる。
  4. 最低限プリミティブ型が正しく読み書きできること。

設定読み込み時の値のチェックはしていません。 バリデーション機能を持たせたメタクラスを導入すれば可能ですが、大げさになり過ぎるので、またおいおい。

2. XML設定クラス定義例:SampleSettingsクラス

以下がXML設定情報を保持するクラス。

設定ファイルに保存したい値をメンバとして定義したシンプルなクラスです。 XMLシリアライズを利用しているのでパブリックなデフォルトコンストラクタが必要です。 基本クラスの XmlSettingFileクラス は、下で説明します。

/// <summary>
/// XMLアプリケーション設定サンプル
/// </summary>
public class SampleSettings : Utils.XmlSettingFile
{
    /// <summary>
    /// 既定の設定情報を生成。
    /// (デフォルトコンストラクタは必ず必要)
    /// </summary>
    public SampleSettings() { }

    //以下のメンバーはXMLに保存される

    public string Name { get; set; } = "Koji Takami";
    public string Email { get; set; } = "vzg03566@gmail.com";
    public int FeatureOption { get; set; } = 0;
    public bool DebugOption { get; set; } = false;

    //保存したくないメンバーは以下のように宣言
    [System.Xml.Serialization.XmlIgnore]
    public string NotSaved;
}

3. 設定ファイルの読み込み処理

上のクラスのインスタンスXMLファイルから読み込むには、XmlSettingFile.Loadを使用します。 このメソッドには、保存場所のルートを決めるスペシャルフォルダの列挙子と、既定値を保持する設定情報のインスタンスを指定します。

以下のコードでは、設定ファイルを読み込んで、SampleSettingクラスのインスタンスを返します。

public partial class App : Application
{
    static public SampleSettings sampleSettings = new SampleSettings();
    public App()
    {
        try {
            sampleSettings = Utils.XmlSettingFile.Load(
                Environment.SpecialFolder.CommonApplicationData,
                new SampleSettings()) as SampleSettings;
        } catch (Exception ex) {
            Console.WriteLine("Error:{0}", ex.Message);
        } finally {
            sampleSettings.Save();
        }
    }
}
  • 上のコードで読み書きされる設定ファイルは、「C:\ProgramData\(CompanyName)\(ProductName)\(ProductVersion)\SampleSetting.xml」です。ここで、(CompanyName)(ProductName)(ProductVersion)アセンブリのファイルバージョン情報の設定値です。プロジェクトの初期状態ではCompanyNameが空白ですが、その部分抜きで正しいパスに生成されます。
  • 読み込み時にファイルがなければ、既定値のインスタンスを使用して作成されます。
  • ここでは、アプリケーションクラスのコンストラクタで読み出していますが、別のタイミングでも問題はありません。
  • 読み込んだ直後に保存しているのは、開発中に追加項目を反映したりObsoleteな設定項目をXMLから削除するためです。これがなくてもファイルがなければ作成されます。このSaveは、必ずしも必要ではありません。

4. XMLファイルの例:SampleSettings.xml

最初の実行時に生成されたファイルの中身は以下のようになります。 書き換えて読み込んだら、ちゃんと設定値が反映されます。それが目的ですからね。

<?xml version="1.0" encoding="utf-8"?>
<SampleSettings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Name>Takami Koji</Name>
  <Email>vzg03566@gmail.com</Email>
  <FeatureOption>0</FeatureOption>
  <DebugOption>false</DebugOption>
</SampleSettings>

5. XML設定ファイルの基本クラス:XmlSettingFileクラス

以下が、このエントリーの御本尊。 派生クラスのメンバーをXMLファイルへシリアライズ/デシリアライズするクラス。 (名前空間は適当に変えてください)

ファイル名の決定や、アクセス権の設定などで長くなっていますが、やってることは意外にシンプル。XmlSerializerのお陰様。

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using System.Xml.Serialization;

namespace StudyDotNet.Utils
{
    public class XmlSettingFile
    {
        /// <summary>
        /// 設定ファイルを読み込む
        /// </summary>
        /// <param name="folderId">
        ///     スペシャルフォルダを指定する</param>
        /// <param name="initial">
        ///     ファイルがない場合の既定値を持つ
        ///     インスタンス</param>
        /// <param name="filename">
        ///     ファイル名。
        ///     省略時はクラス名を採用する</param>
        /// <returns></returns>
        public static XmlSettingFile Load(
            Environment.SpecialFolder folderId,
            XmlSettingFile initial,
            string filename = null)
        {
            //ファイル名を生成
            string pathname = CreatePathname(
                folderId, initial, filename);

            //設定ファイルがなければ既定の値で生成する
            if (!File.Exists(pathname)) {
                Save(pathname, initial);
                AllowUsersReadAndWrite(pathname);
            }

            //設定ファイルを読み込む
            return Read(pathname, initial.GetType());
        }

        /// <summary>
        /// 設定ファイル名。
        /// 実際に読み込んだもの。
        /// 書き戻す場合にも利用する。
        /// </summary>
        [XmlIgnore] public string Filename { get; private set; }

        /// <summary>
        /// 設定ファイルへ書き戻す。
        /// プログラムから設定値を変更した場合などに使用。
        /// </summary>
        public void Save() { Save(Filename, this); }

        #region StaticPrivate

        /// <summary>
        /// 与えられた情報からファイル名を生成する
        /// </summary>
        /// <param name="folderId"></param>
        /// <param name="initial"></param>
        /// <param name="filename"></param>
        /// <returns></returns>
        static private string CreatePathname(
            Environment.SpecialFolder folderId,
            XmlSettingFile initial,
            string filename)
        {
            //ファイル名が指定されていないならクラス名
            if(filename == null) {
                filename = initial.GetType().Name;
            }

            //拡張子がXMLでない場合はXMLに変更する
            if(Path.GetExtension(filename) == "") {
                filename = Path.ChangeExtension(filename, "xml");
            }

            //ファイル名を決定
            string pathname = Path.Combine(
                CreateFolder(folderId), filename);

            return pathname;
        }

        /// <summary>
        /// スペシャルフォルダ以下のパスを生成
        /// </summary>
        /// <param name="folderId"></param>
        /// <returns></returns>
        static private string CreateFolder(
            Environment.SpecialFolder folderId)
        {
            //アセンブリのファイルバージョン情報を利用してフォルダを決定
            var verinfo = FileVersionInfo.GetVersionInfo(
                Assembly.GetExecutingAssembly().Location);

            // 空白の項目はパスに含まれない。
            // 例えば CompanyName が空の場合、
            // @"C:\ProgramData\ProductName\1.0.0.0" などとなる
            string path = Path.Combine(new string[] {
                Environment.GetFolderPath(folderId),
                verinfo.CompanyName,
                verinfo.ProductName,
                verinfo.ProductVersion,
            });

            //フォルダがないなら生成する
            if(!Directory.Exists(path)) {
                Directory.CreateDirectory(path);
            }

            return path;
        }

        /// <summary>
        /// 設定を設定ファイルに保存
        /// </summary>
        /// <param name="filename"></param>
        /// <param name="setting"></param>
        static private void Save(
            string filename, XmlSettingFile setting)
        {
            var writer = new StreamWriter(
                filename, false, new UTF8Encoding(false));
            var xml = new XmlSerializer(setting.GetType());
            xml.Serialize(writer, setting);
            writer.Close();
        }

        /// <summary>
        /// 設定ファイルを読み込む
        /// </summary>
        /// <param name="filename">設定ファイル</param>
        /// <param name="type">設定データの型</param>
        /// <returns>type型のインスタンス。
        /// ファイルない場合はnullを返す。</returns>
        static private XmlSettingFile Read(
            string filename, Type type)
        {
            var reader = new StreamReader(
                filename, new UTF8Encoding(false));
            var xml = new XmlSerializer(type);
            var setting = xml.Deserialize(reader) as XmlSettingFile;
            reader.Close();
            if(setting != null)
            {
                setting.Filename = filename;
            }
            return setting;
        }

        /// <summary>
        /// 指定ファイルにUsersグループのユーザーの読み書き権限を与える
        /// </summary>
        /// <param name="filename"></param>
        static private void AllowUsersReadAndWrite(string filename) {
            FileSystemAccessRule rule = new FileSystemAccessRule(
                new NTAccount("Users"),
                FileSystemRights.Write | FileSystemRights.Read,
                AccessControlType.Allow);
            FileSecurity security = File.GetAccessControl(filename);
            security.AddAccessRule(rule);
            File.SetAccessControl(filename, security);
        }

        #endregion

    }
}

6. 設定ファイルはWindowsの特殊フォルダ以下へ

参考のため、アプリケーションの設定ファイルを保存するために適当なスペシャルフォルダを抜き出してみました。 下表はDOBON.NET プログラミング道:特殊ディレクトリのパスを取得するからの抜粋です。

Windows / .NET的に設定ファイルは、以下のようなフォルダ以下に、アプリケーションの企業名、製品名、バージョンなどのフォルダを掘って、保存するのが望ましいようです。

Environment.SpecialFolder列挙体のメンバ 具体例(参考)
1 CommonApplicationData C:\ProgramData
2 ApplicationData C:\Users\(UserName)\AppData\Roaming
3 LocalApplicationData C:\Users\(UserName)\AppData\Local
4 UserProfile C:\Users\(UserName)
6 MyDocuments C:\Users\(UserName)\Documents
5 CommonDocuments C:\Users\Public\Documents

全ユーザーに共通する設定なら1が良いと思います。ユーザー別なら2か3、4もあり。ただしローミングが必要なら2であるべきですね。

5と6については非推奨。Documentsに設定ファイルを置くのはちょっと違うかもしれないですからね。

7. 参考サイト