English 中文(简体)
Bind to ancestor of adorned element
原标题:

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!

最佳回答

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.

问题回答

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.





相关问题
WPF convert 2d mouse click into 3d space

I have several geometry meshes in my Viewport3D, these have bounds of (w:1800, h:500, d:25). When a user clicks in the middle of the mesh, I want the Point3D of (900, 500, 25)... How can I achieve ...

Editing a xaml icons or images

Is it possible to edit a xaml icons or images in the expression design or using other tools? Is it possible to import a xaml images (that e.g you have exported) in the expression designer for editing?...

WPF: writing smoke tests using ViewModels

I am considering to write smoke tests for our WPF application. The question that I am faced is: should we use UI automation( or some other technology that creates a UI script), or is it good enough to ...

WPF - MVVM - NHibernate Validation

Im facing a bit of an issue when trying to validate a decimal property on domain object which is bound to a textbox on the view through the viewmodel. I am using NHibernate to decorate my property on ...

How do WPF Markup Extensions raise compile errors?

Certain markup extensions raise compile errors. For example StaticExtension (x:Static) raises a compile error if the referenced class cannot be found. Anyone know the mechanism for this? Is it baked ...

WPF design-time context menu

I am trying to create a custom wpf control, I m wondering how I can add some design-time features. I ve googled and can t seem to get to my goal. So here s my simple question, how can I add an entry ...

How to combine DataTrigger and EventTrigger?

NOTE I have asked the related question (with an accepted answer): How to combine DataTrigger and Trigger? I think I need to combine an EventTrigger and a DataTrigger to achieve what I m after: when ...

热门标签