3

情况如下:

<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 属性,我不需要在树内查找 - 绑定看起来像这样:<Binding PlacementTarget.FontSize />

这是几乎相同的问题 - 当项目在 ToolBarOverflowPanel 内时,它似乎在装饰器内,因此 RelativeSource 显然无法绑定。

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

UPD:最不幸的副作用是命令没有达到预期的目标 - 通过冒泡机制的命令传播在装饰者的视觉根处停止:(。明确目标的规范遇到同样的问题 - 目标必须在local:CustomControl视觉树内,无法通过相同的 RelativeSource 绑定来访问。

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

提前致谢!

4

2 回答 2

0

好的,ToolBar它的溢出面板似乎有非常奇怪的行为 - 它有测量问题以及随机绑定问题,所以我设计了简单的CommandsHost控件,它使用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;
      }
    }
  }
}

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

于 2009-11-15T18:28:42.513 回答
0

好的,现在很容易看到这里发生了什么。您原始问题中的线索,但在您发布逻辑树之前,您在做什么对我来说并不明显。

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

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

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

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

但这不会:

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

当然,如果您的项目是 FrameworkElement 派生的,它们可以是控件,并且您可以使用 ControlTemplate 而不是 DataTemplate。或者,它们可以是 ContentPresenters,只是简单地展示他们的数据项。

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

MyItems.ItemsSource = ComputeItems();

有了这个:

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

如果您在 XAML 中设置 ItemsSource,我通常使用的技术是在我自己的类中创建一个附加属性(例如,“DataItemsSource”)并设置一个 PropertyChangedCallback,以便任何时候设置 DataItemsSource,它都会执行 .Select( ) 如上所示创建 ContentPresenters 并设置 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 是适用于的非FrameworkElements的集合。DataTemplate(也可以使用 列出内联项目<Toolbar.DataItemsSource><x:Array ...

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

希望这可以帮助。

于 2009-11-15T01:25:04.273 回答