ToggleButtonを押してPopupの表示/非表示を切り替えたかった話

プログラミング

偶には趣味の話でも。

以下、考察と検証プログラムコード

C# WPFの開発環境で、ToggleButtonを押すとPopupが出てくるUserControlを作成したかったのですが中々思ったような動作にならなかったので備忘録をば。

試しに以下のようなプログラムを実行してみます。

    <Grid>
        <ToggleButton x:Name="PART_ToggleButton" Click="ToggleButton_Click" >
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
                <StackPanel Orientation="Horizontal" Margin="4,1" HorizontalAlignment="Center" >
                    <TextBlock Text="TogglePopup1" />
                </StackPanel>
            </Grid>
        </ToggleButton>
        <Popup x:Name="PART_Popup"
        HorizontalOffset="1"
        PlacementTarget="{Binding ElementName=PART_ToggleButton}"
        VerticalOffset="1"
        AllowsTransparency="True"
        StaysOpen="False"
        Placement="Bottom"
        Focusable="False"
        IsOpen="{Binding IsChecked, ElementName=PART_ToggleButton}">
            <StackPanel Width="250">
                <Border BorderBrush="Gray" BorderThickness="1" Background="#FFE8EBED">
                    <StackPanel Orientation="Vertical">
                        <StackPanel HorizontalAlignment="Center">
                            <CheckBox Content="Check1"/>
                            <CheckBox Content="Check2"/>
                        </StackPanel>
                        <Button Content="OK" Click="Confirm_Click" />
                    </StackPanel>
                </Border>
            </StackPanel>
        </Popup>
    </Grid>
        void ToggleButton_Click(object sender, RoutedEventArgs e)
        {
            Debug.WriteLine(PART_ToggleButton.IsChecked);
        }

するとToggleButtonを押すたびにTrueがデバッグ出力されます。

試しにIsOpen=”{Binding IsChecked, ElementName=PART_ToggleButton}”を消して実行してみると正しくTrueとFalseが交互に出力されるようになります。

これはPopupが表示されている場合にToggleButtonを押すと、StaysOpen=”False”によりPopupが閉じ、ToggleButtonのIsCheckedがFalseになり、その後クリックイベントが発生することでToggleButtonがTrueになっているからだと考えられます。

これを単純に解決するのであればPopupをStaysOpen=”True”にすれば良いのですが、そうするとPopupの画面外をクリックしても閉じることがなくなります。

そこでBindingの条件を変更しClosedイベントハンドラを追加します。

Closed="Popup_Closed"
IsOpen="{Binding IsChecked, ElementName=PART_ToggleButton, Mode=OneWay}
    private void Popup_Closed(object sender, EventArgs e)
    {
        PART_ToggleButton.IsChecked = false;
    }

ただし、これでもボタンをある程度の速度で連打すると挙動が不安定になります。

恐らくクリックしてToggleButtonの状態が切り替わる前に、Popup_Closedが実行され、ToggleButtonがFalseになったあとにクリックイベントが発生しTrueになっているのだと思います。

この問題を回避するための一つの方法として、あまり良い方法ではないですがPopup_Closed後の一定時間はToggleButtonを無効化するように変更します。

        private async void Popup_Closed(object sender, EventArgs e)
        {
            PART_ToggleButton.IsEnabled = false;
            PART_ToggleButton.IsChecked = false;
            await Task.Delay(100);
            PART_ToggleButton.IsEnabled = true;
        }

ただし、ここまでしても挙動がおかしくなり、Popup表示中にToggleButtonを押してもすぐにPopupが再出現することがあります。

正直意味がわかりませんが、考えるとすればこれはクリックイベントと実際にIsCheckedが切り替わる間でPopup_Closedイベントが発生し、Popup_Closedイベント内でisCheckedをしたあとにクリックイベントでIsCheckedがtrueに戻ったということでしょうかね。。

最終的には僅かな遅延を入れることにより上書きされないようにすることにしました。

    private async void Popup_Closed(object sender, EventArgs e)
    {
        PART_ToggleButton.IsEnabled = false;
        await Task.Delay(1);
        PART_ToggleButton.IsChecked = false;
        await Task.Delay(1);
        PART_ToggleButton.IsEnabled = true;
    }

遅延時間に関しては、CPUの処理速度を考えるに十分だと思いますが、検証をしたわけでは無いので状況に応じて修正が必要です。

以上でとりあえず見た目上はToggleButtonを押すたびにPopupの表示/非表示が切り替えられる他、Popupの外をクリックすることでも閉じることの出来るUserControlが作成できました。

これ以上にしっかりした動作のものを作りたいのであれば、そもそものUIの表示方法を変えるほうが正しい選択肢だと思います。

コメント

タイトルとURLをコピーしました