绑定到装饰元素的祖先

发布于 2024-08-10 19:06:14 字数 3797 浏览 0 评论 0 原文

情况如下:

<DataTemplate x:Key="ItemTemplate"
              DataType="local:RoutedCustomCommand">
    <Button Command="{Binding}"
            Content="{Binding Text}"
            ToolTip="{Binding Description}">
        <Button.Visibility>
            <MultiBinding Converter="{StaticResource SomeConverter}">
            <!-- Converter simply checks flags matching 
                 and returns corresponding Visibility -->
                <Binding Path="VisibilityModes" /> 
                <!-- VisibilityModes is a property of local:RoutedCustomCommand -->


                <Binding Path="CurrentMode"
               RelativeSource="{RelativeSource AncestorType=local:CustomControl}" />
                <!-- CurrentMode is a property of local:CustomControl -->
            </MultiBinding>
        <Button.Visibility>
    </Button>
</DataTemplate>
<local:CustomControl>
    <!-- ... -->
    <ToolBar ...
             Width="15"
             ItemTemplate={StaticResource ItemTemplate}
             ... />
    <!-- Take a look at Width - it's especially is set to such a value 
         which forces items placement inside adorner overflow panel -->
    <!-- If you change ToolBar to ItemsControl, items won't be wrapped by adorner
         panel and everything will be OK -->
    <!-- ... -->
</local:CustomControl>

简而言之:当某些元素位于装饰器内部时,您不能简单地使用 Binding 的relativesource属性来访问装饰可视化树内的元素。

当我需要将其 FontSize 绑定到工具提示的所有者 FontSize 时,我已经习惯了 ToolTip 遇到同样的问题 - 有非常方便的 PlacementTarget 属性,我不需要在树内部查找 - 绑定看起来像这样:

这几乎是同样的问题 - 当该项目位于 ToolBarOverflowPanel 内时,它似乎位于装饰器内,因此relativesource 显然无法绑定。

问题是:我该如何解决这个棘手的问题?我真的需要绑定到容器的属性。即使我能够绑定装饰元素,距离祖先也还有很长的路要走。

UPD:最不幸的副作用是命令无法到达预期目标 - 通过冒泡机制的命令传播在装饰器的视觉根处停止:(。 显式目标的规范也会遇到同样的问题 - 目标必须位于 local:CustomControl 的可视化树内,而同一相对源绑定无法访问该可视化树。

UPD2:添加视觉树和逻辑树遍历结果:

UPD3:删除旧的遍历结果。添加了更精确的遍历:

UPD4:(希望这是最终的)。遍历逻辑父母的视觉树:

VisualTree
System.Windows.Controls.Button
System.Windows.Controls.ContentPresenter
System.Windows.Controls.Primitives.ToolBarOverflowPanel inherits from System.Windows.Controls.Panel
    LogicalTree
    System.Windows.Controls.Border
    Microsoft.Windows.Themes.SystemDropShadowChrome inherits from System.Windows.Controls.Decorator
    System.Windows.Controls.Primitives.Popup
    System.Windows.Controls.Grid
    logical root: System.Windows.Controls.Grid
System.Windows.Controls.Border
    LogicalTree
    Microsoft.Windows.Themes.SystemDropShadowChrome inherits from System.Windows.Controls.Decorator
    System.Windows.Controls.Primitives.Popup
    System.Windows.Controls.Grid
    logical root: System.Windows.Controls.Grid
Microsoft.Windows.Themes.SystemDropShadowChrome inherits from System.Windows.Controls.Decorator
    LogicalTree
    System.Windows.Controls.Primitives.Popup
    System.Windows.Controls.Grid
    logical root: System.Windows.Controls.Grid
System.Windows.Documents.NonLogicalAdornerDecorator inherits from System.Windows.Documents.AdornerDecorator
    LogicalTree
    logical root: System.Windows.Controls.Decorator
System.Windows.Controls.Decorator
visual root: System.Windows.Controls.Primitives.PopupRoot inherits from System.Windows.FrameworkElement
    LogicalTree
    System.Windows.Controls.Primitives.Popup
        VisualTree
        System.Windows.Controls.Grid
        System.Windows.Controls.Grid
        here it is: System.Windows.Controls.ToolBar
    System.Windows.Controls.Grid
    logical root: System.Windows.Controls.Grid

提前致谢!

Here is the case:

<DataTemplate x:Key="ItemTemplate"
              DataType="local:RoutedCustomCommand">
    <Button Command="{Binding}"
            Content="{Binding Text}"
            ToolTip="{Binding Description}">
        <Button.Visibility>
            <MultiBinding Converter="{StaticResource SomeConverter}">
            <!-- Converter simply checks flags matching 
                 and returns corresponding Visibility -->
                <Binding Path="VisibilityModes" /> 
                <!-- VisibilityModes is a property of local:RoutedCustomCommand -->


                <Binding Path="CurrentMode"
               RelativeSource="{RelativeSource AncestorType=local:CustomControl}" />
                <!-- CurrentMode is a property of local:CustomControl -->
            </MultiBinding>
        <Button.Visibility>
    </Button>
</DataTemplate>
<local:CustomControl>
    <!-- ... -->
    <ToolBar ...
             Width="15"
             ItemTemplate={StaticResource ItemTemplate}
             ... />
    <!-- Take a look at Width - it's especially is set to such a value 
         which forces items placement inside adorner overflow panel -->
    <!-- If you change ToolBar to ItemsControl, items won't be wrapped by adorner
         panel and everything will be OK -->
    <!-- ... -->
</local:CustomControl>

In several words: when some element is inside adorner, you can't simply use RelativeSource property of Binding to access elements inside adorned visual tree.

I've already used to bump into the same problem with ToolTip, when I needed to bind its FontSize to the tool-tip's owner FontSize - there was very handy PlacementTarget property and I didn't need to lookup inside the tree - the binding looked like this: <Binding PlacementTarget.FontSize />

Here is almost the same problem - when the item is inside ToolBarOverflowPanel it appears to be inside adorner, so RelativeSource obviously fails to bind.

The question is: how do I solve this tricky problem? I really need to bind to the container's property. Even if I were able to bind to adorned element, there also remains long way to the ancestor.

UPD: the most unfortunate side effect is that Command don't reach intended target - Command propagation through bubbling mechanism stops at adorner's visual root :(.
Specification of explicit target runs into the same problem - the target have to be inside local:CustomControl's visual tree, which can't be reached by the same RelativeSource binding.

UPD2: adding visual and logical trees traversal results:

UPD3: removed old traversal results. Added more precise traversal:

UPD4: (hope this one is final). Traversed visual tree of logical parents:

VisualTree
System.Windows.Controls.Button
System.Windows.Controls.ContentPresenter
System.Windows.Controls.Primitives.ToolBarOverflowPanel inherits from System.Windows.Controls.Panel
    LogicalTree
    System.Windows.Controls.Border
    Microsoft.Windows.Themes.SystemDropShadowChrome inherits from System.Windows.Controls.Decorator
    System.Windows.Controls.Primitives.Popup
    System.Windows.Controls.Grid
    logical root: System.Windows.Controls.Grid
System.Windows.Controls.Border
    LogicalTree
    Microsoft.Windows.Themes.SystemDropShadowChrome inherits from System.Windows.Controls.Decorator
    System.Windows.Controls.Primitives.Popup
    System.Windows.Controls.Grid
    logical root: System.Windows.Controls.Grid
Microsoft.Windows.Themes.SystemDropShadowChrome inherits from System.Windows.Controls.Decorator
    LogicalTree
    System.Windows.Controls.Primitives.Popup
    System.Windows.Controls.Grid
    logical root: System.Windows.Controls.Grid
System.Windows.Documents.NonLogicalAdornerDecorator inherits from System.Windows.Documents.AdornerDecorator
    LogicalTree
    logical root: System.Windows.Controls.Decorator
System.Windows.Controls.Decorator
visual root: System.Windows.Controls.Primitives.PopupRoot inherits from System.Windows.FrameworkElement
    LogicalTree
    System.Windows.Controls.Primitives.Popup
        VisualTree
        System.Windows.Controls.Grid
        System.Windows.Controls.Grid
        here it is: System.Windows.Controls.ToolBar
    System.Windows.Controls.Grid
    logical root: System.Windows.Controls.Grid

Thanks in advance!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

夜空下最亮的亮点 2024-08-17 19:06:14

好的,现在很容易看出这里发生了什么。线索存在于您最初的问题中,但在您发布逻辑树之前,我不清楚您在做什么。

正如我怀疑的那样,您的问题是由于缺乏逻辑继承引起的:在大多数示例中,您将在网上看到 ContentPresenter 将呈现一个 FrameworkElement,它将是 ToolBar 的逻辑后代,因此即使在以下情况下,事件路由和 FindAncestor 也可以工作视觉树被弹出窗口打断。

在您的情况下,不存在逻辑树连接,因为 ContentPresenter 呈现的内容不是 FrameworkElement。

换句话说,这将允许绑定和事件路由甚至在装饰器内部工作:

<Toolbar Width="15">
  <MenuItem .../>
  <MenuItem .../>
</Toolbar>

但这不会:

<Toolbar Width="15">
  <my:NonFrameworkElementObject />
  <my:NonFrameworkElementObject />
</Toolbar>

当然,如果您的项目是 FrameworkElement 派生的,它们可以是 Controls,并且您可以使用 ControlTemplate 而不是 DataTemplate。或者,它们可以是仅呈现其数据项的 ContentPresenter。

如果您在代码中设置 ItemsSource,则这是一个简单的更改。将其替换

MyItems.ItemsSource = ComputeItems();

MyItems.ItemsSource = ComputeItems()
  .Select(item => new ContentPresenter { Content = item });

:如果要在 XAML 中设置 ItemsSource,我通常使用的技术是在我自己的类中创建附加属性(例如“DataItemsSource”)并设置 PropertyChangedCallback,以便在设置 DataItemsSource 时,它执行上面所示的 .Select() 来创建 ContentPresenter 并设置 ItemsSource。重点是:

public class MyItemsSourceHelper ...
{
  ... RegisterAttached("DataItemsSource", ..., new FrameworkPropertyMetadata
  {
    PropertyChangedCallback = (obj, e) =>
    {
      var dataSource = GetDataItemsSource(obj);
      obj.SetValue(ItemsControl.ItemsSource,
        dataSource==null ? null :
        dataSource.Select(item => new ContentPresenter { Content = item });
    }
  }

它将允许这个工作:

<Toolbar Width="15" DataTemplate="..."
  my:MyItemsSourceHelper.DataItemsSource="{Binding myItems}" />

其中 myItems 是 DataTemplate 应用到的非 FrameworkElement 的集合。 (也可以使用 内联列出项目)

另请注意,这种包装数据项的技术假设数据的模板是通过样式应用的,而不是通过样式应用的。通过 ItemsControl.ItemTemplate 属性。如果您确实想通过 ItemsControl.ItemTemplate 应用模板,则您的 ContentPresenter 需要将绑定添加到其 ContentTemplate 属性中,该属性使用 FindAncestor 在 ItemsControl 中查找模板。这是在使用“SetBinding”“new ContentPresenter”之后完成的。

希望这有帮助。

Okay, now it is easy to see what is going on here. The clues where there in your original question but it wasn't obvious to me what you were doing until you posted the logical tree.

As I suspected, your problem is caused by a lack of logical inheritance: In most examples you'll see online the ContentPresenter would be presenting a FrameworkElement which would be a logical descendant of the ToolBar, so it event routing and FindAncestor would work even when the visual tree is interrupted by a popup.

In your case, there is no logical tree connection because the content being presented by the ContentPresenter is not a FrameworkElement.

In other words, this will allow bindings and event routing to work even inside an adorner:

<Toolbar Width="15">
  <MenuItem .../>
  <MenuItem .../>
</Toolbar>

But this won't:

<Toolbar Width="15">
  <my:NonFrameworkElementObject />
  <my:NonFrameworkElementObject />
</Toolbar>

Of course if your items are FrameworkElement-derived, they can be Controls and you can use a ControlTemplate instead of a DataTemplate. Alternatively they can be ContentPresenters that simply present their data items.

If you're setting ItemsSource in code, this is an easy change. Replace this:

MyItems.ItemsSource = ComputeItems();

with this:

MyItems.ItemsSource = ComputeItems()
  .Select(item => new ContentPresenter { Content = item });

If you're setting ItemsSource in XAML, the technique I generally use is to create an attached property (for example, "DataItemsSource") in my own class and set a PropertyChangedCallback so that any time DataItemsSource is set, it does the .Select() shown above to create ContentPresenters and sets ItemsSource. Here's the meat:

public class MyItemsSourceHelper ...
{
  ... RegisterAttached("DataItemsSource", ..., new FrameworkPropertyMetadata
  {
    PropertyChangedCallback = (obj, e) =>
    {
      var dataSource = GetDataItemsSource(obj);
      obj.SetValue(ItemsControl.ItemsSource,
        dataSource==null ? null :
        dataSource.Select(item => new ContentPresenter { Content = item });
    }
  }

which will allow this to work:

<Toolbar Width="15" DataTemplate="..."
  my:MyItemsSourceHelper.DataItemsSource="{Binding myItems}" />

where myItems is a collection of non-FrameworkElements that the DataTemplate applies to. (Listing the items inline is also possible with <Toolbar.DataItemsSource><x:Array ...)

Also note that this technique of wrapping data items assumes your data's template is applied through styles, not through the ItemsControl.ItemTemplate property. If you do want to apply the template through ItemsControl.ItemTemplate, your ContentPresenters need to have a binding added to their ContentTemplate property which uses FindAncestor to find the template in the ItemsControl. This is done after "new ContentPresenter" using "SetBinding".

Hope this helps.

琉璃梦幻 2024-08-17 19:06:14

好吧,ToolBar 的溢出面板似乎有非常奇怪的行为 - 它有测量问题以及随机绑定问题,所以我设计了简单的 CommandsHost 控件,它使用 < code>Popup 以及那里的一切都运行良好。

该控件符合我的要求,请随意根据您的需要进行修改。

这是样式:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:vm="clr-namespace:Company.Product">

  <SolidColorBrush x:Key="PressedCommandButtonBackgroundBrush" Color="#FFDFB700" />
  <SolidColorBrush x:Key="DisabledCommandButtonBackgroundBrush" Color="#FFDDDDDD" />
  <SolidColorBrush x:Key="DisabledForegroundBrush" Color="#FF444444" />
  <SolidColorBrush x:Key="FocusedBorderBrush" Color="#FFFFD700" />

  <ControlTemplate x:Key="PopupButtonTemplate"
                  TargetType="vm:Button">
    <Canvas Margin="{TemplateBinding Padding}" 
             Width="16" 
             Height="16">
      <Ellipse x:Name="Circle"
                  Fill="{TemplateBinding Background}"
                  Canvas.Left="0"
                  Canvas.Top="0"
                  Width="16"
                  Height="16"
                  Stroke="{TemplateBinding BorderBrush}"
                  StrokeThickness="2" />
      <Path x:Name="Arrow" 
               Fill="Transparent"
               Canvas.Left="1"
               Canvas.Top="1"
               Width="14"
               Height="14"
               Stroke="Blue"
               StrokeThickness="1.7"
               StrokeStartLineCap="Round"
               StrokeLineJoin="Miter"
               StrokeEndLineCap="Triangle"
               Data="M 1.904,1.904 L 11.096,11.096 M 4.335,9.284 L 11.096,11.096 M 9.284,4.335 L 11.096,11.096" />
    </Canvas>
    <ControlTemplate.Triggers>
      <Trigger Property="IsMouseOver" Value="True">
        <Setter TargetName="Circle"
                     Property="Fill" Value="{DynamicResource FocusedBorderBrush}" />
      </Trigger>
      <Trigger Property="IsFocused" Value="True">
        <Setter TargetName="Circle"
                     Property="Fill" Value="{DynamicResource FocusedBorderBrush}" />
      </Trigger>
      <Trigger Property="IsPressed" Value="True">
        <Setter TargetName="Circle"
                     Property="Fill" Value="{StaticResource PressedCommandButtonBackgroundBrush}" />
      </Trigger>
      <Trigger Property="IsEnabled" Value="False">
        <Setter TargetName="Circle" 
                     Property="Fill" Value="{StaticResource DisabledCommandButtonBackgroundBrush}" />
        <Setter TargetName="Arrow" 
                     Property="Stroke" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
      </Trigger>
    </ControlTemplate.Triggers>
  </ControlTemplate>

  <Style x:Key="PopupButtonStyle"
        TargetType="vm:Button"
        BasedOn="{StaticResource {x:Type vm:Button}}">
    <Setter Property="Template" Value="{StaticResource PopupButtonTemplate}" />
    <Setter Property="Background" Value="Transparent" />
    <Setter Property="BorderBrush" Value="Black" />
    <Setter Property="Padding" Value="0" />
  </Style>

  <ItemsPanelTemplate x:Key="ItemsPanelTemplate">
    <StackPanel Orientation="Vertical" />
  </ItemsPanelTemplate>

  <DataTemplate x:Key="CommandTemplate"
               DataType="vmc:DescriptedCommand">
    <vm:LinkButton Content="{Binding Text}"
                    Command="{Binding}"
                    ToolTip="{Binding Description}" />
  </DataTemplate>

  <ControlTemplate x:Key="ControlTemplate" 
                  TargetType="vm:CommandsHost">
    <Grid>
      <vm:Button x:Name="Button" 
                    Style="{StaticResource PopupButtonStyle}"
                    Margin="0"
                    Command="{x:Static vm:CommandsHost.OpenPopupCommand}"
                    ToolTip="{TemplateBinding ToolTip}"
                    SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />

      <Popup x:Name="PART_Popup" 
                Placement="Right"
                PlacementTarget="{Binding ElementName=Button}"
                StaysOpen="False"
                IsOpen="{Binding IsOpen, Mode=TwoWay, 
                                 RelativeSource={x:Static RelativeSource.TemplatedParent}}">
        <Border BorderThickness="{TemplateBinding BorderThickness}" 
                     Padding="{TemplateBinding Padding}" 
                     BorderBrush="{TemplateBinding BorderBrush}" 
                     Background="{TemplateBinding Background}" 
                     SnapsToDevicePixels="True">
          <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
        </Border>
      </Popup>
    </Grid>
    <ControlTemplate.Triggers>
      <Trigger Property="ToolTip" Value="{x:Null}">
        <Setter TargetName="Button"
                     Property="ToolTip" 
                     Value="{Binding Command.Description, RelativeSource={x:Static RelativeSource.Self}}" />
      </Trigger>
      <Trigger SourceName="PART_Popup"
                  Property="IsOpen" Value="True">
        <Setter TargetName="Button"
                     Property="Background" 
                     Value="{StaticResource PressedCommandButtonBackgroundBrush}" />
      </Trigger>
      <Trigger Property="HasItems" Value="False">
        <Setter Property="IsEnabled" Value="False" />
      </Trigger>
      <MultiDataTrigger>
        <MultiDataTrigger.Conditions>
          <Condition Binding="{Binding HasItems, 
                                              RelativeSource={x:Static RelativeSource.Self}}" 
                            Value="False" />
          <Condition Binding="{Binding EmptyVisibility,
                                              RelativeSource={x:Static RelativeSource.Self},
                                              Converter={StaticResource NotEqualsConverter},
                                              ConverterParameter={x:Null}}" 
                            Value="True" />
        </MultiDataTrigger.Conditions>
        <Setter Property="Visibility"
                     Value="{Binding EmptyVisibility,
                                     RelativeSource={x:Static RelativeSource.Self}}" />
      </MultiDataTrigger>
    </ControlTemplate.Triggers>
  </ControlTemplate>

  <Style TargetType="vm:CommandsHost"
        BasedOn="{StaticResource {x:Type ItemsControl}}">
    <Setter Property="Template" Value="{StaticResource ControlTemplate}" />
    <Setter Property="ItemsPanel" Value="{StaticResource ItemsPanelTemplate}" />
    <Setter Property="ItemTemplate" Value="{StaticResource CommandTemplate}" />
    <Setter Property="Background" Value="White" />
    <Setter Property="BorderBrush" Value="Black" />
    <Setter Property="BorderThickness" Value="1" />
    <Setter Property="Padding" Value="2" />
    <Setter Property="FontSize" Value="{DynamicResource ReducedFontSize}" />
  </Style>

</ResourceDictionary>

这是逻辑:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Input;
using System.Windows.Controls.Primitives;
using System.Windows.Media;

namespace Company.Product
{
  public class CommandsHost : ItemsControl
  {
    #region Override Metadata for DefaultStyleKey dependency property
             private static readonly object DefaultStyleKeyMetadataOverrider =
                 new Func<object>(
                   delegate
    {
      FrameworkElement.DefaultStyleKeyProperty.OverrideMetadata(
                           typeof(CommandsHost),
                           new FrameworkPropertyMetadata(typeof(CommandsHost)));
      return null;
    })();
    #endregion

             #region Add owner to the Popup.IsOpen dependency property
             public bool IsOpen
    {
      get { return (bool)GetValue(IsOpenProperty); }
      set { SetValue(IsOpenProperty, value); }
    }

    public static readonly DependencyProperty IsOpenProperty =
                       Popup.IsOpenProperty.AddOwner(
                               typeof(CommandsHost),
                               new FrameworkPropertyMetadata(false));
    #endregion

             public static readonly DescriptedCommand OpenPopupCommand =
                 new DescriptedCommand("Options", "Show available options",
                                       "OpenPopup", typeof(CommandsHost));

    #region CommandsHost.OpenPopup class-wide command binding
             private static readonly object CommandsHost_OpenPopupCommandClassBindingRegistrator =
                 new Func<object>(
                   delegate
    {
      CommandManager.RegisterClassCommandBinding(
                           typeof(CommandsHost),
                           new CommandBinding(CommandsHost.OpenPopupCommand, OpenPopup, CanOpenPopup));

      return null;
    })();

    private static void CanOpenPopup(object sender, CanExecuteRoutedEventArgs e)
    {
      if (!(sender is CommandsHost))
        throw new Exception("Internal inconsistency - sender contradicts with corresponding binding");

      var instance = (CommandsHost)sender;

      instance.CanOpenPopup(e);
    }

    private static void OpenPopup(object sender, ExecutedRoutedEventArgs e)
    {
      if (!(sender is CommandsHost))
        throw new Exception("Internal inconsistency - sender contradicts with corresponding binding");

      var instance = (CommandsHost)sender;

      if (!((RoutedCommand)e.Command).CanExecute(e.Parameter, instance))
        throw new Exception("Internal inconsistency - Execute called while CanExecute is false");

      instance.OpenPopup(e);
    }

    #endregion

             #region EmptyVisibility dependency property
             public Visibility? EmptyVisibility
    {
      get { return (Visibility?)GetValue(EmptyVisibilityProperty); }
      set { SetValue(EmptyVisibilityProperty, value); }
    }

    public static readonly DependencyProperty EmptyVisibilityProperty =
                 DependencyProperty.Register(
                               "EmptyVisibility", typeof(Visibility?),
                               typeof(CommandsHost),
                               new FrameworkPropertyMetadata(null));
    #endregion

             public Popup popup;

    protected override void OnTemplateChanged(ControlTemplate oldTemplate, ControlTemplate newTemplate)
    {
      if (popup != null)
      {
        popup.Opened -= popup_Opened;
      }

      popup = null;

      base.OnTemplateChanged(oldTemplate, newTemplate);
    }

    public override void OnApplyTemplate()
    {
      base.OnApplyTemplate();

      popup = Template.FindName("PART_Popup", this) as Popup;
      if (popup != null)
      {
        popup.Opened += popup_Opened;
      }
    }

    private UIElement FindFirstFocusableVisualChild(DependencyObject root)
    {
      if (root is UIElement)
      {
        var ui = (UIElement)root;
        if (ui.Focusable)
          return ui;
      }

      UIElement result = null;
      for (var i = 0; result == null && i < VisualTreeHelper.GetChildrenCount(root); ++i)
      {
        var child = VisualTreeHelper.GetChild(root, i);
        result = FindFirstFocusableVisualChild(child);
      }

      return result;
    }

    void popup_Opened(object sender, EventArgs e)
    {
      var firstItem = ItemsSource.Cast<object>().FirstOrDefault();

      var container = ItemContainerGenerator.ContainerFromItem(firstItem) as ContentPresenter;

      if (container == null)
        return;

      if (container.IsLoaded)
      {
        var focusable = FindFirstFocusableVisualChild(container);
        if (focusable != null)
        {
          focusable.CaptureMouse();
          focusable.Focus();
        }
      }
      else
        container.Loaded +=
                         delegate
      {
        var focusable = FindFirstFocusableVisualChild(container);
        if (focusable != null)
        {
          focusable.CaptureMouse();
          focusable.Focus();
        }
      };
    }

    private void CanOpenPopup(CanExecuteRoutedEventArgs e)
    {
      e.CanExecute = HasItems;
    }

    protected void OpenPopup(ExecutedRoutedEventArgs e)
    {
      if (popup != null)
      {
        popup.IsOpen = true;
      }
    }
  }
}

我希望这会对某人有所帮助。

OK, ToolBar appeared to have very weird behavior with its overflow panel - it have measure issues as well as random binding issues, so I've designed simple CommandsHost control which uses Popup and everything there works great.

This control fits my requirements, feel free to modify it for you needs.

Here is styling:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:vm="clr-namespace:Company.Product">

  <SolidColorBrush x:Key="PressedCommandButtonBackgroundBrush" Color="#FFDFB700" />
  <SolidColorBrush x:Key="DisabledCommandButtonBackgroundBrush" Color="#FFDDDDDD" />
  <SolidColorBrush x:Key="DisabledForegroundBrush" Color="#FF444444" />
  <SolidColorBrush x:Key="FocusedBorderBrush" Color="#FFFFD700" />

  <ControlTemplate x:Key="PopupButtonTemplate"
                  TargetType="vm:Button">
    <Canvas Margin="{TemplateBinding Padding}" 
             Width="16" 
             Height="16">
      <Ellipse x:Name="Circle"
                  Fill="{TemplateBinding Background}"
                  Canvas.Left="0"
                  Canvas.Top="0"
                  Width="16"
                  Height="16"
                  Stroke="{TemplateBinding BorderBrush}"
                  StrokeThickness="2" />
      <Path x:Name="Arrow" 
               Fill="Transparent"
               Canvas.Left="1"
               Canvas.Top="1"
               Width="14"
               Height="14"
               Stroke="Blue"
               StrokeThickness="1.7"
               StrokeStartLineCap="Round"
               StrokeLineJoin="Miter"
               StrokeEndLineCap="Triangle"
               Data="M 1.904,1.904 L 11.096,11.096 M 4.335,9.284 L 11.096,11.096 M 9.284,4.335 L 11.096,11.096" />
    </Canvas>
    <ControlTemplate.Triggers>
      <Trigger Property="IsMouseOver" Value="True">
        <Setter TargetName="Circle"
                     Property="Fill" Value="{DynamicResource FocusedBorderBrush}" />
      </Trigger>
      <Trigger Property="IsFocused" Value="True">
        <Setter TargetName="Circle"
                     Property="Fill" Value="{DynamicResource FocusedBorderBrush}" />
      </Trigger>
      <Trigger Property="IsPressed" Value="True">
        <Setter TargetName="Circle"
                     Property="Fill" Value="{StaticResource PressedCommandButtonBackgroundBrush}" />
      </Trigger>
      <Trigger Property="IsEnabled" Value="False">
        <Setter TargetName="Circle" 
                     Property="Fill" Value="{StaticResource DisabledCommandButtonBackgroundBrush}" />
        <Setter TargetName="Arrow" 
                     Property="Stroke" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
      </Trigger>
    </ControlTemplate.Triggers>
  </ControlTemplate>

  <Style x:Key="PopupButtonStyle"
        TargetType="vm:Button"
        BasedOn="{StaticResource {x:Type vm:Button}}">
    <Setter Property="Template" Value="{StaticResource PopupButtonTemplate}" />
    <Setter Property="Background" Value="Transparent" />
    <Setter Property="BorderBrush" Value="Black" />
    <Setter Property="Padding" Value="0" />
  </Style>

  <ItemsPanelTemplate x:Key="ItemsPanelTemplate">
    <StackPanel Orientation="Vertical" />
  </ItemsPanelTemplate>

  <DataTemplate x:Key="CommandTemplate"
               DataType="vmc:DescriptedCommand">
    <vm:LinkButton Content="{Binding Text}"
                    Command="{Binding}"
                    ToolTip="{Binding Description}" />
  </DataTemplate>

  <ControlTemplate x:Key="ControlTemplate" 
                  TargetType="vm:CommandsHost">
    <Grid>
      <vm:Button x:Name="Button" 
                    Style="{StaticResource PopupButtonStyle}"
                    Margin="0"
                    Command="{x:Static vm:CommandsHost.OpenPopupCommand}"
                    ToolTip="{TemplateBinding ToolTip}"
                    SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />

      <Popup x:Name="PART_Popup" 
                Placement="Right"
                PlacementTarget="{Binding ElementName=Button}"
                StaysOpen="False"
                IsOpen="{Binding IsOpen, Mode=TwoWay, 
                                 RelativeSource={x:Static RelativeSource.TemplatedParent}}">
        <Border BorderThickness="{TemplateBinding BorderThickness}" 
                     Padding="{TemplateBinding Padding}" 
                     BorderBrush="{TemplateBinding BorderBrush}" 
                     Background="{TemplateBinding Background}" 
                     SnapsToDevicePixels="True">
          <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
        </Border>
      </Popup>
    </Grid>
    <ControlTemplate.Triggers>
      <Trigger Property="ToolTip" Value="{x:Null}">
        <Setter TargetName="Button"
                     Property="ToolTip" 
                     Value="{Binding Command.Description, RelativeSource={x:Static RelativeSource.Self}}" />
      </Trigger>
      <Trigger SourceName="PART_Popup"
                  Property="IsOpen" Value="True">
        <Setter TargetName="Button"
                     Property="Background" 
                     Value="{StaticResource PressedCommandButtonBackgroundBrush}" />
      </Trigger>
      <Trigger Property="HasItems" Value="False">
        <Setter Property="IsEnabled" Value="False" />
      </Trigger>
      <MultiDataTrigger>
        <MultiDataTrigger.Conditions>
          <Condition Binding="{Binding HasItems, 
                                              RelativeSource={x:Static RelativeSource.Self}}" 
                            Value="False" />
          <Condition Binding="{Binding EmptyVisibility,
                                              RelativeSource={x:Static RelativeSource.Self},
                                              Converter={StaticResource NotEqualsConverter},
                                              ConverterParameter={x:Null}}" 
                            Value="True" />
        </MultiDataTrigger.Conditions>
        <Setter Property="Visibility"
                     Value="{Binding EmptyVisibility,
                                     RelativeSource={x:Static RelativeSource.Self}}" />
      </MultiDataTrigger>
    </ControlTemplate.Triggers>
  </ControlTemplate>

  <Style TargetType="vm:CommandsHost"
        BasedOn="{StaticResource {x:Type ItemsControl}}">
    <Setter Property="Template" Value="{StaticResource ControlTemplate}" />
    <Setter Property="ItemsPanel" Value="{StaticResource ItemsPanelTemplate}" />
    <Setter Property="ItemTemplate" Value="{StaticResource CommandTemplate}" />
    <Setter Property="Background" Value="White" />
    <Setter Property="BorderBrush" Value="Black" />
    <Setter Property="BorderThickness" Value="1" />
    <Setter Property="Padding" Value="2" />
    <Setter Property="FontSize" Value="{DynamicResource ReducedFontSize}" />
  </Style>

</ResourceDictionary>

Here is the logic:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Input;
using System.Windows.Controls.Primitives;
using System.Windows.Media;

namespace Company.Product
{
  public class CommandsHost : ItemsControl
  {
    #region Override Metadata for DefaultStyleKey dependency property
             private static readonly object DefaultStyleKeyMetadataOverrider =
                 new Func<object>(
                   delegate
    {
      FrameworkElement.DefaultStyleKeyProperty.OverrideMetadata(
                           typeof(CommandsHost),
                           new FrameworkPropertyMetadata(typeof(CommandsHost)));
      return null;
    })();
    #endregion

             #region Add owner to the Popup.IsOpen dependency property
             public bool IsOpen
    {
      get { return (bool)GetValue(IsOpenProperty); }
      set { SetValue(IsOpenProperty, value); }
    }

    public static readonly DependencyProperty IsOpenProperty =
                       Popup.IsOpenProperty.AddOwner(
                               typeof(CommandsHost),
                               new FrameworkPropertyMetadata(false));
    #endregion

             public static readonly DescriptedCommand OpenPopupCommand =
                 new DescriptedCommand("Options", "Show available options",
                                       "OpenPopup", typeof(CommandsHost));

    #region CommandsHost.OpenPopup class-wide command binding
             private static readonly object CommandsHost_OpenPopupCommandClassBindingRegistrator =
                 new Func<object>(
                   delegate
    {
      CommandManager.RegisterClassCommandBinding(
                           typeof(CommandsHost),
                           new CommandBinding(CommandsHost.OpenPopupCommand, OpenPopup, CanOpenPopup));

      return null;
    })();

    private static void CanOpenPopup(object sender, CanExecuteRoutedEventArgs e)
    {
      if (!(sender is CommandsHost))
        throw new Exception("Internal inconsistency - sender contradicts with corresponding binding");

      var instance = (CommandsHost)sender;

      instance.CanOpenPopup(e);
    }

    private static void OpenPopup(object sender, ExecutedRoutedEventArgs e)
    {
      if (!(sender is CommandsHost))
        throw new Exception("Internal inconsistency - sender contradicts with corresponding binding");

      var instance = (CommandsHost)sender;

      if (!((RoutedCommand)e.Command).CanExecute(e.Parameter, instance))
        throw new Exception("Internal inconsistency - Execute called while CanExecute is false");

      instance.OpenPopup(e);
    }

    #endregion

             #region EmptyVisibility dependency property
             public Visibility? EmptyVisibility
    {
      get { return (Visibility?)GetValue(EmptyVisibilityProperty); }
      set { SetValue(EmptyVisibilityProperty, value); }
    }

    public static readonly DependencyProperty EmptyVisibilityProperty =
                 DependencyProperty.Register(
                               "EmptyVisibility", typeof(Visibility?),
                               typeof(CommandsHost),
                               new FrameworkPropertyMetadata(null));
    #endregion

             public Popup popup;

    protected override void OnTemplateChanged(ControlTemplate oldTemplate, ControlTemplate newTemplate)
    {
      if (popup != null)
      {
        popup.Opened -= popup_Opened;
      }

      popup = null;

      base.OnTemplateChanged(oldTemplate, newTemplate);
    }

    public override void OnApplyTemplate()
    {
      base.OnApplyTemplate();

      popup = Template.FindName("PART_Popup", this) as Popup;
      if (popup != null)
      {
        popup.Opened += popup_Opened;
      }
    }

    private UIElement FindFirstFocusableVisualChild(DependencyObject root)
    {
      if (root is UIElement)
      {
        var ui = (UIElement)root;
        if (ui.Focusable)
          return ui;
      }

      UIElement result = null;
      for (var i = 0; result == null && i < VisualTreeHelper.GetChildrenCount(root); ++i)
      {
        var child = VisualTreeHelper.GetChild(root, i);
        result = FindFirstFocusableVisualChild(child);
      }

      return result;
    }

    void popup_Opened(object sender, EventArgs e)
    {
      var firstItem = ItemsSource.Cast<object>().FirstOrDefault();

      var container = ItemContainerGenerator.ContainerFromItem(firstItem) as ContentPresenter;

      if (container == null)
        return;

      if (container.IsLoaded)
      {
        var focusable = FindFirstFocusableVisualChild(container);
        if (focusable != null)
        {
          focusable.CaptureMouse();
          focusable.Focus();
        }
      }
      else
        container.Loaded +=
                         delegate
      {
        var focusable = FindFirstFocusableVisualChild(container);
        if (focusable != null)
        {
          focusable.CaptureMouse();
          focusable.Focus();
        }
      };
    }

    private void CanOpenPopup(CanExecuteRoutedEventArgs e)
    {
      e.CanExecute = HasItems;
    }

    protected void OpenPopup(ExecutedRoutedEventArgs e)
    {
      if (popup != null)
      {
        popup.IsOpen = true;
      }
    }
  }
}

I hope this will help somebody.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文