かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

MessengerのかわりにReactive ExtensionsのSubjectあたりを代用してみる


という呟きを見て、あぁ確かに出来そうだと思ったので岩永さんのコードは見てないのですけど、ちょっくら遊んでみました。なるべくReactive Extensionsの生のクラスを使ってMessengerが提供してるような機能を実現してみようと思います。でも、普通はラップする形で実装するのがいいと思います。

メッセージクラスの作成

とりあえず、送るデータの入れ物を定義します。

public interface IMessage
{
    Action<object> Callback { get; set; }
    object Parameter { get; set; }
}

public class Message<T, R> : IMessage
{
    public Action<R> Callback { get; set; }
    public T Parameter { get; set; }

    public Message(T parameter, Action<R> callback)
    {
        this.Parameter = parameter;
        this.Callback = callback;
    }

    #region IMessage メンバー

    Action<object> IMessage.Callback
    {
        get
        {
            return (object o) => this.Callback((R)o);
        }
        set
        {
            this.Callback = (R r) => value(r);
        }
    }

    object IMessage.Parameter
    {
        get
        {
            return this.Parameter;
        }
        set
        {
            this.Parameter = (T)value;
        }
    }

    #endregion
}

IMessengerというタイプセーフじゃない感じのインターフェースを継承する感じでタイプセーフっぽいMessageクラスを定義します。

TriggerとActionを定義する

そして、このIMessageを投げてくるIObservableを監視するトリガーを定義します。

public class ObservableTrigger : TriggerBase<FrameworkElement>
{
    public IObservable<IMessage> Observable
    {
        get { return (IObservable<IMessage>)GetValue(ObservableProperty); }
        set { SetValue(ObservableProperty, value); }
    }

    // 我ながらさいてー
    public static readonly DependencyProperty ObservableProperty =
        DependencyProperty.Register("Observable",
            typeof(IObservable<IMessage>),
            typeof(ObservableTrigger),
            new UIPropertyMetadata(null,
                (s, e) =>
                {
                    // とりあえずということで古いオブジェクトのことは考慮しない
                    var o = e.NewValue as IObservable<IMessage>;
                    o.Subscribe(m =>
                        {
                            ((ObservableTrigger)s).InvokeActions(m);
                        });
                }));

}

// とりあえずメッセージボックスだすアクションは作っとく
public class MessageBoxAction : TriggerAction<FrameworkElement>
{
    protected override void Invoke(object parameter)
    {
        var message = parameter as Message<string, MessageBoxResult>;
        var r = MessageBox.Show(message.Parameter, "確認", MessageBoxButton.OKCancel);
        // コールバックで結果を返す
        message.Callback(r);
    }
}

あとは、これでSubject>といった感じの型を使ってOnNextしてやれば動きそうですが、若干冗長な記述になりがちなので、拡張メソッドを定義して、そこらへんのめんどくささを隠します。

// 戻りもIObservableなのでFirstで待つなりSubscribeするなりお好きに
public static class ObserverExtensions
{
    public static IObservable<R> Send<T, R>(this IObserver<Message<T, R>> self, T parameter)
    {
        var subject = new AsyncSubject<R>();
        self.OnNext(new Message<T, R>(
            parameter, 
            r => 
            { 
                subject.OnNext(r); 
                subject.OnCompleted(); 
            }));
        return subject.AsObservable();
    }
}

ViewModelの定義

そして、MainWindowViewModelを以下のように定義します。

public class MainWindowViewModel : NotificationObject
{
    // こいつがメッセンジャーかわり
    private Subject<Message<string, MessageBoxResult>> subject = new Subject<Message<string, MessageBoxResult>>();

    // メッセージを投げるコマンド
    private DelegateCommand alertCommand;

    // 画面に表示するメッセージ
    private string message;
        
    public DelegateCommand AlertCommand
    {
        get
        {
            return this.alertCommand = this.alertCommand ??
                new DelegateCommand(() =>
                    {
                        // メッセージを送って結果を待つ
                        this.Message = subject.Send("こんにちは")
                            .First()
                            .ToString();
                    });
        }
    }

    public string Message
    {
        get
        {
            return this.message;
        }

        set
        {
            this.message = value;
            base.RaisePropertyChanged(() => Message);
        }
    }

    // ViewへIObservableとして公開しておく。このプロパティにTriggerがバインドする
    public IObservable<Message<string, MessageBoxResult>> Observable
    {
        get
        {
            return subject.AsObservable();
        }
    }

}

Viewを作る

ViewはボタンとテキストブロックとTriggerとActionでさくっといきます。

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="525" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
    <Window.DataContext>
        <l:MainWindowViewModel />
    </Window.DataContext>
    <Grid>
        <i:Interaction.Triggers>
            <l:ObservableTrigger Observable="{Binding Path=Observable}">
                <l:MessageBoxAction />
            </l:ObservableTrigger>
        </i:Interaction.Triggers>
        <Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="12,12,0,0" Name="button1" VerticalAlignment="Top" Width="75" Command="{Binding Path=AlertCommand}" />
        <TextBlock HorizontalAlignment="Left" Margin="12,41,0,0" Name="textBlock1" Text="{Binding Path=Message}" VerticalAlignment="Top" />
    </Grid>
</Window>

実行してみよう

実行してボタンを押したところ。

そして、メッセージボックスのボタンを押してVMに処理が戻って結果が画面に表示されたところ。

とりあえず動くっぽいですね。Rx色々出来るんですなぁ。パズルみたいで面白い。