(C#) 変更を検知して再読み込みする設定ファイルクラス
概要
目標は以下のとおり。とりあえず狙った感じに動いた。
- 設定ファイル用クラスをつくる
- ファイルが変更されたことを検知して再読み込みする
- 再読み込みに失敗したら前回値を保持する
- 再読み込みするかどうか自体も設定値とする
環境
.NET Framework | 4.7.2 |
C# | 4.7 |
OS | Windows 10 1903 |
ソースコード
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.
所感
軽い気持ちで作り始めたけど結構かかってしまった。それなりの量なので動作確認をちゃんとテストコードに起こした方がいい。けどもう眠いので諦める。。説明つける気力もない。。反省点としては「設定値」と「ファイル監視」をもっと疎にすればよかった…かな?大して変わらないかも。