How to open a WPF Popup when another control is clicked, using XAML markup only?

Solution 1:

I did something simple, but it works.

I used a typical ToggleButton, which I restyled as a textblock by changing its control template. Then I just bound the IsChecked property on the ToggleButton to the IsOpen property on the popup. Popup has some properties like StaysOpen that let you modify the closing behavior.

The following works in XamlPad.

 <StackPanel>
  <ToggleButton Name="button"> 
    <ToggleButton.Template>
      <ControlTemplate TargetType="ToggleButton">
        <TextBlock>Click Me Here!!</TextBlock>
      </ControlTemplate>      
    </ToggleButton.Template>
  </ToggleButton>
  <Popup IsOpen="{Binding IsChecked, ElementName=button}" StaysOpen="False">
    <Border Background="LightYellow">
      <TextBlock>I'm the popup</TextBlock>
    </Border>
  </Popup> 
 </StackPanel>

Solution 2:

The following approach is the same as Helge Klein's, except that the popup closes automatically when you click anywhere outside the Popup (including the ToggleButton itself):

<ToggleButton x:Name="Btn" IsHitTestVisible="{Binding ElementName=Popup, Path=IsOpen, Mode=OneWay, Converter={local:BoolInverter}}">
    <TextBlock Text="Click here for popup!"/>
</ToggleButton>

<Popup IsOpen="{Binding IsChecked, ElementName=Btn}" x:Name="Popup" StaysOpen="False">
    <Border BorderBrush="Black" BorderThickness="1" Background="LightYellow">
        <CheckBox Content="This is a popup"/>
    </Border>
</Popup>

"BoolInverter" is used in the IsHitTestVisible binding so that when you click the ToggleButton again, the popup closes:

public class BoolInverter : MarkupExtension, IValueConverter
{
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is bool)
            return !(bool)value;
        return value;
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return Convert(value, targetType, parameter, culture);
    }
}

...which shows the handy technique of combining IValueConverter and MarkupExtension in one.

I did discover one problem with this technique: WPF is buggy when two popups are on the screen at the same time. Specifically, if your toggle button is on the "overflow popup" in a toolbar, then there will be two popups open after you click it. You may then find that the second popup (your popup) will stay open when you click anywhere else on your window. At that point, closing the popup is difficult. The user cannot click the ToggleButton again to close the popup because IsHitTestVisible is false because the popup is open! In my app I had to use a few hacks to mitigate this problem, such as the following test on the main window, which says (in the voice of Louis Black) "if the popup is open and the user clicks somewhere outside the popup, close the friggin' popup.":

PreviewMouseDown += (s, e) =>
{
    // Workaround for popup not closing automatically when 
    // two popups are on-screen at once.
    if (Popup.IsOpen)
    {
        Point p = e.GetPosition(Popup.Child);
        if (!IsInRange(p.X, 0, ((FrameworkElement)Popup.Child).ActualWidth) ||
            !IsInRange(p.Y, 0, ((FrameworkElement)Popup.Child).ActualHeight))
            Popup.IsOpen = false;
    }
};
// Elsewhere...
public static bool IsInRange(int num, int lo, int hi) => 
    num >= lo && num <= hi;

Solution 3:

The following uses EventTrigger to show the Popup. This means we don't need a ToggleButton for state binding. In this example the Click event of a Button is used. You can adapt it to use another element/event combination.

<Button x:Name="OpenPopup">Popup
    <Button.Triggers>
        <EventTrigger RoutedEvent="Button.Click">
            <EventTrigger.Actions>
                <BeginStoryboard>
                    <Storyboard>
                        <BooleanAnimationUsingKeyFrames 
                                 Storyboard.TargetName="ContextPopup" 
                                 Storyboard.TargetProperty="IsOpen">
                            <DiscreteBooleanKeyFrame KeyTime="0:0:0" Value="True" />
                        </BooleanAnimationUsingKeyFrames>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger.Actions>
        </EventTrigger>
    </Button.Triggers>
</Button>
<Popup x:Name="ContextPopup"
       PlacementTarget="{Binding ElementName=OpenPopup}"
       StaysOpen="False">
    <Label>Popupcontent...</Label>
</Popup>

Please note that the Popup is referencing the Button by name and vice versa. So x:Name="..." is required on both, the Popup and the Button.

It can actually be further simplified by replacing the Storyboard stuff with a custom SetProperty EventTrigger Action described in this SO Answer