Search code examples
wpfdata-bindingxamladorner

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!


Solution

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