2018年8月8日水曜日

100件目の記事

初めての投稿から4年ちょっと経ちましたが、読者の皆さんに支えられてついに100件目の記事に到達しました。ありがとうございます。この記事が100件目です。

1年ちょっと前にはトータルPVが10万件を突破したということで記事にしましたが、すでにもう20万件に達しようとしています(この記事執筆時点で195,044件です)。
3年で到達した10万件を今度は1年半程度で実現する勢いで、発言力が上がってきているのを日に日に感じてうれしい反面、そんなに責任を持った発言をしているつもりもないので複雑な気持ちです。

せっかくなので、今回もここ1か月間のトップ5の記事を紹介します。

1位MVVMとは何か2017/11/18702PV
2位ポケモン「サファイア」の時計を復活させる2016/01/22667PV
3位WPFのScrollViewerやScrollBarのスクロール位置を同期させる 2015/01/16313PV
4位プロキシ設定とレジストリ2015/11/09224PV
5位C#の排他制御 - UpgradeableReadLock 2017/11/05209PV

前回のトップ5のうち3件が今回もランクインしていますが、1位は最近書いたMVVMに関する記事になっているようです。
MVVMは難しいです。何をもってMVVMなのか、これはMVVMなのか、私も今でもよく迷います。でも、皆さんも同じく迷っているということなのでしょうね。

それにしても、トップ記事の月間PVが2倍くらいになっています。上にはトータルPVで見た話をしましたが、月間PVでも如実に表れていますね。これだけPV稼げるようになってきたのなら、広告を掲載したら小遣い稼ぎ程度にはなるんでしょうかね。


ま、これからも気負わず何か気づいたことがあったら記事にしていこうと思いますので、これからも遠くからあたたかな目で見守っていただければと思います。よろしくお願いいたします。

InteractionMessageActionの自作

MVVMのデータバインディング機構は「状態」を伝達するのに非常に便利です。一方で、一般的なグラフィカル・ユーザー・インターフェースでは「状態」だけでは伝えきれない動作もしばしばあります。

その代表的なものがウィンドウ遷移です。ViewModelが「ダイアログボックスを表示しなさい」という指示を出し、それを受け取ったViewがウィンドウを表示するという動作は、状態では表現できません。
ウィンドウも「表示状態」「非表示状態」の2種類の状態がある、と考えれば確かに状態で表現できそうな気もしますが、それはあまり自然な発想ではないですよね。

そのため、解決策として多くのMVVMライブラリにはそのような指示をViewModelからViewへ伝達する機能を備えています。
LivetではMessengerというものがそれに該当します。ViewModel側でInteractionMessageに伝えたい情報を載せてMessenger.Raiseすると、View側のInteractionMessageTriggerが発動し、その受け取ったメッセージをもとに様々な動作を行うことができます。

例えばメッセージボックスを表示する例です。
<Window x:Class="MessengerTest.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:MessengerTest.Views"
        xmlns:vm="clr-namespace:MessengerTest.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="ContentRendered">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
        </i:EventTrigger>

        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>

        <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="Info">
            <l:InformationDialogInteractionMessageAction />
        </l:InteractionMessageTrigger>
    </i:Interaction.Triggers>
    
    <Grid>
        
    </Grid>
</Window>
ダイアログボックスはInteractionMessageTriggerの部分です。キーを設定し(今回は"Info")、そのキーに対してメッセージを送ると中のInformationDialogInteractionMessageActionが発動します。

肝心のメッセージはこのように送ります。
public class MainWindowViewModel : ViewModel
{
    public void Initialize()
    {
        Messenger.Raise(new InformationMessage("InformationMessage raised.", "Information", MessageBoxImage.Information, "Info"));
    }
}
Messenger.Raiseに必要な情報を与えて呼び出すと、このタイミングでInformationDialogInteractionMessageActionが発動します。
ポイントは、InteractionMessageとInteractionMessageActionは対になっていて、パラメーターの箱(Message)とそれを受けた動作(Action)をうまく定義することで、ViewModelからViewにいろいろな動作を働きかけることができるということです。
LivetでどのようなInteractionMessageやInteractionMessageActionが用意されているかというのは、以前書いたこちらの記事を参考にしてください。

InteractionMessageActionの自作

さて、このようなInteractionMessageActionを自作するにはどうしたら良いのでしょう。
ScrollViewerを任意の場所にスクロールするInteractionMessageActionを例に取りながら説明していきます。

ScrollViewerにはスクロール位置を示すVerticalOffsetプロパティHorizontalOffsetプロパティはあるのですが、これらは読み取り専用でスクロール位置を変更するのには使えません。スクロール位置を変更するにはScrollToVerticalOffsetメソッドScrollToHorizontalOffsetメソッドを呼び出す必要があります。
メソッドの呼び出しはデータバインディングで行うことはできませんので、InteractionMessageActionに頼る必要があります。

まずはInteractionMessageを自作します。
public class ScrollInteractionMessage : InteractionMessage
{
    public ScrollInteractionMessage() : base()
    {
        Command = ScrollCommand.None;
        Offset = 0;
    }

    public ScrollInteractionMessage(string messageKey) : base(messageKey)
    {
        Command = ScrollCommand.None;
        Offset = 0;
    }

    public ScrollInteractionMessage(ScrollCommand command) : base()
    {
        Command = command;
        Offset = 0;
    }

    public ScrollInteractionMessage(ScrollCommand command, string messageKey) : base(messageKey)
    {
        Command = command;
        Offset = 0;
    }

    public ScrollInteractionMessage(ScrollCommand command, double offset) : base()
    {
        Command = command;
        Offset = offset;
    }

    public ScrollInteractionMessage(ScrollCommand command, double offset, string messageKey) : base(messageKey)
    {
        Command = command;
        Offset = offset;
    }

    public ScrollCommand Command { get; set; }

    public double Offset { get; set; }

    protected override Freezable CreateInstanceCore()
    {
        return new ScrollInteractionMessage(Command, Offset, MessageKey);
    }
}
大部分がコンストラクタで見にくいですが、要するにCommand(どのようなスクロールをするか)とOffset(オフセットへの移動の場合の値)を持たせたInteractionMessageを作りました。
ここで使っているScrollCommandはこのような列挙型です。
public enum ScrollCommand
{
    None,
    Home,
    End,
    Top,
    Bottom,
    LeftEnd,
    RightEnd,
    VerticalOffset,
    HorizontalOffset,
}

さて、このInteractionMessageの実装で重要なのが、CreateInstanceCoreのオーバーライドです。これをやらずにドツボにはまったことが私はありました。
InteractionMessageはFreezableを継承しており、Messenger.Raise中でFreezeされます。そしてその後にコピーされてInteractionMessageActionに渡されるのですが、その際、CreateInstanceCoreを呼び出してコピーされます。
これをオーバーライドしておかないと、InteractionMessage.CreateInstanceCoreでコピー作業が行われます。それはすなわち、InteractionMessageActionに渡されるのがInteractionMessage型のインスタンスとなってしまい追加のパラメーターがすべて消えることを意味しています。
ハマりポイントですので、ぜひ回避するようにしましょう。

また、当然ですが、CreateInstanceCoreメソッドでは同じパラメーターのインスタンスを生成するためのものですので、継承時に追加したプロパティはしっかりコピーするようにしてください。今回ならばCommandとOffsetですね。

これを受けるScrollInteractionMessageActionはこんな感じに実装しました。
public class ScrollInteractionMessageAction : InteractionMessageAction<FrameworkElement>
{
    protected override void InvokeAction(InteractionMessage message)
    {
        if(AssociatedObject is ScrollViewer ctrl && message is ScrollInteractionMessage msg) {
            switch(msg.Command) {
                case ScrollCommand.Home:
                    ctrl.ScrollToHome();
                    break;
                case ScrollCommand.End:
                    ctrl.ScrollToEnd();
                    break;
                case ScrollCommand.Top:
                    ctrl.ScrollToTop();
                    break;
                case ScrollCommand.Bottom:
                    ctrl.ScrollToBottom();
                    break;
                case ScrollCommand.LeftEnd:
                    ctrl.ScrollToLeftEnd();
                    break;
                case ScrollCommand.RightEnd:
                    ctrl.ScrollToRightEnd();
                    break;
                case ScrollCommand.VerticalOffset:
                    ctrl.ScrollToVerticalOffset(msg.Offset);
                    break;
                case ScrollCommand.HorizontalOffset:
                    ctrl.ScrollToHorizontalOffset(msg.Offset);
                    break;
            }
        }
    }
}
C#7.0からは「変数 is 型 新しい変数」で「変数を新しい変数にキャストしつつ、キャストできたかどうかをboolで返す」という構文が導入されたので、型判定と変換を同時に行っています。便利なものです。
AssociatedObjectはこのActionがぶら下げられたコントロール、messageはMessenger.Raiseに渡されたメッセージです。受け取ったmessegeのCommandをもとに、呼び出すべきスクロールメソッドに制御を振り分けています。

さて、XAMLはこのようになります。
<ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
    <Image Source="{Binding FileName}" />
    <i:Interaction.Triggers>
        <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="Scroll">
            <a:ScrollInteractionMessageAction />
        </l:InteractionMessageTrigger>
    </i:Interaction.Triggers>
</ScrollViewer>
InteractionMessageTrigger(メッセージで発生するトリガー)にScrollInteractionMessageActionをぶら下げています。

ViewModelからはこのようにMessenger.Raiseを呼び出すだけです。
ScrollToLeft = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.LeftEnd, "Scroll")));
ScrollToRight = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.RightEnd, "Scroll")));
ScrollToTop = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.Top, "Scroll")));
ScrollToBottom = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.Bottom, "Scroll")));
コマンドのコールバックとしてMessenger.Raiseを呼び出しています。パラメーターにはコマンド名とメッセージキーだけです。これらのコマンドをボタンに結び付ければ完成です。
ボタンを押すだけで四隅へ移動できるソフトが出来上がりました。

参考までにViewとViewModelの全容を載せておきます。
<Window x:Class="MessengerTest.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:MessengerTest.Views"
        xmlns:a="clr-namespace:MessengerTest.Views.Actions"
        xmlns:vm="clr-namespace:MessengerTest.ViewModels"
        Title="MainWindow" Height="350" Width="525">

    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <i:Interaction.Triggers>
        <i:EventTrigger EventName="ContentRendered">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
        </i:EventTrigger>

        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>
    </i:Interaction.Triggers>

    <Grid>
        <DockPanel>
            <Grid DockPanel.Dock="Top">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="1*" />
                    <ColumnDefinition Width="1*" />
                    <ColumnDefinition Width="1*" />
                    <ColumnDefinition Width="1*" />
                </Grid.ColumnDefinitions>
                <Button Grid.Column="0" Content="Left" Command="{Binding ScrollToLeft}" Margin="2" />
                <Button Grid.Column="1" Content="Top" Command="{Binding ScrollToTop}" Margin="2" />
                <Button Grid.Column="2" Content="Bottom" Command="{Binding ScrollToBottom}" Margin="2" />
                <Button Grid.Column="3" Content="Right" Command="{Binding ScrollToRight}" Margin="2" />
            </Grid>
            <Grid DockPanel.Dock="Bottom">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="1*" />
                    <ColumnDefinition Width="100" />
                </Grid.ColumnDefinitions>
                <TextBlock Grid.Column="0" Text="Path: " VerticalAlignment="Center" Margin="2" />
                <TextBox Grid.Column="1" Text="{Binding FileName}" VerticalAlignment="Center" Margin="2" />
                <Button Grid.Column="2" Content="Browse..." VerticalAlignment="Center" Margin="2">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="Click">
                            <l:OpenFileDialogInteractionMessageAction>
                                <l:DirectInteractionMessage CallbackMethodTarget="{Binding}" CallbackMethodName="FileSelected">
                                    <l:OpeningFileSelectionMessage Filter="Image file (*.jpg;*.png;*.gif;*.bmp)|*.jpg;*.png;*.gif;*.bmp" MultiSelect="False" />
                                </l:DirectInteractionMessage>
                            </l:OpenFileDialogInteractionMessageAction>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Button>
            </Grid>
            <ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
                <Image Source="{Binding FileName}" />
                <i:Interaction.Triggers>
                    <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="Scroll">
                        <a:ScrollInteractionMessageAction />
                    </l:InteractionMessageTrigger>
                </i:Interaction.Triggers>
            </ScrollViewer>
        </DockPanel>
    </Grid>
</Window>
public class MainWindowViewModel : ViewModel
{
    public MainWindowViewModel()
    {
        ScrollToLeft = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.LeftEnd, "Scroll")));
        ScrollToRight = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.RightEnd, "Scroll")));
        ScrollToTop = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.Top, "Scroll")));
        ScrollToBottom = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.Bottom, "Scroll")));
    }

    public void Initialize()
    {
    }

    #region FileName変更通知プロパティ
    private string _FileName;

    public string FileName
    {
        get
        { return _FileName; }
        set
        {
            if(_FileName == value)
                return;
            _FileName = value;
            RaisePropertyChanged(nameof(FileName));
        }
    }
    #endregion

    public ViewModelCommand ScrollToLeft { get; }
    public ViewModelCommand ScrollToRight { get; }
    public ViewModelCommand ScrollToTop { get; }
    public ViewModelCommand ScrollToBottom { get; }

    public void FileSelected(OpeningFileSelectionMessage msg)
    {
        if(msg.Response != null && msg.Response.Length == 1) {
            FileName = msg.Response.Single();
        }
    }
}
試してみたい方はこれとScrollInteractionMessageAction/ScrollInteractionMessageの2つのクラスで概ね動作させられるはずです。

おまけ

Livetを使っていると気づくかもしれませんが、WindowActionMessageをMessenger.Raiseしたときにうまく動きません。DirectInteractionMessage経由(XAMLで全部記述)ならばうまくいくのですが。
ですので、ViewModel側からウィンドウ操作をしたい場合は、WindowActionごとにInteractionMessageTriggerを用意して、キーで呼び分ける必要がでてきます。

なぜこんなことになっているのかというと、ただのバグです。
WindowActionMessageのCreateInstanceCoreのオーバーライドにてWindowActionのコピーを忘れています。ですので、Messenger.RaiseでどんなWindowActionを設定してもここの時点で闇に葬られるわけですね。
DirectInteractionMessageだとCreateInstanceCoreは呼ばれない(元のインスタンスのまま渡る)ので、設定した値がそのまま生かされます。

ま、もうLivetは事実上開発が止まっていますので、自分で修正してビルドして使うなりなんなりしてやるしかないですね。はい。