読者です 読者をやめる 読者になる 読者になる

xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

IsVisibleChangedをEventTriggerで拾えない

MVVMをやっていると大変お世話になるSystem.Windows.Interactivity.dllのEventTrigger。これでIsVisibleChangedイベントを拾えないという、どー考えてもバグらしい現象に遭遇して悲しくなったのが今日の業務のハイライト。

障害が出る簡単なサンプル

Windowは次のような感じです。IsVisibleChangedを発行するため、ボタンをクリックしたら、とあるコントロールのVisibilityプロパティを変更します。

<Window x:Class="WpfApplication.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"
        Title="Not triggering !?" Height="300" Width="300">
    <DockPanel>
        <!-- イベント発行用のボタン -->
        <Button DockPanel.Dock="Top" Content="Fire!!" Click="Button_Click" />

        <!-- 結果表示用のラベル -->
        <TextBlock DockPanel.Dock="Top" Background="MistyRose" Text="{Binding Text}" TextAlignment="Center" />

        <!-- IsVisibleChangedの検出対象 -->
        <TextBlock x:Name="target" Background="Azure">
            <i:Interaction.Triggers>
                <!-- IsVisibleChangedが発生したらViewModelのOnVisibleChangedが呼び出されるはず! -->
                <i:EventTrigger EventName="IsVisibleChanged">
                    <ei:CallMethodAction TargetObject="{Binding}" MethodName="OnVisibleChanged" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </TextBlock>
    </DockPanel>
</Window>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        this.InitializeComponent();
        this.DataContext = new ViewModel();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        this.target.Visibility = this.target.Visibility == Visibility.Visible
                               ? Visibility.Hidden
                               : Visibility.Visible;
    }
}

IsVisibleChangedイベントが発生したら、ViewModelのとあるメソッドを呼び出してもらうようにします。メソッドが呼び出されたら画面に時刻が表示されるようにしています。

public class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        var handler = this.PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }

    private string text = null;
    public string Text
    {
        get{ return this.text; }
        set
        {
            if (this.text == value)
                return;
            this.text = value;
            this.OnPropertyChanged("Text");
        }
    }

    //--- 呼び出されない!
    public void OnVisibleChanged()
    {
        this.Text = DateTime.Now.ToString();
    }
}

これを実行してボタンをクリックしても、悲しい哉、OnVisibleChangedメソッドは呼び出されません。謎ぃ...。

コードビハインドは呼び出される

EventTriggerじゃなければどうなのか?ということでコードビハインド側でIsVisibleChangedイベントを受けてみることにします。

<Window x:Class="WpfApplication.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Triggering !!" Height="300" Width="300">
    <DockPanel>
        <Button DockPanel.Dock="Top" Content="Fire!!" Click="Button_Click" />
        <TextBlock DockPanel.Dock="Top" x:Name="display" Background="MistyRose" Text="{Binding Text}" TextAlignment="Center" />
        <TextBlock x:Name="target" Background="Azure" IsVisibleChanged="OnVisibleChanged" />
    </DockPanel>
</Window>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        this.InitializeComponent();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        this.target.Visibility  = this.target.Visibility == Visibility.Visible
                                ? Visibility.Hidden
                                : Visibility.Visible;
    }

    private void OnVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        this.display.Text = DateTime.Now.ToString();
    }
}

言わずもがな、これはうまく行きます。ですよね...。こうなるとEventTriggerさんの動きが怪しい気配。

どう回避するか

楽をしたいなら、(少し悲しい思いをしながら)コードビハインドを利用するのもアリでしょう。少し立ち止まって「本当に可視状態の変更のタイミングを使わないとダメなのか」と、シチュエーション自体を見直すのもアリです。もうひとつは、Visibilityプロパティの変更を受けてトリガーとすることです。これが手っ取り早いかと思います。

<TextBlock x:Name="target">
    <i:Interaction.Triggers>
        <ei:PropertyChangedTrigger Binding="{Binding Visibility, ElementName=target}">
            <ei:CallMethodAction TargetObject="{Binding}" MethodName="OnVisibleChanged" />
        </ei:PropertyChangedTrigger>
    </i:Interaction.Triggers>
</TextBlock>

こちらにも同様の解法が載っていました。このようなことは稀だとは思いますが、思った挙動をしないと少しテンパりますね...。

MVVM binding to CLR Events - Stack Overflow