简体   繁体   中英

How to not close a xtk:SplitButton's custom Popup when a click is made on a button inside it?

I have a custom control with an override for OnApplyTemplate. In it I tried to access children of children templates but they seem to not be loaded. What I wish: when the PART_IncreaseButton inside the Popup of a xtk:SplitButton is clicked, the Popup will not close but just let the Button react to the click.

CustomIntegerUpDown and CustomSplitButton are derived from the Xceed Extended WPF Toolkit. CustomIntegerUpDown has no changed style or template or code-behind and currently its only purpose is to do what I said above but I am just at the beginning of it. Below is all the relevant source.

I tried this:

IncrementButton = Utils.FindChild<RepeatButton>(PartPopup, "PART_IncreaseButton")

and IncrementButton is null after that, although in the Immediate Window:

Utils.FindChild<Popup>(this, "PART_Popup") returns the Popup as obtained from GetTemplateChild("PART_Popup") .

then

Utils.FindChild<ButtonSpinner>(PartPopup, "PART_Spinner") returns null .

Utils.FindChild<CustomIntegerUpDown>(PartPopup, "MyCustomIntegerUpDown") return null .

VisualTreeHelper.GetChildrenCount(PartPopup) returns 0 .

PartPopup.ApplyTemplate() returns false .

I've also seen this and I am not sure if it is worth trying that way.

FindChild is this (took from here ):

/// <summary>
/// Finds a Child of a given item in the visual tree.
/// </summary>
/// <param name="parent">A direct parent of the queried item.</param>
/// <typeparam name="T">The type of the queried item.</typeparam>
/// <param name="childName">x:Name or Name of child. </param>
/// <returns>The first parent item that matches the submitted type parameter.
/// If not matching item can be found,
/// a null parent is being returned.</returns>
public static T FindChild<T>(System.Windows.DependencyObject parent, string childName)
    where T : System.Windows.DependencyObject
{
    // Confirm parent and childName are valid.
    if (parent == null) return null;
    T foundChild = null;
    int childrenCount = System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent);
    for (int i = 0; i < childrenCount; i++)
    {
        var child = System.Windows.Media.VisualTreeHelper.GetChild(parent, i);
        // If the child is not of the request child type child
        T childType = child as T;
        if (childType == null)
        {
            // recursively drill down the tree
            foundChild = FindChild<T>(child, childName);
            // If the child is found, break so we do not overwrite the found child.
            if (foundChild != null) break;
        }
        else if (!string.IsNullOrEmpty(childName))
        {
            var frameworkElement = child as System.Windows.FrameworkElement;
            // If the child's name is set for search
            if (frameworkElement != null && frameworkElement.Name == childName)
            {
                // if the child's name is of the request name
                foundChild = (T)child;
                break;
            }
        }
        else
        {
            // child element found.
            foundChild = (T)child;
            break;
        }
    }
    return foundChild;
}

In CustomSplitButton.xaml.cs I have this:

internal Popup PartPopup;
internal Button PartButtonWith1, PartButtonWith5, PartButtonWith10, PartButtonWithCustom;
internal RepeatButton IncrementButton;
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    PartPopup = (Popup)GetTemplateChild("PART_Popup");
    PartButtonWith1 = (Button)GetTemplateChild("PART_ButtonWith1");
    PartButtonWith5 = (Button)GetTemplateChild("PART_ButtonWith5");
    PartButtonWith10 = (Button)GetTemplateChild("PART_ButtonWith10");
    PartButtonWithCustom = (Button)GetTemplateChild("PART_ButtonWithCustom");
    PartPopup.ApplyTemplate();
    IncrementButton = Utils.FindChild<RepeatButton>(PartPopup, "PART_IncreaseButton");
    if (PartPopup != null)
    {
        PartPopup.PreviewMouseDown += PART_Popup_PreviewMouseUp;
        PartPopup.PreviewMouseUp += PART_Popup_PreviewMouseUp;
    }
    if (PartButtonWith1 != null)
    {
        PartButtonWith1.Click += Btns_NewTimer_Click;
    }
    if (PartButtonWith5 != null)
    {
        PartButtonWith5.Click += Btns_NewTimer_Click;
    }
    if (PartButtonWith10 != null)
    {
        PartButtonWith10.Click += Btns_NewTimer_Click;
    }
    if (PartButtonWithCustom != null)
    {
        PartButtonWithCustom.Click += BtnCustom_Click;
    }
}

The visual tree is this:

实时视觉树的屏幕截图

The style of CustomSplitButton is the following ( xmlns:xtkThemes="clr-namespace:Xceed.Wpf.Toolkit.Themes;assembly=Xceed.Wpf.Toolkit" ):

<Style x:Key="AddCountSplitButtonStyle" TargetType="{x:Type xtk:SplitButton}">
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="IsTabStop" Value="False"/>
    <Setter Property="HorizontalContentAlignment" Value="Center"/>
    <Setter Property="VerticalContentAlignment" Value="Center"/>
    <Setter Property="Background" Value="{DynamicResource {ComponentResourceKey ResourceId=ButtonNormalBackgroundKey, TypeInTargetAssembly={x:Type xtkThemes:ResourceKeys}}}"/>
    <Setter Property="BorderBrush" Value="{DynamicResource {ComponentResourceKey ResourceId=ButtonNormalOuterBorderKey, TypeInTargetAssembly={x:Type xtkThemes:ResourceKeys}}}"/>
    <Setter Property="DropDownContentBackground">
        <Setter.Value>
            <LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
                <GradientStop Color="#FFF0F0F0" Offset="0"/>
                <GradientStop Color="#FFE5E5E5" Offset="1"/>
            </LinearGradientBrush>
        </Setter.Value>
    </Setter>
    <Setter Property="Padding" Value="3"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type xtk:SplitButton}">
                <Grid x:Name="MainGrid" SnapsToDevicePixels="True">
                    <xtk:ButtonChrome x:Name="ControlChrome" BorderThickness="0" Background="{TemplateBinding Background}" RenderEnabled="{TemplateBinding IsEnabled}">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>
                            <Button x:Name="PART_ActionButton" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="0" Padding="{TemplateBinding Padding}" Style="{x:Null}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}">
                                <Button.Template>
                                    <ControlTemplate TargetType="{x:Type Button}">
                                        <ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}"/>
                                    </ControlTemplate>
                                </Button.Template>
                                <Grid>
                                    <xtk:ButtonChrome x:Name="ActionButtonChrome" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" RenderMouseOver="{Binding IsMouseOver, ElementName=PART_ActionButton}" RenderPressed="{Binding IsPressed, ElementName=PART_ActionButton}" RenderEnabled="{TemplateBinding IsEnabled}">
                                        <xtk:ButtonChrome.BorderThickness>
                                            <Binding ConverterParameter="2" Path="BorderThickness" RelativeSource="{RelativeSource TemplatedParent}">
                                                <Binding.Converter>
                                                    <xtk:ThicknessSideRemovalConverter/>
                                                </Binding.Converter>
                                            </Binding>
                                        </xtk:ButtonChrome.BorderThickness>
                                        <ContentPresenter x:Name="ActionButtonContent" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                                    </xtk:ButtonChrome>
                                </Grid>
                            </Button>
                            <ToggleButton x:Name="PART_ToggleButton" Grid.Column="1" IsChecked="{Binding IsOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
                                <ToggleButton.IsHitTestVisible>
                                    <Binding Path="IsOpen" RelativeSource="{RelativeSource TemplatedParent}">
                                        <Binding.Converter>
                                            <xtk:InverseBoolConverter/>
                                        </Binding.Converter>
                                    </Binding>
                                </ToggleButton.IsHitTestVisible>
                                <ToggleButton.Template>
                                    <ControlTemplate TargetType="{x:Type ToggleButton}">
                                        <ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}"/>
                                    </ControlTemplate>
                                </ToggleButton.Template>
                                <Grid>
                                    <xtk:ButtonChrome x:Name="ToggleButtonChrome" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="1,0" RenderMouseOver="{Binding IsMouseOver, ElementName=PART_ToggleButton}" RenderPressed="{Binding IsPressed, ElementName=PART_ToggleButton}" RenderChecked="{TemplateBinding IsOpen}" RenderEnabled="{TemplateBinding IsEnabled}">
                                        <Grid x:Name="arrowGlyph" IsHitTestVisible="False" Margin="4,3">
                                            <Path x:Name="Arrow" Data="M0,0L3,0 4.5,1.5 6,0 9,0 4.5,4.5z" Fill="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" Height="5" Margin="0,1,0,0" Width="9"/>
                                        </Grid>
                                    </xtk:ButtonChrome>
                                </Grid>
                            </ToggleButton>
                        </Grid>
                    </xtk:ButtonChrome>
                    <Popup x:Name="PART_Popup" AllowsTransparency="True" Focusable="False" HorizontalOffset="1" IsOpen="{Binding IsChecked, ElementName=PART_ToggleButton}" Placement="{TemplateBinding DropDownPosition}" VerticalOffset="1"
                                StaysOpen="False">

                        <Border BorderThickness="{DynamicResource DefaultBorderThickness}" Margin="10,0,10,10" Background="{DynamicResource DarkerBaseBrush}" BorderBrush="{DynamicResource PopupBorderBrush}" CornerRadius="{DynamicResource DefaultCornerRadius}">
                            <Grid MinWidth="100" Name="PART_ContentPresenter">
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="Auto"/>
                                    <RowDefinition Height="Auto"/>
                                    <RowDefinition Height="Auto"/>
                                    <RowDefinition Height="Auto"/>
                                </Grid.RowDefinitions>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="*"/>
                                    <ColumnDefinition Width="Auto"/>
                                </Grid.ColumnDefinitions>
                                <Button x:Name="PART_ButtonWith1" Grid.Row="0" Grid.ColumnSpan="2">
                                    1
                                </Button>
                                <Button x:Name="PART_ButtonWith5" Grid.Row="1" Grid.ColumnSpan="2">
                                    5
                                </Button>
                                <Button x:Name="PART_ButtonWith10" Grid.Row="2" Grid.ColumnSpan="2">
                                    10
                                </Button>
                                <local:CustomIntegerUpDown Grid.Row="3" Value="1"
                                                            Increment="1" ClipValueToMinMax="True"              
                                                            x:Name="MyCustomIntegerUpDown">
                                </local:CustomIntegerUpDown>
                                <Button x:Name="PART_ButtonWithCustom" Grid.Row="3" Grid.Column="1" Padding="2,2,2,2">
                                    &gt;
                                </Button>
                            </Grid>
                            <Border.Effect>
                                <DropShadowEffect ShadowDepth="0" BlurRadius="10" Color="{DynamicResource Base6Color}" />
                            </Border.Effect>
                        </Border>
                    </Popup>
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter Property="Fill" TargetName="Arrow" Value="#FFAFAFAF"/>
                        <Setter Property="Foreground" TargetName="ActionButtonChrome" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

In the OnApplyTemplate I would expect to be able to access children of templates inside templates inside this . But I did not find a way to do this.

Related question of mine here .

Update 1

The starting point of the example, updated (it uses the TryFindVisualChildElementByName extension method from BionicCode's answer):

internal Popup PartPopup;
internal Button PartButtonWith1, PartButtonWith5, PartButtonWith10, PartButtonWithCustom;
internal RepeatButton IncrementButton;

private void SplitButton_Loaded(object sender, RoutedEventArgs e)
{
    PartPopup = (Popup)GetTemplateChild("PART_Popup");
    PartButtonWith1 = (Button)GetTemplateChild("PART_ButtonWith1");
    PartButtonWith5 = (Button)GetTemplateChild("PART_ButtonWith5");
    PartButtonWith10 = (Button)GetTemplateChild("PART_ButtonWith10");
    PartButtonWithCustom = (Button)GetTemplateChild("PART_ButtonWithCustom");

    if (PartPopup != null)
    {
        PartPopup.ApplyTemplateRecursively();

        if (PartPopup.TryFindVisualChildElementByName("PART_IncreaseButton", out FrameworkElement incButton))
        {
            IncrementButton = (RepeatButton)incButton;

            // do something with IncrementButton here
        }

        PartPopup.PreviewMouseDown += PART_Popup_PreviewMouseUp;
        PartPopup.PreviewMouseUp += PART_Popup_PreviewMouseUp;
    }

    if (PartButtonWith1 != null)
    {
        PartButtonWith1.Click += Btns_NewTimer_Click;
    }
    if (PartButtonWith5 != null)
    {
        PartButtonWith5.Click += Btns_NewTimer_Click;
    }
    if (PartButtonWith10 != null)
    {
        PartButtonWith10.Click += Btns_NewTimer_Click;
    }
    if (PartButtonWithCustom != null)
    {
        PartButtonWithCustom.Click += BtnCustom_Click;
    }
}

The ApplyTemplateRecursively extension method used above, in 2 versions:

Not working version

Is it possible to make this version work somehow? I think it is more efficient.

/// <summary>
/// Not working because the ApplyTemplate affects the VisualTree and when applying
/// templates recursively it does not see the correct updated visual tree to be able
/// to continue.
/// </summary>
/// <param name="root"></param>
internal static void ApplyTemplateRecursively(this System.Windows.DependencyObject root)
{
    if (root is System.Windows.Controls.Primitives.Popup p)
    {
        p.Child.ApplyTemplateRecursively();
        return;
    }

    if (root is FrameworkElement r)
    {
        r.ApplyTemplate();
    }

    foreach (object element in System.Windows.LogicalTreeHelper.GetChildren(root))
    {
        if (element is System.Windows.DependencyObject el)
        {
            ApplyTemplateRecursively(el);
        }
    }
}

Working version

/// <summary>
/// I am not sure if this is sufficiently efficient, because it goes through the entire visual tree.
/// </summary>
/// <param name="root"></param>
internal static void ApplyTemplateRecursively(this System.Windows.DependencyObject root)
{
    if (root is System.Windows.Controls.Primitives.Popup p)
    {
        p.Child.ApplyTemplateRecursively();
        return;
    }

    if (root is FrameworkElement r)
    {
        r.ApplyTemplate();
    }

    for (int i = 0; i < System.Windows.Media.VisualTreeHelper.GetChildrenCount(root); ++i)
    {
        DependencyObject d = VisualTreeHelper.GetChild(root, i);
        ApplyTemplateRecursively(d);
    }
}

Now I am trying to solve the actual problem.

Update 2

I have reported this issue .

The point is that the content of a Popup is not directly part of the visual tree. That's why looking for visual children of a Popup will always return null . The content of a Popup is rendered separately and is assigned to the Popup.Child property. You need to extract those from the Child property before continuing tree traversal inside the Popup .

The following is a custom visual tree helper method to return the first child element that matches a given name. This helper properly searches inside a Popup element. This method is an Extension Method of type DependencyObject and has to be put into a static class :

public static bool TryFindVisualChildElementByName(
  this DependencyObject parent,
  string childElementName,
  out FrameworkElement resultElement)
{
  resultElement = null;

  if (parent is Popup popup)
  {
    parent = popup.Child;
    if (parent == null)
    {
      return false;
    }
  }

  for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
  {
    DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);        

    if (childElement is FrameworkElement uiElement && uiElement.Name.Equals(
          childElementName,
          StringComparison.OrdinalIgnoreCase))
    {
      resultElement = uiElement;
      return true;
    }

    if (childElement.TryFindVisualChildElementByName(childElementName, out resultElement))
    {
      return true;
    }
  }

  return false;
}

It's an Extension Method and it is used like this:

CustomSplitButton.xaml.cs

// Constructor
public CustomSplitButton()
{
  this.Loaded += GetParts;
}

private void GetParts(object sender, RoutedEventArgs e)
{
  if (this.TryFindVisualChildElementByName("PART_Popup", out FrameworkElement popupPart))
  {
    if (popupPart.TryFindVisualChildElementByName("PART_ContentPresenter", out FrameworkElement contentPresenter))
    {
      if (!contentPresenter.IsLoaded)
      {
        contentPresenter.Loaded += CompleteSearch;
      }
      else 
      {
        CompleteSearch(contentPresenter, null);
      }
    }
  }
}

private void CompleteSearch(object sender, RoutedEventArgs e)
{      
  contentPresenter.Loaded -= CompleteSearch;

  if ((sender as DependencyObject).TryFindVisualChildElementByName("PART_IncreaseButton", out FrameworkElement increaseButton))
  {        
    IncrementButton = (RepeatButton) increaseButton;
  }
}

Remarks

It is very important to search after a parent element was Loaded .

This is true for all elements in the visual tree. Since the SplitButton consists of a drop down that is collapsed by default, not all contents are loaded initially. Once the drop down is opened the SplitButton makes its content visible, which will add them to the visual tree. Up to this point the SplitButton.IsLoaded property will return false , indicating the button's incomplete visual state. What you need to do is, once you encountered a FrameworkElement where FrameworkElement.IsLoaded returns false you have to subscribe to the FrameworkElement.Loaded event. In this handler you can continue the visual tree traversal.
Popup like elements or collapsed controls add complexity to the visual tree traversal.


Edit: Keep Popup open when content was clicked

Now that you have told me that you are using the SplitButton inside a ToolBar I instantly knew the origin of your problem:

Classes in WPF which are focus scopes by default are Window , MenuItem , ToolBar , and ContextMenu . [Microsoft Docs: Logical Focus ]

Simply remove the focus scope from the ToolBar to prevent the focus from being removed from the Popup as soon as any of its content was clicked (received logical focus):

<ToolBar FocusManager.IsFocusScope="False"> 
  <CustomSplitButton />
</ToolBar>

Edit: Keep Popup open when clicked on PART_ToggleButton while the Popup is open

To prevent the Popup from closing and reopening when the PART_ToggleButton is clicked while the Popup is open you need to handle the mouse down event (application wide) and the opening of the Popup yourself.

First modify the PART_Popup to make it stay open and remove the binding from the IsOpen property:

CustomSplitButton.xaml

<Popup x:Name="PART_Popup"
       IsOpen="False"
       StaysOpen="True"
       AllowsTransparency="True"
       Focusable="False"
       HorizontalOffset="1"
       Placement="{TemplateBinding DropDownPosition}"
       VerticalOffset="1">

Then in your CustomSplitButton observe the mouse device for mouse down events and determine the hit target. I assume that you retrieved the underlying PART_Popup and PART_ToggleButton element and stored it in a property named PartPopup and PartToggleButton (see first part of this answer on how to do it):

CustomSplitButton.xaml.cs

public CustomSplitButton()
{
  this.Loaded += OnLoaded;
}

private void OnLoaded(object sender, RoutedEventArgs e)
{
  Mouse.AddPreviewMouseDownHandler(Application.Current.MainWindow, KeepPopupOpen);
}

private void KeepPopupOpen(object sender, RoutedEventArgs routedEventArgs)
{
  var mouseClickSourceElement = routedEventArgs.OriginalSource as DependencyObject;
  var isPopupContentClicked = false;
  var isPartToggleButtonClicked = 
    object.ReferenceEquals(routedEventArgs.Source, this) 
      && mouseClickSourceElement.TryFindVisualParentElement(out ButtonBase button) 
      && button.Name.Equals(this.PartToggleButton.Name, StringComparison.OrdinalIgnoreCase);

  if (!isPartToggleButtonClicked)
  {
    isPopupContentClicked = 
      object.ReferenceEquals(routedEventArgs.Source, this) 
        && mouseClickSourceElement.TryFindVisualParentElementByName("PART_ContentPresenter", out FrameworkElement popupContentPresenter));
  }

  this.PartPopup.IsOpen = this.IsOpen = isPartToggleButtonClicked || isPopupContentClicked ;
}

Extension Methods to find the visual parent by type and by name

public static class HelperExtensions
{
  public static bool TryFindVisualParentElement<TParent>(this DependencyObject child, out TParent resultElement)
    where TParent : DependencyObject
  {
    resultElement = null;

    if (child == null)
    {
      return false;
    }

    DependencyObject parentElement = VisualTreeHelper.GetParent(child);

    if (parentElement is TParent parent)
    {
      resultElement = parent;
      return true;
    }

    return parentElement.TryFindVisualParentElement(out resultElement);
  }

  public static bool TryFindVisualParentElementByName(
      this DependencyObject child,
      string elementName,
      out FrameworkElement resultElement)
    {
      resultElement = null;

      if (child == null)
      {
        return false;
      }

      DependencyObject parentElement = VisualTreeHelper.GetParent(child);

      if (parentElement is FrameworkElement frameworkElement &&
          frameworkElement.Name.Equals(elementName, StringComparison.OrdinalIgnoreCase))
      {
        resultElement = frameworkElement;
        return true;
      }

      return parentElement.TryFindVisualParentElementByName(elementName, out resultElement);
    }
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM