views:

739

answers:

2

Hi there!

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!

A: 

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.

Ray Burns
Simple example: `<ToolBar Width="15"><Button Content="{Binding Title, RelativeSource={RelativeSource AncestorType=Window}}" /></ToolBar>` - bindings still not working. I'm very sorry for misleading you - command routing as well as event routing worked great without any explicit ContentPresenter wrappers...
archimed7592
I pasted your code `<ToolBar Width="15"><Button Content="{Binding Title, RelativeSource={RelativeSource AncestorType=Window}}" /></ToolBar>` into a brand new WPF application and it worked fine, ie. the toobar went into overflow and the button in the popup was labeled "Window1". Does this work for you?
Ray Burns
OK, I've slightly localized the problem. First of all, when I insert the same code into brand new WPF application - yes, it works. Secondly, when I remove the Button from the ToolBar, then define ItemTemplate `Double -> <Button Content="{Binding Title, RelSource=Window}" FontSize="{Binding}" />` and after all I add ItemsSource **inside the code** as `new [] { 20.0, 40.0 }` - it works too. Then, when I replace ItemsSource initialization to the x:Array extension - Binding stops working. It is first very weird thing. Oops, characters limit.
archimed7592
Second weird thing. Let us fall back to the place where we just inserted toolbar with button in it into brand new WPF application and it worked. Guess what the behavior will display when you add one RowDefinition or ColumnDefinition, or both? The Button will be empty. But when you explicitly specify height on the toolbar or RowDefinition the Button will contain window title.
archimed7592
Ray, thanks for your assistance. It was very helpful in figuring out what's wrong with the World :).
archimed7592
A: 

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:

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

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

archimed7592