かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

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色々出来るんですなぁ。パズルみたいで面白い。