Jandy Jandy - 1 month ago 8
C# Question

One popup for all button

As topic mentioned.I want to use only one popup for all button in my application.I don't know how to get what I want.
Here is what my window looks like:

enter image description here

Info 1:

enter image description here

Info 2:

enter image description here

You can see popup appear on wrong position.I know I can position a popup by setting the PlacementTarget.But each Popup has a different value for the placement property.That is the problem.I'm looking another way to do it.

Here is a popup for option 1:

<StackPanel Orientation="Horizontal">
<!--Option 1: text and button-->
<TextBlock Text="Option 1"
Margin="10"
VerticalAlignment="Center" />
<Popup x:Name="popInfo"
PlacementTarget="{Binding ElementName=btnInfoOption1}"
IsOpen="{Binding IsShowInfo1}">
<ContentControl Style="{StaticResource ContentInfoStyle}">
<TextBlock Text="{Binding InfoContent}"
TextWrapping="Wrap"
Foreground="White"
Width="340"
Padding="10"
Margin="30,0,30,5"
FontSize="15" />
</ContentControl>
</Popup>

<Button x:Name="btnInfoOption1"
Style="{StaticResource btnIcons}"
Background="#0063b1"
Width="30"
Height="30"
Margin="10,10,20,10"
Command="{Binding CmdShowInfo, Delay=1500}"
Tag="{StaticResource ic_ginfo}" />
</StackPanel>


popup for option 2:

<StackPanel Orientation="Horizontal">
<!--Option 2: text and button-->
<TextBlock Text="Option 2"
Margin="10"
VerticalAlignment="Center" />
<Button x:Name="btnOption2"
Style="{StaticResource btnIcons}"
Background="#0063b1"
Width="30"
Height="30"
Margin="10,10,20,10"
Command="{Binding CmdShowInfo, Delay=1500}"
Tag="{StaticResource ic_ginfo}" />
</StackPanel>


ContentControl Style:

<Style TargetType="{x:Type ContentControl}"
x:Key="ContentInfoStyle">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Border Background="Green"
CornerRadius="3"
Padding="10,0,12,10">
<StackPanel>
<Button HorizontalAlignment="Right"
Tag="{StaticResource ic_gclear}"
Style="{StaticResource btnIcons}"
Background="White"
Margin="10,5,12,5"
Command="{Binding DataContext.CmdCloseInfo}"
Height="24" />
<ContentPresenter x:Name="content"
TextBlock.FontSize="14"
TextBlock.Foreground="White"
TextBlock.FontFamily="Arial"
Content="{TemplateBinding ContentControl.Content}" />
</StackPanel>
</Border>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>


Button icon style:

<Style TargetType="Button"
x:Key="btnIcons">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="brd" Background="Transparent"
SnapsToDevicePixels="True">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver" />
<VisualState x:Name="Pressed" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Path Stretch="Uniform" VerticalAlignment="Center"
Fill="{TemplateBinding Background}"
Data="{TemplateBinding Tag}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>


ViewModel.cs:
the content of popup:

private string _InfoContent;

public string InfoContent
{
get { return _InfoContent; }
set
{
if (value != _InfoContent)
{
_InfoContent = value;
OnRaise("InfoContent");
}
}
}


show the popup for option2 and option1:

private bool _IsShowInfo2;

public bool IsShowInfo2
{
get { return _IsShowInfo2; }
set
{
if (value != _IsShowInfo2)
{
_IsShowInfo2 = value;
OnRaise("IsShowInfo2");
}
}
}
//show the popup for option1
private bool _IsShowInfo1;

public bool IsShowInfo1
{
get { return _IsShowInfo1; }
set
{
if (value != _IsShowInfo1)
{
_IsShowInfo1 = value;
OnRaise("IsShowInfo1");
}
}
}


the command for button:

private ICommand _CmdShowInfo;

public ICommand CmdShowInfo
{
get
{
_CmdShowInfo = _CmdShowInfo ?? new RelayCommand(x => this.ShowInfo(true, 1), () => true);
return _CmdShowInfo;
}
}

private ICommand _CmdShowInfo2;

public ICommand CmdShowInfo2
{
get
{
_CmdShowInfo2 = _CmdShowInfo2 ?? new RelayCommand(x => this.ShowInfo(true, 0), () => true);
return _CmdShowInfo2;
}
}

private void ShowInfo(bool show = true, byte option = 0)
{
if (option == 0)
{
this.InfoContent = "Option 1...";
}
else if (option == 1)
{
this.InfoContent = "Option 2...";
}
this.IsShowInfo1 = show;
}

Answer

My initial thought was to do this with a styled HeaderedContentControl, but then you've got the icon fill color and the icon data, and I'd have had to add attached properties for those. Once you go there, you may as well just write a custom control.

The dependency properties of IconPopupButton can be bound like any dependency property:

<hec:IconPopupButton
    IsOpen="{Binding IsShowInfo1}"
    IconFill="YellowGreen"
    Content="Another Test Popup"
    IconData="M -10,-10 M 0,3 L 17,20 L 20,17 L 3,0 Z M 0,0 L 0,20 L 20,20 L 20,0 Z"
    />

If you want to parameterize the Style applied to the ContentControl in the Popup, add another dependency property. You'll need to give that some thought, though, because you need that ToggleButton to be bound to IsOpen on the templated parent, one way or another. Perhaps you could bind it to the viewmodel property that's bound to the popup button's IsOpen. There's always a way.

So here's that. With snippets to create dependency properties, this is pretty much just a fill-in-the-blanks exercise. Much less to it than meets the eye.

IconPopupButton.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;

namespace HollowEarth.Controls
{
    public class IconPopupButton : ContentControl
    {
        static IconPopupButton()
        {
            //  This causes it to use the style from Generic.xaml
            DefaultStyleKeyProperty.OverrideMetadata(typeof(IconPopupButton), new FrameworkPropertyMetadata(typeof(IconPopupButton)));
        }

        #region IconData Property
        public Geometry IconData
        {
            get { return (Geometry)GetValue(IconDataProperty); }
            set { SetValue(IconDataProperty, value); }
        }

        public static readonly DependencyProperty IconDataProperty =
            DependencyProperty.Register("IconData", typeof(Geometry), typeof(IconPopupButton),
                new PropertyMetadata(null));
        #endregion IconData Property

        #region IconFill Property
        public Brush IconFill
        {
            get { return (Brush)GetValue(IconFillProperty); }
            set { SetValue(IconFillProperty, value); }
        }

        public static readonly DependencyProperty IconFillProperty =
            DependencyProperty.Register("IconFill", typeof(Brush), typeof(IconPopupButton),
                new PropertyMetadata(SystemColors.ControlTextBrush));
        #endregion IconFill Property

        #region IsOpen Property
        public bool IsOpen
        {
            get { return (bool)GetValue(IsOpenProperty); }
            set { SetValue(IsOpenProperty, value); }
        }

        public static readonly DependencyProperty IsOpenProperty =
            DependencyProperty.Register("IsOpen", typeof(bool), typeof(IconPopupButton),
                new PropertyMetadata(false));
        #endregion IsOpen Property

        #region Placement Property
        public PlacementMode Placement
        {
            get { return (PlacementMode)GetValue(PlacementProperty); }
            set { SetValue(PlacementProperty, value); }
        }

        public static readonly DependencyProperty PlacementProperty =
            DependencyProperty.Register("Placement", typeof(PlacementMode), typeof(IconPopupButton),
                new PropertyMetadata(PlacementMode.Right));
        #endregion Placement Property
    }
}

Themes\Shared.xaml

<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:HeaderedPopupTest.Themes"
    >
    <Geometry x:Key="ic_gclear">M56,4 52,0 28,24 4,0 0,4 24,28 0,52 4,56 28,32 52,56 56,52 32,28Z</Geometry>
    <Geometry x:Key="ic_ginfo">M31,0C13.879,0,0,13.879,0,31s13.879,31,31,31s31-13.879,31-31S48.121,0,31,0z M34,46h-6V27.969h6V46z M34,21.969h-6V16h6V21.969z</Geometry>

    <Style TargetType="ButtonBase" x:Key="btnIcons">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ButtonBase}">
                    <Border x:Name="brd" Background="Transparent" SnapsToDevicePixels="True">
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualState x:Name="Normal" />
                                <VisualState x:Name="MouseOver" />
                                <VisualState x:Name="Pressed" />
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Grid>
                            <Path 
                                x:Name="Path"
                                Stretch="Uniform" 
                                VerticalAlignment="Center" 
                                Fill="{TemplateBinding Background}"
                                Data="{TemplateBinding Tag}" 
                                />
                            <TextBlock 
                                x:Name="MissingIconData"
                                Visibility="Collapsed" 
                                Text="?" 
                                FontWeight="Bold" 
                                FontSize="30" 
                                ToolTip="IconData (Tag) not set"
                                HorizontalAlignment="Center"
                                VerticalAlignment="Center"
                                />
                        </Grid>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="Tag" Value="{x:Null}">
                            <Setter TargetName="MissingIconData" Property="Visibility" Value="Visible" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

Themes\Generic.xaml

<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:HeaderedPopupTest.Themes"
    xmlns:hec="clr-namespace:HollowEarth.Controls"
    >

    <ResourceDictionary.MergedDictionaries>
        <!-- Change HeaderedPopupTest to the name of your own assembly -->
        <!-- You can't just use a relative path for this in Generic.xaml -->
        <ResourceDictionary Source="/HeaderedPopupTest;component/Themes/Shared.xaml" />
    </ResourceDictionary.MergedDictionaries>

    <Style TargetType="hec:IconPopupButton">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="hec:IconPopupButton">
                    <Grid>
                        <ToggleButton
                            x:Name="OpenButton"
                            Style="{StaticResource btnIcons}"
                            Background="{TemplateBinding IconFill}"
                            Tag="{TemplateBinding IconData}"
                            IsChecked="{Binding IsOpen, RelativeSource={RelativeSource TemplatedParent}}"
                            ToolTip="{TemplateBinding ToolTip}"
                            />
                        <Popup
                            x:Name="Popup"
                            StaysOpen="False"
                            IsOpen="{Binding IsOpen, RelativeSource={RelativeSource TemplatedParent}}"
                            PlacementTarget="{Binding ElementName=ToggleButton}"
                            Placement="{TemplateBinding Placement}"
                            >
                            <Border 
                                Background="Green"
                                CornerRadius="3"
                                Padding="10,0,12,10">
                                <StackPanel>
                                    <ToggleButton 
                                        HorizontalAlignment="Right" 
                                        Tag="{StaticResource ic_gclear}"
                                        Style="{StaticResource btnIcons}"
                                        Background="White"
                                        Margin="10,5,12,5"
                                        IsChecked="{Binding IsOpen, RelativeSource={RelativeSource TemplatedParent}}"
                                        Height="24" 
                                        />
                                    <ContentPresenter 
                                        x:Name="content"
                                        TextBlock.FontSize="14"
                                        TextBlock.Foreground="White"
                                        TextBlock.FontFamily="Arial"
                                        Content="{TemplateBinding Content}"
                                        />
                                </StackPanel>
                            </Border>
                        </Popup>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger SourceName="Popup" Property="IsOpen" Value="True">
                            <!-- 
                            If the button is enabled while the popup is open, then clicking on it
                            will cause the popup to flicker rather than close. 
                            -->
                            <Setter TargetName="OpenButton" Property="IsEnabled" Value="False" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

MainWindow.xaml example usage:

<Window 
    x:Class="HeaderedPopupTest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:HeaderedPopupTest"
    xmlns:hec="clr-namespace:HollowEarth.Controls"
    mc:Ignorable="d"
    Title="MainWindow" Height="350" Width="525"
    >
    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Themes\Shared.xaml" />
            </ResourceDictionary.MergedDictionaries>

            <Style 
                x:Key="InfoPopupButton" 
                TargetType="hec:IconPopupButton" 
                BasedOn="{StaticResource {x:Type hec:IconPopupButton}}"
                >
                <Setter Property="IconFill" Value="DeepSkyBlue" />
                <Setter Property="IconData" Value="{StaticResource ic_ginfo}" />
            </Style>
        </ResourceDictionary>
    </Window.Resources>

    <Grid>
        <StackPanel 
            Orientation="Vertical"
            HorizontalAlignment="Left"
            >
            <hec:IconPopupButton
                Style="{StaticResource InfoPopupButton}"
                Content="This is a test popup"
                ToolTip="Test Popup Tooltip"
                />
            <hec:IconPopupButton
                IconFill="YellowGreen"
                Content="Another Test Popup"
                IconData="M -10,-10 M 0,3 L 17,20 L 20,17 L 3,0 Z M 0,0 L 0,20 L 20,20 L 20,0 Z"
                />
            <hec:IconPopupButton
                IconFill="DarkRed"
                Content="Missing IconData behavior example"
                />
        </StackPanel>
    </Grid>
</Window>

You'll notice I changed your buttons to ToggleButton. This is for convenience in wiring them up to the IsOpen property: With a ToggleButton, I just bind IsChecked and I'm done. No need for commands. I changed the btnIcons style to target ButtonBase, so it works identically with Button and ToggleButton.