概要
WinFormsなので、割と今更ネタ。
WinFormsくらいの開発くらいしか出来なさそうなメンバーが多いときで、どうしてもWPFのリッチなGUIを使いたいとき、最初に調べて使いだすのはElementHostであろう。
WinFormsから入った人が、WPFの開発にいきなり慣れること自体ハードルが高いのはそうなのだが
実はもっとつらい問題がある・・・。
このElementHost、WinFormsのツールボックスから読み込めるが故に、
WinForms同様に、イベントの検出したいよなぁ
・・・って思って、あれ、出来なくね?と悩みだすのである。
あるいは、ElementHostにWinFormsから命令したいなあと思ったとき
よう分からんなと投げ出してしまいたいものである。
直近の案件で、.NET Framework 4.8のアプリケーションを開発していて、
この課題をクリアする必要があったので説明しようと思う。
前提条件
項目 | 説明 |
---|---|
開発環境 | .NET Framework 4.8.1 |
API、フレームワーク | WinForms, WPF (MVVMのためにPrism等併用していたとしてもやり方は大体同じ) |
また、WPFのライブラリはwpflibrary.dllといった具合にDLLモジュール化されているものとする。
exeの実行はWinFormsで構築。
例えば、DataGridが定義されていて、そのイベントの検出と命令を行なうとしよう。
1. ElementHostから来るイベントを検出する
これは意外と簡単。
通知するだけであれば、WPFのxamlのBinding Propertyで、
RaisePropertyChanged(nameof(何かプロパティ));
として、Mode=OneWayにする処理の代わりに、イベントデリゲートを実行する処理に書き換わるだけなので、極端におかしな処理になる可能性は低い(オブジェクトの副作用さえ気を付ける必要があるので、通知するときの値のオブジェクトの関係性は慎重に考慮が必要)。
次のような手順で実施する。
イベントデリゲート用のパラメータクラスの作成
public class LineNotification
{
public int RowIndex
{
get;
set;
}
public string Name
{
get;
set;
}
}
イベントデリゲートとイベントを追加を定義
/**
LineSelectedEventプロパティ以外は、
Prismを使用していれば、ViewModelの内部に定義されていても問題ない
**/
public delegate void NotifyLineSelected(object sender, LineNotification notification);
private NotifyLineSelected _line_selected ;
public event NotifyLineSelected LineSelectedEvent
{
add
{
_line_selected += value;
}
remove
{
_line_selected -= value;
}
}
WinFormsからイベントの追加
ElementHostを使用している場合、コードビハインドのクラスは、
次のように参照できる。
var host = element_host.Child;
つまり、キャストしてイベント登録すれば、
WPFからイベントがあったときにそれを通知させることが出来るようになる。
/**
フォーム内のLoadメソッド
*/
public void Form_Load(object sender, EventArgs e)
{
/// ~~~ (省略) ~~~ //
var host = (WpfLibrary)element_host.Child;
host.LineSelectedEvent += LineSelected;
/// ~~~ (省略) ~~~ //
}
public void LineSelected(object sender, LineNotification notification)
{
// DataGridの行が選択されたときの処理を書く
}
2. ElementHostに命令を送る。
単純な方法は、1.同様にElementHostのChildをキャストして、直接コードビハインドのメソッドを叩く方法。
private void Order()
{
var host = (WpfLibrary)element_host.Child;
// WpfLibraryの中に、
// UpdateValueというメソッドがあって
// 一つはint, もう一つはstringだとすると...
host.UpdateValue( 0, "aaa" );
}
・・・が、これはWPFにWinForms専用のコードビハインドのメソッドをどっさり書かないといけない。
単純だが、MVVM的にはいろいろと面倒だ。
そこで、何とかしてElementHostのプロパティとして実行コマンドを送れないか?と考える。
そうすれば、WinForms専用の処理なんて新しく定義しなくても、プロパティをいじればWPFに命令できる。
が、これがなかなか難しい。
最初は、以前WinForms&VBでよく知られているdobon.netさんのサイトでこうした方法が知られていたので、これを応用する方法を考えていた。
「プロパティマップに独自の設定を追加する」を参照。
ところが、このまま使おうとしようものなら、かなりいろいろ問題がある。
- PropertyTranslatorとして扱えるプロパティはElementHostに登録済みのプロパティでなくてはいけない
- 一部のプロパティは、値を変更すると、勝手にPropertyTranslatorを起こせるので、他のプロパティの情報と重なってしまう。
そこで、私が考えた方法は、ElementHostのTagプロパティに、イベント命令用の通知を入れてしまおうというアイディアである。
これはWinFormsをある程度やっている人ならば想像たやすいだろう。
各コンポーネントで定義されているTagプロパティは(唯一といってもいい)他のプロパティに干渉しないobject型のプロパティであり、ここだけはユーザの任意の値をコンポーネント内に足すことが出来る。
したがって、私は次のように実施を行なった。
WPFにICommandを実行するためのコードビハインドを追加する
実行しようとしているViewModelはMainViewModelというクラスであるとする
public void Order(Func<MainViewModel, ICommand> functor, object param)
{
var viewmodel = (ViewModel.MainViewModel)DataContext;
var command = functor(viewmodel);
if (command != null && command.CanExecute(param))
{
command.Execute(param);
}
}
TagをPropertyTranslatorに登録する
dobon.netさんのサイト通り、PropertyTranslatorにTagを割り当てよう。
class SomethingInstruction
{
public ICommand Command
{
get;
set;
}
public object Parameter
{
get;
set;
}
}
public void Form_Load(object sender, EventArgs e)
{
/// 省略 ///
LocationTable.PropertyMap.Add(nameof(LocationTable.Tag), PropertyTranslator);
/// 省略 ///
}
private void PropertyTranslator(object host, string property_name, object value)
{
if (value == null)
{
return;
}
//ElementHostを取得する
var element_host = (System.Windows.Forms.Integration.ElementHost)host;
if( property_name != nameof(element_host.Tag))
{
return;
}
var wpf_control = (WpfLibrary)host_child;
if (element_host != null && element_host.Child != null)
{
/// ☆ 何かイベントを書く ///
/// 例:
var inst = (SomethingInstruction)value;
wpf_control.Order(inst.Command, inst.Parameter);
}
}
☆ 何かイベントを書くの部分に、
valueの値を検出して、その内容に応じてWpfLibraryに定義済みの内容の命令をする
といった形を取ればだいぶすっきり書くことが出来そうだ。
要は、ViewModelはICommandでビヘイビアやトリガーを実行してイベントを組んでいるんだから、WinFormsの枠組みのイベントもICommandとして定義する方が自然だよね! という発想である。
このように外部実行できるような形式にしさえすれば、後はWinFormsから
element_host.OnPropertyChanged(nameof(element_host.Tag), new SomethingInstruction()
{
Command = model => model.Commands.SomethingCommand,
Parameter = "text"
});
とすることで、WPFのViewModelのCommandを直接指定して実行することが出来る。
Parameterの部分はobject型になっているので、ViewModelに定義しているオブジェクトをそのまま使えば、
WPFのxamlファイルと同じインタフェースのまま直接実行することが可能だ。
追記
このあたりは、ElementHostに新しいプロパティを追加することで、
Tagを使う必要もないようだ。
ElementHostを継承したMyElementHostを作って
public class MyElementHost : ElementHost
{
private WpfRootCommand _commander;
/// <summary>
/// WPF側にコマンドを送るときに使用します。
/// </summary>
public WpfRootCommand Commander
{
get
{
return _commander;
}
set
{
if (_commander != null)
{
_commander = value;
OnPropertyChanged(nameof(Commander), value);
}
}
}
/// <summary>
/// コンストラクタ
/// </summary>
public MyElementHost()
{
_commander = new WpfRootCommand();
PropertyMap.Add(nameof(Commander), PropertyTranslator);
}
private void PropertyTranslator(object host, string property_name, object value)
{
if (property_name == nameof(Commander))
{
// Commanderが送られたとき、
// WPFのコマンドを実行する
if (value == null)
{
return;
}
if (value.GetType().IsSubclassOf(typeof(WpfRootCommand)))
{
var element_host = (CtkElementHost)host;
var wpf_control = (IReceiveWinFormOrder)element_host.Child;
var command = (WpfRootCommand)value;
wpf_control.Order(command.RootCommand, command.Parameter);
if (command.AfterExecuted != null)
{
command.AfterExecuted();
}
}
}
}
}
とし、命令用のクラスWpfRootCommand
public class WpfRootCommand
{
public virtual Func<object, ICommand> RootCommand { get; set; }
public virtual object Parameter { get; set; }
public virtual Action AfterExecuted { get; set; }
}
をWinForms側に用意する。そして、WPF側にはUserControlに新しくIReceiveWinFromOrderというインタフェースを追加して
public interface IReceiveWinFormOrder
{
void Order(Func<object, ICommand> functor, object param);
}
として、UserControlにインタフェースを実装してしまう。
そうすれば、MyElementHostにCommanderを送り込むことで、WPFに直感的にメッセージを送るアクションを実現することが出来る。
まとめ
最終的に、こんな感じにすることで
MVVMをあまり崩さずにElementHostを運用することが出来る。