scrap book

( ..)φメモメモ

(C#) 変更を検知して再読み込みする設定ファイルクラス

ソースコード

main

using System;
using System.Threading;

namespace ReloadableConfiguration
{
    class Program
    {
        static void Main(string[] args)
        {
            // SimpleConfigの使い方サンプル
            UseSimpleConfig();


            // ReloadableConfigの使い方サンプル
            Console.WriteLine();
            UseReloadableConfig();


            // キー入力待ち
            Console.ReadKey();
        }

        /// <summary>
        /// SimpleConfigの使い方サンプル
        /// </summary>
        static void UseSimpleConfig()
        {
            //
            //
            //
            WriteTitle("(a-1) 初回アクセス時に読み込み");

            // ファイルパスを指定
            SimpleConfig.FilePath = @"SampleFile\Sample1.xml";

            // 読み込んだ内容を出力
            Console.WriteLine("Name : " + SimpleConfig.Config.Name);

            // 設定内容を出力
            Console.WriteLine("--------------------------------");
            Console.WriteLine(SimpleConfig.Config.ToXmlStirng());


            //
            //
            //
            WriteTitle("(a-2) 明示的読み込み");

            // ファイルパスを指定
            SimpleConfig.FilePath = @"SampleFile\Sample2.xml";

            // 明示的に読み込み、成否を出力
            bool isLoadSuccess = SimpleConfig.Load();
            Console.WriteLine("読込" + (isLoadSuccess ? "成功" : "失敗"));

            // 読み込んだ内容を出力
            Console.WriteLine("Name : " + SimpleConfig.Config.Name);


            //
            //
            //
            WriteTitle("(a-3) 明示的読み込み(失敗)");

            // 存在しないファイルパスを指定
            SimpleConfig.FilePath = @"SampleFile\InvalidFilePath.xml";

            // 明示的に読み込み、成否を出力
            isLoadSuccess = SimpleConfig.Load();
            Console.WriteLine("読込" + (isLoadSuccess ? "成功" : "失敗"));

            // 読み込んだ内容を出力
            //   成功時の内容が保持される
            Console.WriteLine("Name : " + SimpleConfig.Config.Name);
        }

        /// <summary>
        /// ReloadableConfigの使い方サンプル
        /// </summary>
        static void UseReloadableConfig()
        {
            //
            //
            //
            WriteTitle("(b-1) 初回アクセス時に読み込み");

            // ファイルパスを指定
            ReloadableConfig.FilePath = @"SampleFileReloadable\SampleReload1.xml";

            // 読み込んだ内容を出力
            Console.WriteLine("Name : " + ReloadableConfig.Config.Name);

            // 設定内容を出力
            Console.WriteLine("--------------------------------");
            Console.WriteLine(ReloadableConfig.Config.ToXmlStirng());


            // 設定値の"名前"について値を変更し、ファイルを生成
            // ->  検知されないことを期待
            ReloadableConfig.Config.Name = "expect : no detect 1";
            XmlFileHelper.GenerateXmlFile(ReloadableConfig.Config, ReloadableConfig.FilePath, true);
            Thread.Sleep(500);


            //
            // 
            //
            WriteTitle("(b-2) 明示的読み込み");

            // ファイルパスを指定
            ReloadableConfig.FilePath = @"SampleFileReloadable\SampleReload2.xml";

            // 明示的に読み込み、成否を出力
            bool isLoadSuccess = ReloadableConfig.Load();
            Console.WriteLine("読込" + (isLoadSuccess ? "成功" : "失敗"));

            // 読み込んだ内容を出力
            Console.WriteLine("Name : " + ReloadableConfig.Config.Name);


            //
            //
            //
            WriteTitle("(b-3) 明示的読み込み(失敗)");

            // 存在しないファイルパスを指定
            ReloadableConfig.FilePath = @"SampleFileReloadable\InvalidFilePath.xml";

            // 明示的に読み込み、成否を出力
            isLoadSuccess = ReloadableConfig.Load();
            Console.WriteLine("読込" + (isLoadSuccess ? "成功" : "失敗"));

            // 読み込んだ内容を出力
            //   成功時の内容が保持される
            Console.WriteLine("Name : " + ReloadableConfig.Config.Name);


            //
            //
            //
            WriteTitle("(b-4) 生成の検知");

            // ファイルパスを指定
            ReloadableConfig.FilePath = @"SampleFileReloadable\SampleReloadEnabled1.xml";

            // 明示的に読み込み、成否を出力
            isLoadSuccess = ReloadableConfig.Load();
            Console.WriteLine("読込" + (isLoadSuccess ? "成功" : "失敗"));

            // 読み込んだ内容を出力
            Console.WriteLine("Name            : " + ReloadableConfig.Config.Name);
            Console.WriteLine("EnableFileWatch : " + ReloadableConfig.Config.EnableFileWatch);

            // ファイルを削除
            System.IO.File.Delete(ReloadableConfig.FilePath);
            Thread.Sleep(500);

            // 設定値の"名前"について値を変更し、ファイルを生成
            // ->  この時点でCreatedイベントハンドラが動作することを期待
            // ->  Created がハンドルされた
            ReloadableConfig.Config.Name = "sample reloadable enabled - created";
            XmlFileHelper.GenerateXmlFile(ReloadableConfig.Config, ReloadableConfig.FilePath, true);
            Thread.Sleep(500);


            //
            //
            //
            WriteTitle("(b-5) 更新の検知");

            // 設定値の"名前"について値を変更し、ファイルを生成
            // ->  この時点でChangedイベントハンドラが動作することを期待
            // ->  Changed -> Changed がハンドルされた
            ReloadableConfig.Config.Name = "sample reloadable enabled - changed";
            XmlFileHelper.GenerateXmlFile(ReloadableConfig.Config, ReloadableConfig.FilePath, true);
            Thread.Sleep(500);


            //
            //
            //
            WriteTitle("(b-6) 名前変更の検知");

            // 設定ファイルをリネームし、監視対象から外す
            // ->  検知されないことを期待
            // ->  Changed がハンドルされた
            if (System.IO.File.Exists(ReloadableConfig.FilePath + @".AfterRename"))
            {
                System.IO.File.Delete(ReloadableConfig.FilePath + @".AfterRename");
            }
            System.IO.File.Move(ReloadableConfig.FilePath, ReloadableConfig.FilePath + @".AfterRename");
            Thread.Sleep(500);

            // 検知されたことが分かるように設定値の"名前"について値を変更し、ファイルを生成
            ReloadableConfig.Config.Name = "sample reloadable enabled - renamed";
            XmlFileHelper.GenerateXmlFile(ReloadableConfig.Config, ReloadableConfig.FilePath + @".BeforeRename", true);

            // 設定ファイルをリネームし、監視対象にする
            // ->  この時点でChangedイベントハンドラが動作することを期待
            // ->  Changed がハンドルされた
            System.IO.File.Move(ReloadableConfig.FilePath + @".BeforeRename", ReloadableConfig.FilePath);
            Thread.Sleep(500);


            //
            //
            //
            WriteTitle("(b-7) ファイル監視状態の変更");

            //
            ReloadableConfig.FilePath = @"SampleFileReloadable\SampleReloadDisabled1.xml";
            ReloadableConfig.Load();

            // 設定値の"名前"について値を変更し、ファイルを生成
            // ->  検知されないことを期待
            // ->  検知されない
            ReloadableConfig.Config.Name = "reload is disabled??";
            XmlFileHelper.GenerateXmlFile(ReloadableConfig.Config, ReloadableConfig.FilePath, true);
            Thread.Sleep(500);

            Console.WriteLine(Environment.NewLine + "end.");


            // finalize
            ReloadableConfig.FinalizeFileWatch();
        }

        static void WriteTitle(string title)
        {
            Console.WriteLine();
            Console.WriteLine("================================");
            Console.WriteLine($" {title}");
            Console.WriteLine("================================");
        }
    }
}

(比較的)シンプルな設定値クラス

目標その1。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using System.Xml.Serialization;

namespace ReloadableConfiguration
{
    /// <summary>
    /// 設定
    /// </summary>
    [Serializable]
    public class SimpleConfig
    {
        #region inner class
        [Serializable]
        public class BoxVolume
        {
            [XmlAttribute]
            public int Value { get; set; }
            [XmlAttribute]
            public string Unit { get; set; }
        }

        public enum BoxItemType
        {
            Undefined,
            Fruit,
            Fish
        }

        [Serializable]
        public class BoxItem
        {
            [XmlAttribute]
            public BoxItemType BoxItemType { get; set; }
            [XmlElement]
            public string Name { get; set; }
            [XmlElement]
            public int Count { get; set; }
        }

        #endregion

        #region クラスフィールド/クラスプロパティ
        /// <summary>
        /// 設定
        /// </summary>
        private static SimpleConfig config;

        /// <summary>
        /// 設定
        /// </summary>
        public static SimpleConfig Config
        {
            get
            {
                if (config == null)
                {
                    Load();
                }

                return config;
            }
        }

        /// <summary>
        /// ファイルパス
        /// </summary>
        public static string FilePath { get; set; }

        #endregion


        [XmlAttribute]
        public string Name { get; set; }
        public BoxVolume Volume { get; set; }
        public List<BoxItem> Items { get; set; }


        #region インスタンスメソッド
        /// <summary>
        /// コンストラクタ
        /// </summary>
        private SimpleConfig()
        {
        }

        /// <summary>
        /// XML文字列化
        /// </summary>
        /// <returns>XML文字列化</returns>
        public string ToXmlStirng()
        {
            var xmlString = string.Empty;

            try
            {
                XmlSerializer serializer = new XmlSerializer(config.GetType());
                using (StringWriter writer = new StringWriter())
                {
                    serializer.Serialize(writer, config);
                    xmlString = writer.ToString();
                }
            }
            catch
            {
            }

            return xmlString;
        }

        #endregion

        #region クラスメソッド
        /// <summary>
        /// 読込
        /// </summary>
        /// <returns>成否</returns>
        public static bool Load()
        {
            bool result = true;

            try
            {
                XmlSerializer serializer = new XmlSerializer(typeof(SimpleConfig));
                using (StreamReader reader = new StreamReader(FilePath))
                {
                    config = (SimpleConfig)serializer.Deserialize(reader);
                }
            }
            catch
            {
                result = false;
            }

            return result;
        }

        #endregion
    }
}

再読み込みする設定値クラス

目標その2~4。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using System.Xml.Serialization;
using System.Windows;

namespace ReloadableConfiguration
{
    /// <summary>
    /// 設定
    /// </summary>
    [Serializable]
    public class ReloadableConfig
    {
        #region inner class
        [Serializable]
        public class BoxVolume
        {
            [XmlAttribute]
            public int Value { get; set; }
            [XmlAttribute]
            public string Unit { get; set; }
        }

        public enum BoxItemType
        {
            Undefined,
            Fruit,
            Fish
        }

        [Serializable]
        public class BoxItem
        {
            [XmlAttribute]
            public BoxItemType BoxItemType { get; set; }
            [XmlElement]
            public string Name { get; set; }
            [XmlElement]
            public int Count { get; set; }
        }

        #endregion

        #region クラスフィールド/クラスプロパティ
        /// <summary>
        /// 設定
        /// </summary>
        private static ReloadableConfig config;

        /// <summary>
        /// 設定
        /// </summary>
        public static ReloadableConfig Config
        {
            get
            {
                if (config == null)
                {
                    Load();
                }

                return config;
            }
        }

        /// <summary>
        /// ファイルパス
        /// </summary>
        public static string FilePath { get; set; }

        /// <summary>
        /// ファイル監視
        /// </summary>
        private static FileSystemWatcher watcher;

        #endregion


        /// <summary>
        /// ファイル監視有効化状態
        /// </summary>
        public Nullable<bool> EnableFileWatch { get; set; } = null;

        [XmlAttribute]
        public string Name { get; set; }
        public BoxVolume Volume { get; set; }
        public List<BoxItem> Items { get; set; }


        #region インスタンスメソッド
        /// <summary>
        /// コンストラクタ
        /// </summary>
        private ReloadableConfig()
        {
        }

        /// <summary>
        /// XML文字列化
        /// </summary>
        /// <returns>XML文字列化</returns>
        public string ToXmlStirng()
        {
            var xmlString = string.Empty;

            try
            {
                XmlSerializer serializer = new XmlSerializer(config.GetType());
                using (StringWriter writer = new StringWriter())
                {
                    serializer.Serialize(writer, config);
                    xmlString = writer.ToString();
                }
            }
            catch
            {
            }

            return xmlString;
        }

        #endregion

        #region クラスメソッド
        /// <summary>
        /// 読込
        /// </summary>
        /// <returns>成否</returns>
        public static bool Load()
        {
            bool result = true;

            try
            {
                XmlSerializer serializer = new XmlSerializer(typeof(ReloadableConfig));
                using (StreamReader reader = new StreamReader(FilePath))
                {
                    config = (ReloadableConfig)serializer.Deserialize(reader);
                }

                // ファイル監視を設定
                ConfigureFileWatch();
            }
            catch
            {
                result = false;
            }

            return result;
        }

        /// <summary>
        /// ファイル監視設定
        /// </summary>
        private static void ConfigureFileWatch()
        {
            // インスタンス生成が未だの場合、インスタンス生成と初期化を実施
            if (watcher == null)
            {
                watcher = new FileSystemWatcher();

                watcher.Created += Watcher_Created;
                watcher.Changed += Watcher_Changed;
                watcher.Renamed += Watcher_Renamed;

                watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName;
                watcher.IncludeSubdirectories = false;
            }

            // 一時的にファイル監視を無効化
            watcher.EnableRaisingEvents = false;

            // 監視対象ファイルパスを設定
            watcher.Path = Path.GetDirectoryName(FilePath);
            watcher.Filter = Path.GetFileName(FilePath);

            // 設定値に従いファイル監視の有効/無効を変更
            watcher.EnableRaisingEvents = config.EnableFileWatch.GetValueOrDefault(false);
        }

        /// <summary>
        /// ファイル監視終了
        /// </summary>
        public static void FinalizeFileWatch()
        {
            watcher?.Dispose();
            watcher = null;
        }

        /// <summary>
        /// ファイル監視イベント(作成)
        /// </summary>
        /// <param name="sender">イベント元</param>
        /// <param name="e">イベント引数</param>
        private static void Watcher_Created(object sender, FileSystemEventArgs e)
        {
            Console.WriteLine(System.Reflection.MethodInfo.GetCurrentMethod().Name + "()");
            Console.WriteLine("  " + e.ChangeType);
            Console.WriteLine("  " + e.FullPath);

            if (Path.Combine(((FileSystemWatcher)sender).Path, ((FileSystemWatcher)sender).Filter) == e.FullPath)
            {
                Load();
                Console.WriteLine("  reloaded!! : " + config.Name);
            }
        }

        /// <summary>
        /// ファイル監視イベント(変更)
        /// </summary>
        /// <param name="sender">イベント元</param>
        /// <param name="e">イベント引数</param>
        private static void Watcher_Changed(object sender, FileSystemEventArgs e)
        {
            Console.WriteLine(System.Reflection.MethodInfo.GetCurrentMethod().Name + "()");
            Console.WriteLine("  " + e.ChangeType);
            Console.WriteLine("  " + e.FullPath);

            if (Path.Combine(((FileSystemWatcher)sender).Path, ((FileSystemWatcher)sender).Filter) == e.FullPath)
            {
                Load();
                Console.WriteLine("  reloaded!! : " + config.Name);
            }
        }

        /// <summary>
        /// ファイル監視イベント(リネーム)
        /// </summary>
        /// <param name="sender">イベント元</param>
        /// <param name="e">イベント引数</param>
        private static void Watcher_Renamed(object sender, RenamedEventArgs e)
        {
            Console.WriteLine(System.Reflection.MethodInfo.GetCurrentMethod().Name + "()");
            Console.WriteLine("  " + e.ChangeType);
            Console.WriteLine("  " + e.FullPath);

            if (Path.Combine(((FileSystemWatcher)sender).Path, ((FileSystemWatcher)sender).Filter) == e.FullPath)
            {
                Load();
                Console.WriteLine("  reloaded!! : " + config.Name);
            }
        }

        #endregion
    }
}

XMLファイル生成クラス(動作確認用のおまけ)

using System.IO;
using System.Xml.Serialization;

namespace ReloadableConfiguration
{
    /// <summary>
    /// XMLファイルヘルパー
    /// </summary>
    public static class XmlFileHelper
    {
        /// <summary>
        /// XML文字列生成
        /// </summary>
        /// <param name="obj">オブジェクト</param>
        /// <param name="filepath">ファイルパス</param>
        /// <param name="enableException">例外送出有無</param>
        /// <returns>XML文字列</returns>
        public static void GenerateXmlFile(object obj, string filepath, bool enableException = false)
        {
            try
            {
                XmlSerializer serializer = new XmlSerializer(obj.GetType());
                using (StreamWriter writer = new StreamWriter(filepath))
                {
                    serializer.Serialize(writer, obj);
                }
            }
            catch
            {
                if (enableException)
                {
                    throw;
                }
            }
        }
    }
}

設定ファイル

「シンプルな設定値クラス」用

SampleFile/Sample1.xml
<SimpleConfig Name="sample 1">
  <Volume Value="10" Unit="kL" />
  <Items>
    <BoxItem BoxItemType="Fruit">
      <Name>Orange</Name>
      <Count>2</Count>
    </BoxItem>
    <BoxItem BoxItemType="Fish">
      <Name>Tuna</Name>
      <Count>4</Count>
    </BoxItem>
    <BoxItem BoxItemType="Undefined">
      <Count>0</Count>
    </BoxItem>
  </Items>
</SimpleConfig>
SampleFile/Sample2.xml
<SimpleConfig Name="sample 2">
  <Volume Value="10" Unit="kL" />
  <Items>
    <BoxItem BoxItemType="Fruit">
      <Name>Orange</Name>
      <Count>2</Count>
    </BoxItem>
    <BoxItem BoxItemType="Fish">
      <Name>Tuna</Name>
      <Count>4</Count>
    </BoxItem>
    <BoxItem BoxItemType="Undefined">
      <Count>0</Count>
    </BoxItem>
  </Items>
</SimpleConfig>

「再読み込みする設定値クラス」用

SampleFileReloadable/SampleReload1.xml
<ReloadableConfig Name="sample reloadable 1">
  <Volume Value="10" Unit="kL" />
  <Items>
    <BoxItem BoxItemType="Fruit">
      <Name>Orange</Name>
      <Count>3</Count>
    </BoxItem>
    <BoxItem BoxItemType="Fish">
      <Name>Tuna</Name>
      <Count>6</Count>
    </BoxItem>
    <BoxItem BoxItemType="Undefined">
      <Count>9</Count>
    </BoxItem>
  </Items>
</ReloadableConfig>
SampleFileReloadable/SampleReload2.xml
<ReloadableConfig Name="sample reloadable 2">
  <Volume Value="10" Unit="kL" />
  <Items>
    <BoxItem BoxItemType="Fruit">
      <Name>Orange</Name>
      <Count>2</Count>
    </BoxItem>
    <BoxItem BoxItemType="Fish">
      <Name>Tuna</Name>
      <Count>4</Count>
    </BoxItem>
    <BoxItem BoxItemType="Undefined">
      <Count>0</Count>
    </BoxItem>
  </Items>
</ReloadableConfig>
SampleFileReloadable/SampleDisabled1.xml
<ReloadableConfig Name="sample reloadable disabled 1">
  <EnableFileWatch>false</EnableFileWatch>
  <Volume Value="10" Unit="kL" />
  <Items>
    <BoxItem BoxItemType="Fruit">
      <Name>Orange</Name>
      <Count>2</Count>
    </BoxItem>
    <BoxItem BoxItemType="Fish">
      <Name>Tuna</Name>
      <Count>4</Count>
    </BoxItem>
    <BoxItem BoxItemType="Undefined">
      <Count>0</Count>
    </BoxItem>
  </Items>
</ReloadableConfig>
SampleFileReloadable/SampleReloadEnabled1.xml
<ReloadableConfig Name="sample reloadable enabled 1">
  <EnableFileWatch>true</EnableFileWatch>
  <Volume Value="10" Unit="kL" />
  <Items>
    <BoxItem BoxItemType="Fruit">
      <Name>Orange</Name>
      <Count>2</Count>
    </BoxItem>
    <BoxItem BoxItemType="Fish">
      <Name>Tuna</Name>
      <Count>4</Count>
    </BoxItem>
    <BoxItem BoxItemType="Undefined">
      <Count>0</Count>
    </BoxItem>
  </Items>
</ReloadableConfig>

実行結果

「シンプルな設定値クラス」分

================================
 (a-1) 初回アクセス時に読み込み
================================
Name : sample 1
--------------------------------
<?xml version="1.0" encoding="utf-16"?>
<SimpleConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Name="sample 1">
  <Volume Value="10" Unit="kL" />
  <Items>
    <BoxItem BoxItemType="Fruit">
      <Name>Orange</Name>
      <Count>2</Count>
    </BoxItem>
    <BoxItem BoxItemType="Fish">
      <Name>Tuna</Name>
      <Count>4</Count>
    </BoxItem>
    <BoxItem BoxItemType="Undefined">
      <Count>0</Count>
    </BoxItem>
  </Items>
</SimpleConfig>

================================
 (a-2) 明示的読み込み
================================
読込成功
Name : sample 2

================================
 (a-3) 明示的読み込み(失敗)
================================
読込失敗
Name : sample 2

「再読み込みする設定値クラス」分

================================
 (b-1) 初回アクセス時に読み込み
================================
Name : sample reloadable 1
--------------------------------
<?xml version="1.0" encoding="utf-16"?>
<ReloadableConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Name="sample reloadable 1">
  <EnableFileWatch xsi:nil="true" />
  <Volume Value="10" Unit="kL" />
  <Items>
    <BoxItem BoxItemType="Fruit">
      <Name>Orange</Name>
      <Count>3</Count>
    </BoxItem>
    <BoxItem BoxItemType="Fish">
      <Name>Tuna</Name>
      <Count>6</Count>
    </BoxItem>
    <BoxItem BoxItemType="Undefined">
      <Count>9</Count>
    </BoxItem>
  </Items>
</ReloadableConfig>

================================
 (b-2) 明示的読み込み
================================
読込成功
Name : sample reloadable 2

================================
 (b-3) 明示的読み込み(失敗)
================================
読込失敗
Name : sample reloadable 2

================================
 (b-4) 生成の検知
================================
読込成功
Name            : sample reloadable enabled 1
EnableFileWatch : True
Watcher_Created()
  Created
  SampleFileReloadable\SampleReloadEnabled1.xml
  reloaded!! : sample reloadable enabled - created

================================
 (b-5) 更新の検知
================================
Watcher_Changed()
  Changed
  SampleFileReloadable\SampleReloadEnabled1.xml
  reloaded!! : sample reloadable enabled - changed

================================
 (b-6) 名前変更の検知
================================
Watcher_Renamed()
  Renamed
  SampleFileReloadable\SampleReloadEnabled1.xml.AfterRename
Watcher_Renamed()
  Renamed
  SampleFileReloadable\SampleReloadEnabled1.xml
  reloaded!! : sample reloadable enabled - renamed

================================
 (b-7) ファイル監視状態の変更
================================

end.

所感

軽い気持ちで作り始めたけど結構かかってしまった。それなりの量なので動作確認をちゃんとテストコードに起こした方がいい。けどもう眠いので諦める。。説明つける気力もない。。反省点としては「設定値」と「ファイル監視」をもっと疎にすればよかった…かな?大して変わらないかも。

FileSystemWatcherについて
  • Filterから外れるリネームの場合でもRenamedイベントが発生する。意外だった。Renamedイベントハンドラは「変更後」がFilterと合う場合のみ再読み込みするようにした。
  • 書き込み側アプリの挙動次第で発生するイベントが変わる面がある(というかいつイベントが発生するか厳密に理解していない)ので、Renamedイベントハンドラ以外もとりあえずガード入れた。