Hatena::ブログ(Diary)

koyakの日記 このページをアンテナに追加 RSSフィード

2012-09-02

コードビハインドを使わずにWPF画面を作ろうとしたときに最初にぶち当たりそうな壁(Windowの操作)

| 12:38

 久方ぶりにSeasar.NETのリリース情報以外の更新です。

 ネタは枯れきっているわけでもなく最新の技術ってわけでもないWPF

 微妙なネタでごめんなさい。


 現在、ちまちまとWPFを個人的にさわっております。

 基本方針は「WindowクラスにVisualStudioが自動生成する以外のコードを書かない」。

 (MVVMやデザインとコードの分離、といった真面目な議論は割愛させていただきます)


 環境は.NET Framework4.0。ライブラリは下記を使用しています。さすがにこれが無いと色々と面倒そうです。

 Microsoft Expression Blend 4 Software Development Kit (SDK) for .NET 4

 このライブラリの使い方については検索すると色々引っかかるため割愛させていただきます。


 以下、WPF画面(System.Windows.Windowクラスを継承した画面クラス)のことを『Window』と呼ばせていただきます。


 上記方針でぶち当たった壁が「Windowをどうやって操作するか?」です。

 『EventTrigger』『TriggerAction』『Command』などを組み合わせればイベントを捕まえるのは簡単なのですが、画面を閉じる、などのWindow操作を行おうとしたときに「……で、Windowオブジェクトはどう取得すればいいの?」となります。コードビハインドを使う場合はWindowクラスの中にイベント処理を書くので、出くわすことはない壁です。


================================================================================================================

2012.09.06追記 後述のように面倒な処理を書かなくても簡単にWindow操作を行える方法を教えていただきました!

いやはやお恥ずかしい。。。 Window.GetWindowなるメソッドを使えばOKです。例えば「画面を閉じる」処理を書きたい場合はこんな感じで。

/// <summary>Windowを「閉じる」アクションクラス</summary>
public class CloseWindowAction : TriggerAction<FrameworkElement> {
    protected override void Invoke(object parameter) {
            Window.GetWindow(AssociatedObject).Close();
        }
    }
}

 自分への戒めのために、元の文章も残しておきますorz ↓↓↓

================================================================================================================


 上記問題を解決する一案としてこんな実装を考えてみました。


 まずは画面のxamlコード。

<Window x:Class="VSArrange.Config.ConfigWindow"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" xmlns:v="clr-namespace:VSArrange.Config.ViewModel" xmlns:a="clr-namespace:AddInCommon.Presentation.TriggerAction;assembly=AddInCommon" xmlns:request="clr-namespace:AddInCommon.Presentation.InteractionRequest;assembly=AddInCommon"> <Window.DataContext> <v:ConfigWindowViewModel/> </Window.DataContext> <i:Interaction.Triggers> <request:RequestTrigger SourceObject="{Binding Path=CloseWindowRequest, Mode=OneTime}"> <a:CloseWindowAction /> </request:RequestTrigger> </i:Interaction.Triggers> <StackPanel> <Button Content="閉じる" Command="{Binding Path=CloseWindowCommand, Mode=OneTime}"/> </StackPanel> </Window>

 「閉じる」ボタンを押すと画面を閉じる。それだけの画面です。

 (Windowの名前が『ConfigWindow』となっているのは自分が作っている某アドインのコードをそのまま持ってきているためです。どうかご了承&読み替えていただけたらと思います)


 上記画面で『閉じる』ボタンを押下するとバインドされた『CloseWindowCommand』が呼ばれます。

『CloseWindowCommand』はViewModel側で定義します。

namespace VSArrange.Config.ViewModel {
    /// <summary>設定画面ViewModel</summary>
    public class ConfigWindowViewModel {
        private readonly PlainRequest _closeWindowRequest = new PlainRequest();

        /// <summary>Windowを閉じるイベント通知</summary>
        public PlainRequest CloseWindowRequest {
            get { return _closeWindowRequest; }
        }

        /// <summary>Windowを閉じるCommand</summary>
        public ICommand CloseWindowCommand {
            get { return new DelegateCommand(() => CloseWindowRequest.Raise(), null); }
        }
    }
}

 『CloseWindowCommand』が呼ばれると『CloseWindowRequest.Raise』メソッドを実行し、"画面を閉じる"イベントを発生させます。

サンプルで使用している『DelegateCommand』『PlainRequest』の実装は下記の通りです。

namespace AddInCommon.Presentation.Command {
    /// <summary>処理委譲Commandクラス</summary>
    /// <remarks>匿名メソッドに処理を委譲するCommand</remarks>
    public class DelegateCommand : ICommand {
        /// <summary>Executeメソッド委譲処理</summary>
        private readonly Action _invokeExecute;

        /// <summary>CanExecuteメソッド委譲処理</summary>
        private readonly Func<bool> _invokeCanExecute;

        /// <summary>コンストラクタ</summary>
        /// <param name="invokeExecute"></param>
        /// <param name="invokeCanExecute"></param>
        public DelegateCommand(Action invokeExecute, Func<bool> invokeCanExecute) {
            if (invokeExecute == null) { throw new ArgumentNullException("invokeExecute"); }
            _invokeExecute = invokeExecute;
            _invokeCanExecute = invokeCanExecute;
        }

        #region ICommand メンバー

        public bool CanExecute(object parameter) {
            return _invokeCanExecute == null ? true : _invokeCanExecute();
        }

        public event EventHandler CanExecuteChanged;
        
        public void Execute(object parameter) {
            _invokeExecute();
        }

        #endregion
    }
}

namespace AddInCommon.Presentation.InteractionRequest {
    /// <summary>単純にイベントを通知するだけの汎用イベント発生クラス</summary>
    public class PlainRequest {
        /// <summary>汎用イベント</summary>
        public event EventHandler<EventArgs> Raised;

        /// <summary>イベント発生</summary>
        public void Raise() {
            var handle = this.Raised;
            if (handle != null) {
                handle(this, new EventArgs());
            }
        }
    }
}

 Raiseメソッドが呼ばれると『RequestTrigger』で"画面を閉じる(Raise)"イベントとして処理します。

namespace AddInCommon.Presentation.InteractionRequest {
    /// <summary>汎用イベントトリガー</summary>
    public class RequestTrigger : EventTrigger {
        protected override string GetEventName() {
            // Requestクラスのイベント名
            return "Raised";
        }
    }
}

 "画面を閉じる(Raise)"イベントが発生するとxamlで紐づけられた『CloseWindowAction』が呼ばれ、Windowを閉じる処理が行われます。

namespace AddInCommon.Presentation.TriggerAction {
    /// <summary>Windowを「閉じる」アクションクラス</summary>
    public class CloseWindowAction : TriggerAction<Window> {
        protected override void Invoke(object parameter) {
            AssociatedObject.Close();
        }
    }
}

 ようやく辿り着きました。『TriggerAction<Window>』とすることでAssociatedObjectを介してWindowオブジェクトを操作できます。


 長くなってしまいましたが、処理の流れをまとめると下記のようになります。

<閉じるボタン押下(画面操作)>

『CloseWindowCommand』で『CloseWindowRequest』呼び出し

『CloseWindowRequest』で"画面を閉じる"イベント発生

『RequestTrigger』で"画面を閉じる"イベントをキャッチし『CloseWindowAction』呼び出し

『CloseWindowAction』でWindowオブジェクトのCloseメソッド呼び出し

<画面を閉じる>


 パッと見、コードビハインドで同様の処理を書く場合に比べてかなり面倒です。

 しかし、上記のようなクラスを予め作成しておくことで使い回しがきく上に『CloseWindowAction』以外はWindowに依存していないためテストコードを書きやすくなります。

 『Prisim』などのライブラリを活用したり、ジェネリックやAction,Funcを使った処理委譲と組み合わせれば、より汎用的な部品が作ることが可能です。

トラックバック - http://d.hatena.ne.jp/koyak/20120902/1346557120
リンク元