読者です 読者をやめる 読者になる 読者になる

銀の弾丸

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

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

C# VisualStudio Windows

ここではアプリケーションの設定を編集可能なXMLファイルとして利用するための基本クラスとサンプルをご紹介。

.NETのプロジェクトには「アプリケーション構成ファイル」があって、設定情報(構成情報?)を書いておけるような感じですが、 インストーラーでセットアップすると、読み取り専用のアプリケーションフォルダに入ってしまい、編集するには管理者権限が必要になります。 つまり運用中の書き換えは考慮されていないってことですね。

f:id:takamints:20170221171853p:plain

基礎からわかる C#
基礎からわかる C#
posted with amazlet at 17.02.21
シーアンドアール研究所 (2017-02-16)
売り上げランキング: 16,889

■■■ 目次 ■■■

  1. はじめに - 機能仕様
  2. XML設定クラス定義例:SampleSettingsクラス
  3. 設定ファイルの読み込み処理
  4. XMLファイルの例:SampleSettings.xml
  5. XML設定ファイルの基本クラス:XmlSettingFileクラス
  6. 設定ファイルはWindowsの特殊フォルダ以下へ
  7. 参考サイト

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. 参考サイト

OpenCVの画像処理をお手軽に ― OpenCVフィルター処理ライブラリ cvImagePipeline のご紹介

OpenCV 画像処理 C / C++

OpenCVの画像処理をお手軽にできるC++のフィルター処理ライブラリ cvImagePipeline のご紹介。

cvImagePipeline は、OpenCVの画像処理関数をモジュール化したC++向けのライブラリです。

画像処理の流れや各モジュールのパラメータの指定をXMLで記述できるようにしています(コードからも可能です)。

このため、画像処理の処理順の変更やパラメーター調整作業がはかどります。

背景除去のサンプルプログラム

以下の画像は、ライブラリに含まれているサンプルプログラムの実行中画面です(必要以上にテカっているのはワタシの額w)。

f:id:takamints:20161228110352p:plain

プログラミング言語C++ 第4版
SBクリエイティブ (2016-02-04)
売り上げランキング: 10,392

単純な背景除去の実装実験を行っている画面です。 入力画像や前処理中の画像などを、6分割した画面に統合して表示しています。 (カメラ入力や画面の分割、Window表示もそれぞれ、このライブラリのフィルターです。)

左上から順に、

  1. キャプチャ画像(の鏡像)
  2. 1をグレイスケールに変換
  3. 2に対して、ヒストグラム均一化とガウシアンブラーを適用
  4. 3の過去18000フレーム分を平均
  5. 3と4の差分(絶対値差分)
  6. 5の二値化

このあと、6の二値化画像に対して、膨張(dilate)と縮小(erode)を適当に繰り返してから元画像(1の画像)をマスクすれば背景と前景に分離できそうです。

処理は全てsample.xml(↓)に記述しており、参考として下に掲載しているC++のコード(capture.cpp)で読み込んで処理させています。 このためビルド無しで処理を変更可能です。(XMLを読み込む処理は複数のフィルターをまとめるImgProcSetの機能です)

sample.xml - 背景除去するXML

<cvImagePipeline name="testProcessor">
  <Processor class="VideoCapture" name="cap">
    <Property name="deviceIndex" value="0"/>
  </Processor>
  <Processor class="Flipper" name="fripHoriz">
    <Property name="flipDir" value="1"/>
  </Processor>
  <Processor class="ImagePoint" name="raw"/>
  <Processor class="ColorConverter" name="grayscale"/>
  <Processor class="EqualizeHist" name="equalizeHist"/>
  <Processor class="GaussianBlur" name="blur"/>
  <Processor class="DepthTo32F" name="depth32F"/>
  <Processor class="ImagePoint" name="pp"/>
  <Processor class="RunningAvg" name="background"> 
    <Property name="averageCount" value="18000"/>
  </Processor>
  <Processor class="AbsDiff" name="diff" autoBind="false">
    <Input to="src1" from="pp"/>
    <Input to="src2" from="background"/>
  </Processor>
  <Processor class="Convert" name="to8UC">
    <Property name="rtype" value="0"/>
    <Property name="alpha" value="255"/>
    <Property name="beta" value="0"/>
  </Processor>
  <Processor class="Threshold" name="binary">
    <Property name="type" value="CV_THRESH_BINARY"/>
        <!--
   CV_THRESH_BINARY
   CV_THRESH_BINARY_INV
   CV_THRESH_TRUNC
   CV_THRESH_TOZERO
   CV_THRESH_TOZERO_INV
   -->
    <Property name="otsu" value="1"/>
    <Property name="thresh" value="50"/>
    <Property name="maxval" value="255"/>
  </Processor>

  <Processor class="FitInGrid" name="integratedImage" autoBind="false">
    <Property name="width" value="960"/>
    <Property name="height" value="480"/>
    <Property name="cols" value="3"/>
    <Property name="rows" value="2"/>
    <Property name="interleave" value="0"/>
    <Input to="0" from="raw"/>
    <Input to="1" from="grayscale"/>
    <Input to="2" from="blur"/>
    <Input to="3" from="background"/>
    <Input to="4" from="diff"/>
    <Input to="5" from="binary"/>
  </Processor>
  <Processor class="ImageWindow" name="window">
    <Property name="windowName" value="cvImagePipeline"/>
    <Property name="showFPS" value="1"/>
  </Processor>
</cvImagePipeline>

capture.cpp - XMLを処理するプログラム

#include "stdafx.h"
#if defined(_MSC_VER)
#include <windows.h>
#else
#include <unistd.h>
#define Sleep(millisec) usleep(millisec * 1000)
#endif
#include <opencv2/opencv.hpp>
#include "cvImagePipeline.h"

using namespace cvImagePipeline;
using namespace cvImagePipeline::Filter;

#if defined(_MSC_VER)
int _tmain(int argc, _TCHAR* argv[])
#else
int main(int argc, char* argv[])
#endif
{
    cvInitSystem(argc, argv);
    ImgProcSet processors;
    std::string xml_filename("sample.xml");
    if (argc > 1) {
        xml_filename = argv[1];
    }
    if (!processors.loadXml(xml_filename)) {
        std::cerr << "ファイル読み込み失敗 ファイル名:"
            << xml_filename << std::endl;
        return -1;
    }
    Sleep(2000);
    while(true) {
        processors.execute();
        int ch = cvWaitKeyEx(1);
        if (ch == '\x1b') {
            fprintf(stderr, "exit.\n");
            break;
        }
    }
    cvDestroyAllWindows();
    return 0;
}

リポジトリ

上の例も含めて、その他詳細は下記リポジトリのREADMEを参照してください。

実を言うと、このリポジトリ、1年以上放置しているんですねえ。 しかし今でも Clone してくださる方が月に数人いらっしゃるようです。 READMEが古くなっていて申し訳ないですから、これを機にキチンとメンテしようと思ってはいますが・・・。

github.com

そういや以下の記事でもシレッと使っていますねw

takamints.hatenablog.jp

takamints.hatenablog.jp

今後の課題

今考えつく今後の課題は以下の様なことです。放置している場合ではないなあ。

  • OpenCV 3.0以上での動作確認。
  • Windows 10 での動作確認。
  • 基本フィルタと他のサンプルフィルタの分離。
  • 追加実装のしやすさを追求。
  • XMLXAML的文法に変更(記述量を少なくできそう)。
  • XMLのビジュアルな編集。
  • デバッグ機能の充実。
  • 実行中のフィルタのバイパスやパラメータの変更機能。
  • Pythonから利用できるインタフェース。C++はやっぱり敷居が高いかも。
  • 妙に凝った変な名称のフィルタを改名するw

まだ間に合うXAMLの基本

WPF XAML

XAMLはなんだか複雑だ」と思ってました。

しかし、あることに気が付いてから「なかなかシンプルなんじゃない?」と思えるようになりました。

ちゃんと知っている方にとっては、当たり前のことかと思いますが、その「ちょっとしたこと」を出発点に、1段掘り下げて調べた結果を書いておきます。

f:id:takamints:20161026121034p:plain
photo credit: Doug Kline Star Wars Celebration IV - X-Wing fighter (back) via photopin (license)

目次

  1. XAML要素でインスタンスが生成される
  2. 属性でパブリックプロパティを設定する
  3. プロパティへクラスオブジェクトを代入する
  4. 静的クラスのプロパティへオブジェクトを代入する
  5. そもそも子要素は何処に格納されるのか

1. XAML要素でインスタンスが生成される

XAMLの要素名はクラス名で、実行時には、そのクラスのデフォルトコンストラクタでインスタンスが生成される

クラス名であることは知っていましたが、デフォルトコンストラクタで生成されるという認識がありませんでした。 だから実行時のオブジェクトの状態を正確に把握できていなかったんですわ。

MainWindow.xamlMVVM的に真っ当にMessageBoxを表示する - 銀の弾丸より)

<Window x:Class="StudyDotNet.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:StudyDotNet"
        xmlns:vm="clr-namespace:StudyDotNet.ViewModels"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
        xmlns:tr="clr-namespace:StudyDotNet.Triggers"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <i:Interaction.Triggers>
        <tr:MessageBoxTrigger>
            <tr:MessageBoxAction />
        </tr:MessageBoxTrigger>
    </i:Interaction.Triggers>

    <Grid>
        <Button x:Name="button" Content="Button"
                Command="{Binding SampleCommand}"
                HorizontalAlignment="Left" Margin="50,50,0,0"
                VerticalAlignment="Top" Width="75"/>
    </Grid>
</Window>
takamints.hatenablog.jp

上のXAMLでは、次のクラスオブジェクトが生成されています。 (ドットを含む要素名と、子要素の所在については後述します)

  • MainWindowViewModel
  • MessageBoxTrigger
  • MessageBoxAction
  • Grid
  • Button

確かにビューモデルやボタンなどは、実行時に生成されていて、コードビハインドからアクセスできますよね。 でも、デフォルトコンストラクタとの関連は想像していなかったんです。 このような、標準の部品に関しては、.NETフレームワーク内で特別なことが行われているのだろう・・・などと思ってました。

でも、自作クラスも生成されてるってことは・・・という段になって、やっと気付いた。

思った以上にシンプルですね。そもそもフレームワークをそんなガチガチに作るわけ無いですし。

2. 属性でパブリックプロパティを設定する

要素の属性はそのクラスオブジェクトのパブリックプロパティへの代入です。 デフォルトコンストラクタで生成されたのち、プロパティが設定されます。

属性値として直接記述できるのは、文字列とか数値といったプリミティブな型に制限されると思います。 バインディングの解決は、少し複雑なことがなされているのかもしれませんね。

3. プロパティへクラスオブジェクトを代入する

ところで、XAMLでデータコンテキストを生成しているところ(以下)。

<Window x:Class="StudyDotNet.MainWindow" ~中略~ >
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    ・
    ・
    ・
</Window>

この Window.DataContext要素のように、親エレメント.プロパティ名という要素は、親要素のインスタンスプロパティへの代入なんですね。

つまりここでは、MainWindowクラスの基本クラスであるWindowDataContextプロパティへ、MainWindowViewModelインスタンスを設定(Property Set)しているということです。

4. 静的クラスのプロパティへオブジェクトを代入する

以下のInteraction.Triggersも似ていますが、ドットの前がInteractionとなっていて、親要素ではありません。 これは、System.Windows.Interactivity.Interactionというスタティッククラスで、そのTriggersというプロパティにMessageBoxTriggerを追加しています。

<Window x:Class="StudyDotNet.MainWindow" ~中略~ >
    ・
    ・
    ・
    <i:Interaction.Triggers>
        <tr:MessageBoxTrigger>
            <tr:MessageBoxAction />
        </tr:MessageBoxTrigger>
    </i:Interaction.Triggers>
    ・
    ・
    ・
</Window>

ところが InteractionクラスにそのものスバリのTriggersというプロパティはありません。 かわりにTriggerCollection GetTriggers(DependencyObject obj)というメソッドがありますので、XAMLのパーサーがうまくやってくれているのだと思います(詳細不明)。

以下のように、コードビハインドからthisを与えて返されるコレクションに、ちゃんとMessageBoxTriggerインスタンスが入っていました。

using System;
using System.Windows;
using System.Windows.Interactivity;

namespace StudyDotNet
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            var triggers = Interaction.GetTriggers(this);
            foreach(var trigger in triggers)
            {
                Console.WriteLine(trigger.GetType().Name);
            }
        }
    }
}

ちなみにInitializeComponentの前では何も表示されなかったので、XAMLInitializeComponentで処理されているのでしょう。

5. そもそも子要素は何処に格納されるのか

XAMLのパーサーは、子要素のインスタンスを親要素の何処に格納するの?という疑問。 コレクションであることは間違いなさそうなのですが。

XAMLの以下の部分などですね。

<i:Interaction.Triggers>
    <tr:MessageBoxTrigger>
        <tr:MessageBoxAction />
    </tr:MessageBoxTrigger>
</i:Interaction.Triggers>

XAMLのパーサーが、MessageBoxTriggerのインスタンスに、MessageBoxActionのインスタンスを保持させている」ということは明らか。

実際にアクションはイベントトリガーのActionsというコレクションプロパティに保持されており、 このプロパティは、MessageBoxTriggerの基本クラスであるTriggerBaseに実装されています。 でも、XAMLだけを見る限りは、そんな事実はわからない。

ということで、System.Windows.Interactivity.TriggerBaseの説明を見てみると、以下のようになっています。

[ContentPropertyAttribute("Actions")] 
public abstract class TriggerBase : DependencyObject, IAttachedObject

なるほど、ContentPropertyAttributeで「XMLのコンテントはプロパティ名 Actions に」と読めますね。 XAMLのパーサーはこの宣言に従って子要素を親要素に格納している。

ということで、Interaction.Triggersの中身を、以下のように冗長に書き換えても同じように動作します。

<i:Interaction.Triggers>
    <tr:MessageBoxTrigger>
        <tr:MessageBoxTrigger.Actions>
            <tr:MessageBoxAction />
        </tr:MessageBoxTrigger.Actions>
    </tr:MessageBoxTrigger>
</i:Interaction.Triggers>

まとめと所感

このように、曖昧に済ませていたことを深掘りしてみて、かなりスッキリ気分爽快。 XAMLで何でもできる気がしてきました。

基本を押さえるのはホントに大切。 知識やスキルを習得するときの効率性に直結しますね。

いくつになってもお勉強です。


MVVM的に真っ当にMessageBoxを表示する

WPF MVVM VisualStudio C#

WPFでのMVVMパターンとしては、ビュー以外からメッセージボックスを直接表示するのは良くないらしい。

メッセージボックスで親ウィンドウを指定しないとモーダルにならないのですが「ビュー以外からメインウィンドウを逆方向へ参照するのは気持ち悪い」と・・・。

「それ、潔癖すぎないですか?」とも思うけど、勉強中の身ですし、最初から基本を崩すのもよろしくない。

じゃあ、どうすんの?ってーと「メッセンジャーパターン」を使いなさいとのことでした。

f:id:takamints:20161023233634p:plain
photo credit: waterj2 DSCF0629 via photopin (license)

メッセンジャーパターン」?

メッセンジャーパターンは「VMからViewを操作する方法」のことらしい。以下のようなカラクリだと理解しまして、

  1. メッセンジャー」というオブジェクトからイベントを発行して、
  2. あらかじめビューに実装されたイベントトリガーが、このイベントを受けて、
  3. イベントトリガーが保有するトリガーアクションが処理をする(イベントの引数も渡される)。

これを、自分なりに整理して、ごちゃごちゃしたこと抜きにして、System.Windows.MessageBox と同じように使えるコードをご紹介。 GitHub Gistにも置いていますのでご利用ください。

コードの概要

  1. メッセンジャーとイベントトリガー(これが本体)
  2. トリガーアクションの実装例
  3. メインウィンドウ(MainWindow.xaml)の記述例
  4. メッセージボックスを表示する

ここで、

  1. メッセンジャーは単なるクラス。
  2. イベントトリガーは、System.Windows.Interactibity.EventTriggerの派生クラス。
  3. トリガーアクションは、System.Windows.Interactibity.TriggerActionの派生クラス。

※ System.Windows.Interactibityは、プロジェクトから参照されていないかもしれません。 参照マネージャの左のツリーから[アセンブリ]/[拡張]を開いてチェックを付ければOKです。

1.メッセンジャーとイベントトリガー(これが本体)

内部クラスを含めて3つのクラスを定義しています。System.Windows.MessageBoxと同じように使えるようにしたら長くなりました。

  1. MessageBox - メッセンジャー。スタティックなShowメソッドでシングルトンのインスタンスからイベントを発行。
  2. MessageBox.Action トリガーアクションの抽象基底クラス。実際にメッセージを表示するクラスの基本クラスです。
  3. MessageBoxTrigger - イベントトリガークラス。MessageBoxからのイベントを受け取って、トリガーアクションを実行します。

MessageBoxTrigger.cs

using System;
using System.Windows;
using System.Windows.Interactivity;

namespace StudyDotNet.Triggers
{
    /// <summary>
    /// MVVM的メッセージボックスを表示するためのメッセンジャー。
    ///
    /// MessageBox.Showメソッドで、イベントトリガーを起動する。
    /// 
    /// 実際の表示は、イベントトリガーから実行される
    /// トリガーアクションに実装される。
    /// </summary>
    public class MessageBox
    {
        /// <summary>
        /// MessageBoxTriggerを起動するイベント。
        /// </summary>
        public event EventHandler<EventArgs> ShowMessageBox;

        /// <summary>
        /// MessageBoxTriggerを起動するイベントの名前。
        /// </summary>
        public static string EventName
        { get { return "ShowMessageBox"; } }

        /// <summary>
        /// このクラスはシングルトン。
        /// </summary>
        public static MessageBox Instance
        { get; private set; } = new MessageBox();
        private MessageBox() { }

        /// <summary>
        /// MessageBoxTriggerを起動するイベントの引数。
        /// トリガーアクションへ渡されて処理される。
        /// </summary>
        public class EventArgs : System.EventArgs
        {
            public string Text { get; set; }
            public string Title { get; set; }
            public MessageBoxButton Button { get; set; }
            public MessageBoxImage Icon { get; set; }

            /// <summary>
            /// メッセージボックスの結果を受け取るコールバック
            /// </summary>
            public Action<MessageBoxResult> NotifyResult
            { get; set; }
        }

        /// <summary>
        /// MVVM的メッセージボックスを表示。
        /// 実際にはイベントを発行してイベントトリガーを起動する。
        /// </summary>
        /// <param name="messageBoxText"></param>
        /// <param name="title"></param>
        /// <param name="button"></param>
        /// <param name="icon"></param>
        /// <returns></returns>
        public static MessageBoxResult Show(
            string messageBoxText,
            string title = null,
            MessageBoxButton button = MessageBoxButton.OK,
            MessageBoxImage icon = MessageBoxImage.Information)
        {
            //メッセージボックスの結果
            MessageBoxResult messageBoxResult = MessageBoxResult.Cancel;

            //イベントを発行する
            Instance.ShowMessageBox?.Invoke(
                Instance,
                new MessageBox.EventArgs()
                {
                    Text = messageBoxText,
                    Title = title,
                    Button = button,
                    Icon = icon,

                    //コールバックで結果を受け取る
                    NotifyResult = result =>
                    {
                        messageBoxResult = result;
                    }
                });

            //メッセージボックスの結果を返す
            return messageBoxResult;
        }

        /// <summary>
        /// メッセージを表示するトリガーアクション実装用の抽象基底クラス。
        /// 
        /// 派生クラスでShowMessageを実装する。 
        /// </summary>
        public abstract class Action : TriggerAction<DependencyObject>
        {
            /// <summary>
            /// アクションの実態
            /// </summary>
            /// <param name="parameter"></param>
            protected override void Invoke(object parameter)
            {
                //イベント引数の種別を検査
                var messageBoxArgs = parameter as MessageBox.EventArgs;
                if(messageBoxArgs == null)
                {
                    return;
                }

                //メッセージボックスの表示結果を取得
                MessageBoxResult result = ShowMessage(
                        messageBoxArgs.Text,
                        messageBoxArgs.Title,
                        messageBoxArgs.Button,
                        messageBoxArgs.Icon);

                //コールバックで結果を通知
                messageBoxArgs.NotifyResult?.Invoke(result);
            }

            /// <summary>
            /// メッセージボックスを表示する抽象メソッド。
            /// </summary>
            /// <param name="text"></param>
            /// <param name="title"></param>
            /// <param name="button"></param>
            /// <param name="icon"></param>
            /// <returns></returns>
            abstract protected MessageBoxResult
            ShowMessage(
                string text, string title,
                MessageBoxButton button,
                MessageBoxImage icon);
        }
    }

    /// <summary>
    /// MVVM的メッセージボックスを表示するためのイベントトリガー。
    ///
    /// MessageBox メッセンジャーの発行するイベントで起動される。
    /// </summary>
    public class MessageBoxTrigger : System.Windows.Interactivity.EventTrigger
    {
        public MessageBoxTrigger()
            : base(MessageBox.EventName)
        {
            SourceObject = MessageBox.Instance;
        }
    }
}

2.トリガーアクションの実装例

MesssageBoxのトリガーから実行されるトリガーアクションクラスの実装例です。

MessageBox.Actionから派生した、MessageBoxActionクラスを実装しています。 メッセージボックスを表示して、その結果を返すクラスです。

このクラスから使っている MessageBox クラスは、System.Windows のものということに注意してください。

MessageBoxAction.cs

using System.Windows;

namespace StudyDotNet.Triggers
{
    /// <summary>
    /// メッセージボックスを表示するトリガーアクション
    /// </summary>
    public class MessageBoxAction : MessageBox.Action
    {
        protected override MessageBoxResult ShowMessage(
            string text,
            string title,
            MessageBoxButton button,
            MessageBoxImage icon)
        {
            return System.Windows.MessageBox.Show(
                Window.GetWindow(AssociatedObject), text,
                (title != null ? title :
                    Application.Current.MainWindow.Title),
                button, icon);
        }
    }
}

3.メインウィンドウ(MainWindow.xaml)の記述例

上のメッセージトリガーとトリガーアクションクラスは以下のように、Xamlに Interaction.Trigger 要素を追加します。

XML名前空間xmlns:ixmlns:tr の宣言にも注意。iは、System.Windows.Interactibityを使用するためで、 trは、上記のトリガー/アクションを使用するためのものです。

<Window x:Class="StudyDotNet.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:StudyDotNet"
        xmlns:vm="clr-namespace:StudyDotNet.ViewModels"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
        xmlns:tr="clr-namespace:StudyDotNet.Triggers"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <!-- ここから -->
    <i:Interaction.Triggers>
        <tr:MessageBoxTrigger>
            <tr:MessageBoxAction />
        </tr:MessageBoxTrigger>
    </i:Interaction.Triggers>
    <!-- ここまで -->

    <Grid>
        <Button x:Name="button" Content="Button"
                Command="{Binding SampleCommand}"
                HorizontalAlignment="Left" Margin="50,50,0,0"
                VerticalAlignment="Top" Width="75"/>
    </Grid>
</Window>

4.メッセージボックスを表示する

使う側からは単純に、MessageBox.Showを呼び出すだけです。 System.Windows.MessageBoxとゴッチャにならないように注意は必要。

using System;
using System.Windows.Input;
using StudyDotNet.Triggers;

namespace StudyDotNet.Commands
{
    public class SampleCommand : ICommand
    {
        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        { return true; }

        public void Execute(object parameter)
        {
            MessageBox.Show("MVVMバンザイ!");
        }
    }
}

いくつになってもお勉強

これによって、より少ない変更で、従来のメッセージボックス使用部分をメッセンジャーパターンに移行できます。

ユニットテストは、固定の応答を返すスタブで対応。

また、別の表示方法に切り替えるのも容易です。

例えば独自のウィンドウで表示したり、 ポップアップせずメインウィンドウ内にメッセージを配置したり、 履歴を参照する機能を追加したりと、広がりがあります。

ということで、やはりビュー以外からは、表示内容やUIに直接の関わりを持たないほうが良いのでしょう。

いくつになってもお勉強です。

WPFのMVVMでコマンドをバインディングする利点

C# VisualStudio WPF MVVM

MVVMパターンでは、ボタンを押した時の処理などは、コマンドにバインディングいたしましょうということらしく、 従来の「Clickイベントをコードビハインドで受けて、、、」というのは嫌われるらしい。一部の人からはモーレツに。

ただ、理由なく「MVVMでなきゃダメ!」的一辺倒では、いつまで経っても反抗期なおじさんとして納得しがたいところもあったわけ。

とはいえ、いがみ合っても誰得?ですから、この際、きちんと理解しようと頑張りました。いくつになってもお勉強です。

▼あわせてどうぞ ― WPF・MVVM関連記事▼

takamints.hatenablog.jp

f:id:takamints:20160924175447p:plain
photo credit: Storm Trooper at Oxford via photopin (license)

WHY JAPANESE PEOPLE!? [DVD]
WHY JAPANESE PEOPLE!? [DVD]
posted with amazlet at 16.09.24
アニプレックス (2015-06-24)
売り上げランキング: 10,600
C#実践開発手法 (マイクロソフト公式解説書)
Gary McLean Hall
日経BP
売り上げランキング: 152,501

コマンドの実装例

具体的に、コマンドを利用するには

  1. 実行したい処理を、ICommandインターフェースを実装したコマンドクラスに記述して、
  2. ビューモデルから、そのインスタンスをプロパティで公開して、
  3. ボタンコントロールのCommand属性からバインディングする

と、これで幸せになれるらしい。

以降、以下の順に実装例を掲載してます。

  1. コマンドクラス:SampleCommand.cs
  2. ビューモデル MainWindowViewModel.cs
  3. ビュー:MainWindow.xaml
  4. ビューのコードビハインド:MainWindow.xaml.cs(おまけ)

コマンド:SampleCommand.cs

押してから5秒間、押せなくなるボタンです。 やってることは無意味ですが、 最低限ICommandインターフェースの全機能を使おうとして長くなってしまいました。

using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Input;

namespace StudyDotNet.Commands
{
    public class SampleCommand : ICommand
    {
        /// <summary>
        ///忙しいフラグ。忙しい時は何もできません。
        /// </summary>
        private bool _isBusy = false;
        
        /// <summary>
        /// 忙しいフラグのプロパティ。
        /// コマンドが実行可能かどうかに関連するプロパティなので。
        /// 代入されたらCanExecuteChangedイベントを投げる。
        /// </summary>
        public bool IsBusy
        {
            get { return _isBusy; }
            set
            {
                _isBusy = value;
                CanExecuteChanged?.Invoke(this, new EventArgs());
            }
        }

        /// <summary>
        /// 以下でタイマー使っているので、
        /// UIスレッドで画面更新するために必要
        /// </summary>
        private AsyncOperation _uiThreadOperation =
            AsyncOperationManager.CreateOperation(null);
        //
        // 以降 ICommand インターフェースの実装
        //

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        {
            //忙しくない時だけコマンド実行できる
            Console.WriteLine("実行可否を調べられてる。"
                + (!IsBusy?"お仕事できます":"今無理です"));
            return !IsBusy;
        }

        public void Execute(object parameter)
        {
            //忙しいフラグON
            //一定時間後には暇になる。
            Console.WriteLine("忙しくなるぞー");
            IsBusy = true;
            Timer _busyTimer = new Timer( timerParam => {
                _uiThreadOperation.Post(updatePropParam => {
                    IsBusy = false;
                    Console.WriteLine("暇になった。");
                }, null);
            }, null, 5000, Timeout.Infinite);
        }
    }
}
  • CanExecuteChangedイベント - コマンドを実行可否の状態変化時に発行するイベント。UIオブジェクトではこのイベントによって、画面表示状態を変更する。なので別スレッドから投げるときは、AsyncOperationでUIスレッドへActionをPostしなくてはなりませんね。
  • CanExecuteプロパティ - コマンドが実行可能な状況ではtrue、実行できない状況ならfalseを返すプロパティです。
  • Executeメソッド - コマンドの処理本体です。

ビューモデル MainWindowViewModel.cs

ビューモデルは非常にシンプル。上のコマンドクラスのインスタンスをパブリックプロパティとして持ってるだけ。 「この画面にはモデルを操作するための、こういう名前のコマンドがありますよ」と。そして「何をするかはコマンドを見て頂戴」というところでしょうか。

using StudyDotNet.Commands;

namespace StudyDotNet.ViewModels
{
    class MainWindowViewModel
    {
        public SampleCommand SampleCommand
        { get; private set; } = new SampleCommand();
    }
}

このプロパティのsetterからは、INotifyPropertyChangedインターフェースを実装して、PropertyChangedイベントを発行すべきだと思い込んでいましたが、なくても動いているようです(ここ、ちょっと理解が曖昧)。

ビュー:MainWindow.xaml

ビューからは ButtonのCommand属性からビューモデルのコマンドにバインディングButtonCommand="{Binding SampleCommand}"の部分ですね。

<Window x:Class="StudyDotNet.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:StudyDotNet"
        xmlns:vm="clr-namespace:StudyDotNet.ViewModels"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <Button x:Name="button" Content="Button"
                Command="{Binding SampleCommand}"
                HorizontalAlignment="Left" Margin="50,50,0,0"
                VerticalAlignment="Top" Width="75"/>
    </Grid>
</Window>

ビューのコードビハインド:MainWindow.xaml.cs(おまけ)

ちなみにMainWindowのコードビハインドは、何も触っていません。

using System.Windows;

namespace StudyDotNet
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

コマンドを使う利点

処理をコマンドクラスに記述することで得られる利点は、以下のようなものかと推測します。他にもたくさんありそな気もする。

  1. コードビハインド(*.xaml.cs)に何も書く必要がない。
  2. コマンドの単体テストで、ビューやビューモデルが不要です。
  3. ビューモデルがシンプルに保てます。例えば、コマンドの実行可否がコマンド自体から得られて(CanExecuteプロパティ)、UIオブジェクトに直結しているので、別途IsEnabledプロパティでバインディングしなくても良い。複数の条件が絡み合ったUIオブジェクトが複数ある場合、ビューモデルにIs〇〇ButtonEnabledみたいなプロパティがゴロゴロ沸いてきて煩雑になりやすいですからね。

まあ、一言でいえば、コマンドの独立性が高く保たれますということで、疎結合度合が半端ないです。

いくつになってもお勉強

とまあ、偉そうに説明してますが・・・先に書いたように、先日まとめて理解した知識です。

設計思想的には古くからあるDocument-ViewやMVCなどを最新技術で発展させたもののように思いました。古くはVC++MFCアプリケーションでメニュー項目などを更新するUpdateUIなどと同じ便利さ加減かなと。当時あの機構をまともに理解している人が少なくて、往生した覚えがありますわって話が古い。古すぎる。いやしかし自分もMVVMに関して同じような状況でしたね。

なにより、「どんな利点があるのか」という観点から、腰を据えて理解できて非常によかった。ふたを開ければかつて知ったる設計思想・・・的な気分でもあり。

まだ知り始め、まだまだ便利なことがあるのかもしれないですね。 いくつになってもお勉強です。


関連記事:

takamints.hatenablog.jp