2009-11-14 1 views
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> 

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

Я уже использовал, чтобы столкнуться с той же проблемой с ToolTip, когда мне нужно было связать свой FontSize с владельцем инструментального средства FontSize - было очень удобное свойство PlacementTarget, и мне не нужно было искать внутри дерева - привязка выглядела так: <Binding PlacementTarget.FontSize />

Здесь почти та же проблема - когда элемент находится внутри ToolBarOverflowPanel, он оказывается внутри adorner, поэтому RelativeSource, очевидно, не удается связать.

Вопрос в том, как решить эту сложную проблему? Мне действительно нужно привязываться к свойству контейнера. Даже если бы я мог привязываться к украшенному элементу, также остается длинный путь к предку.

UPD: самый неприятный побочный эффект в том, что команда не достигают намеченной цели - распространения команд через механизм барботирования останавливается при визуальном корня Adorner в :( Спецификация явной цели работает в одной и той же задачи - цель есть. чтобы быть внутри визуального дерева local:CustomControl «s, которое не может быть достигнуто с помощью одной и той же 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 

Заранее благодарен!

+1

Поскольку маршрутизация событий не работает, вы можете подумать о том, что ваши отдельные кнопки TooBar не являются частью вашего основного логического дерева. Это, конечно же, повлияет на наследование DataContext так, как вы описываете. Поэтому я бы разбил программу в отладчике и проверил логическое дерево (у меня есть класс-оболочка для этого, который предназначен для использования с браузером VS.NET, или вы можете использовать Mole). –

+0

Спасибо за совет, но я уже сделал это, прежде чем задавать вопрос. Причина, по которой я не упоминал обход, - это то, что я не нашел там ничего полезного. Я обновил свой вопрос с результатами обхода. – archimed7592

+0

ОК, я в конце концов обнаружил, что ToolBar пересекает сначала визуальное дерево с Button, затем логическое дерево от найденного визуального предка PopupRoot и, наконец, визуальное дерево от найденного логического предка Popup. Проблема все еще сохраняется - как мы привязываемся к тому, что доступно странным образом предкам. – archimed7592

ответ

0

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

Как я подозревал, ваша проблема вызвана отсутствием логического наследования. В большинстве примеров вы увидите онлайн ContentPresenter будет представлять FrameworkElement, который будет логическим потомком ToolBar, поэтому он маршрутизирует события и FindAncestor будет работать даже тогда, когда визуальное дерево прерывается всплывающим окном.

В вашем случае нет логического древовидного соединения, потому что контент, предоставляемый ContentPresenter, не является элементом FrameworkElement.

Другими словами, это позволит привязок и маршрутизации событий работать даже внутри Adorner:

<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 является сбор не- FrameworkElement s, что DataTemplate относится. (Перечисление позиций inline также возможно с помощью <Toolbar.DataItemsSource><x:Array ...)

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

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

+0

Простой пример: '

+0

Я вставил ваш код '

+0

Хорошо, я немного локализовал проблему. Прежде всего, когда я вставляю один и тот же код в новое приложение WPF - да, он работает. Во-вторых, когда я удаляю Button из ToolBar, тогда определите ItemTemplate 'Double ->

0

OK, 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; 
     } 
    } 
    } 
} 

Я надеюсь, что это поможет кому-то.