Привязать к предку украшенного элемента - PullRequest
3 голосов
/ 14 ноября 2009

Вот случай:

<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>

В нескольких словах: когда какой-то элемент находится внутри рекламного элемента, вы не можете просто использовать свойство RelativeSource объекта Binding для доступа к элементам внутри украшенного визуального дерева.

Я уже привык сталкиваться с той же проблемой с ToolTip, когда мне нужно было привязать его FontSize к владельцу подсказки FontSize - там было очень удобное свойство 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

Заранее спасибо!

Ответы [ 2 ]

0 голосов
/ 15 ноября 2009

ОК, 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;
      }
    }
  }
}

Надеюсь, это кому-нибудь поможет.

0 голосов
/ 15 ноября 2009

Хорошо, теперь легко увидеть, что здесь происходит. Подсказки были в твоем исходном вопросе, но мне было не очевидно, что ты делал, пока не опубликовал логическое дерево.

Как я и подозревал, ваша проблема вызвана отсутствием логического наследования: в большинстве примеров вы увидите, что в Интернете 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 });

Если вы устанавливаете ItemsSource в XAML, метод, который я обычно использую, заключается в создании присоединенного свойства (например, «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 - это набор не-1027 *, к которым применяется DataTemplate. (Перечисление элементов в строке также возможно с <Toolbar.DataItemsSource><x:Array ...)

Также обратите внимание, что этот метод обертывания элементов данных предполагает, что шаблон ваших данных применяется через стили, а не через ItemsControl.ItemTemplate property. Если вы хотите применить шаблон через ItemsControl.ItemTemplate, вашим ContentPresenters необходимо добавить привязку к их свойству ContentTemplate, которое использует FindAncestor для поиска шаблона в ItemsControl. Это делается после «нового ContentPresenter» с использованием «SetBinding».

Надеюсь, это поможет.

...