Custom ToggleButton with Popup

0
174

Introduction

I needed custom ToggleButton with embedded Popup. Popup’s IsOpen is bounded to Toggle’s IsChecked,

The problem is that ToggleButton behaves differently when different controls embedded in the Popup are clicked:

  • click on embedded Button
    • IsChecked isn’t changed and Popup stays opened
    • Toggle’s command is not executed
  • click in embedded Label / TextBlock / etc.
    • IsChecked is set to false and the Popup closes
    • Toggle’s command is executed

I’ve found a way to prevent Popup’s closing when embeded non-Button is clicked.

Note: I didn’t have an option to use other type of custom control, but ToggleButton.

Solution

Popup’s IsOpen bounded to IsChecked of ToggleButton, thus clicking on ToggleButton opens / closes the Popup:

<Popup IsOpen="{Binding IsChecked, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ToggleButton}}" ...
ToggleButton checked/unchecked
ToggleButton is unchecked with Popup closed

ToggleButton is checked with Popup opened

The popup may contain various child elements. When a child element in the popup is clicked it changes IsChecked to false and Popup closes. But when embedded Button is clicked, IsChecked remains true. In this case ToggleButton’s command is not executed.

I’ve looked for a way to change the non-Button elements behavior and moreover  to let the developer to determine each non-button  element behavior.

I’ve solved this by defining two attached properties:

public static readonly DependencyProperty DontUnCheckToggleFromChildElementProperty = DependencyProperty.RegisterAttached(
 "DontUnCheckToggleFromChildElement", 
 typeof(bool), 
 typeof(MainWindow), 
 new PropertyMetadata(false));

public static bool GetDontUnCheckToggleFromChildElement(DependencyObject d)
{
 return (bool)d.GetValue(DontUnCheckToggleFromChildElementProperty);
}

public static void SetDontUnCheckToggleFromChildElement(DependencyObject d, bool value)
{
 d.SetValue(DontUnCheckToggleFromChildElementProperty, value);
}

public static readonly DependencyProperty IgnoreClickOnToggleButtonProperty = DependencyProperty.RegisterAttached(
 "IgnoreClickOnToggleButton", 
 typeof(bool), 
 typeof(MainWindow), 
 new PropertyMetadata(false));

public static bool GetIgnoreClickOnToggleButton(DependencyObject d)
{
 return (bool)d.GetValue(IgnoreClickOnToggleButtonProperty);
}

public static void SetIgnoreClickOnToggleButton(DependencyObject d, bool value)
{
 d.SetValue(IgnoreClickOnToggleButtonProperty, value);
}

DontUnCheckToggleFromChildElement set to true allows me to indicate that I don’t want the click on particular element will close the Popup:

<Label Content="Label1"
   local:MainWindow.DontUnCheckToggleFromChildElement="True"/>

<Label Content="Labe2"/>

In code snippet above, click on Label1 will not cause the containing Popup to close, but click on Label2 – will close it.

In Window’s PreviewMouseDown handler (it can be other container PreviewMouseDown handler) I’ve added a call to method that  looks for DontUnCheckToggleFromChildElement property up through VisualTree. The method receives OriginalSource that raised PreviewMouseDown and traverces VisualTree until it reaches the ToggleButton:

private void PreviewMouseDownHandler(object sender, MouseButtonEventArgs e)
{
 TestForDontUnCheckToggleFromChildElement(e.OriginalSource as DependencyObject);
}

private void TestForDontUnCheckToggleFromChildElement(Object obj)
{
 bool dontUncheck = false;
 DependencyObject dObj = obj as DependencyObject;
 while (dObj != null)
 {
 dontUncheck = dontUncheck || MainWindow.GetDontUnCheckToggleFromChildElement(dObj);

 ToggleButton tb = dObj as ToggleButton;
 if (tb != null && dontUncheck)
 {
 MainWindow.SetIgnoreClickOnToggleButton(dObj, true);
 break;
 }

 dObj = VisualTreeHelper.GetParent(dObj);
 if (dObj != null && dObj.GetType().Name == "PopupRoot")
 dObj = LogicalTreeHelper.GetParent(dObj);
 }
}

In some applications if DontUnCheckToggleFromChildElement = true is found, its enough to set e.Handled = true in PreviewMouseDown handler to prevent ToggleButton’s IsChecked to change.

In my case it wasn’t enough (havy application operated by touch screen only).

Thus in my case, if any child element has DontUnCheckToggleFromChildElement = true, the containing ToggleButton receives attached property IgnoreClickOnToggleButton = true.

Now, having IgnoreClickOnToggleButton = true, I use it in following methods:

  • in Converter for Tag property, that is called each time IsChecked changes
  • in a callback for ToggleButton’s Command property

Using in Converter:

in XAML:

<local:ToggleButtonIsCheckedHelper x:Key="isCheckedHelper"/>

<Style x:Key="toggleWithPopup" TargetType="ToggleButton">
 ...
 <Setter Property="Tag">
 <Setter.Value>
 <MultiBinding Converter="{StaticResource isCheckedHelper}">
 <Binding RelativeSource="{RelativeSource Mode=Self}"/>
 <Binding Path="IsChecked" RelativeSource="{RelativeSource Mode=Self}"/>
 </MultiBinding>
 </Setter.Value>
 </Setter>
 ...
</Style>

in code-behind:

public class ToggleButtonIsCheckedHelper : IMultiValueConverter
{
 public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
 {
 ToggleButton tb = values[0] as ToggleButton;

 if (MainWindow.GetIgnoreClickOnToggleButton(tb as DependencyObject))
 tb.IsChecked = true;

 return tb.Tag;
 }

 ...
}

The above code

if (MainWindow.GetIgnoreClickOnToggleButton(tb as DependencyObject)) tb.IsChecked = true;

keeps the IsChecked equal to true and thus keeps the Popup opened.

Using in Command callback:

in XAML:

<local:ToggleButtonIsCheckedHelper x:Key="isCheckedHelper"/>

<Style x:Key="toggleWithPopup" TargetType="ToggleButton">
 ...
 <Setter Property="Command" Value="{Binding MyCommand1}"/>
 <Setter Property="CommandParameter" Value="{Binding RelativeSource={RelativeSource Self}}"/>
 ...
</Style>

in code-behind:

MyCommand1 = new RelayCommand( o =>
{
 bool ignoreClick = MainWindow.GetIgnoreClickOnToggleButton(o as DependencyObject);
 if (ignoreClick)
 {
 MainWindow.SetIgnoreClickOnToggleButton(o as DependencyObject, false);
 return;
 }
  ....
});

The above block

bool ignoreClick = MainWindow.GetIgnoreClickOnToggleButton(o as DependencyObject);
if (ignoreClick)
{
 MainWindow.SetIgnoreClickOnToggleButton(o as DependencyObject, false);
 return;
}

allows to leave Command callback before execution.

History

LEAVE A REPLY