Search code examples
wpfbindinglistboxscrollviewerdatatrigger

How to bind a DataTrigger to the ComputedVerticalScrollBarVisibility property of a separate ListBox ScrollViewer?


I want to adjust the width or visibility of a Border or Margin that is placed above a separate ListBox if the vertical scroll bar of the ListBox is collapsed.

I am trying something like this, but cannot figure out how to get to the ListBox ScrollViewer. Obviously, the Path in the DataTrigger is not correct.

<Border Width={Binding Source={x:Static SystemParameters.ScrollWidth}}">
   <Border.Style>
      <Style>
         <Setter Property="Border.Visibility" Value="Visible"/>
         <Style.Triggers>
            <DataTrigger Binding="{Binding ElementName=listBox, 
                         Path=***ScrollViewer.ComputedVerticalScrollBarVisibility***}"
                         Value="Collapsed">
               <Setter Property="Border.Visibility" Value="Collapsed"/>
            </DataTrigger>
         </Style.Triggers>
      </Style>
   </Border.Style>
</Border>

<ListBox Name="listBox" ItemsSource="{Binding MyItems}"/>

Is there a way to get to that ListBox property? If not, is there a better way to solve this problem?

Thanks much!


Solution

  • The workaround of wrapping the ListBox in another ScrollViewer allows an all-XAML solution at the expense of a redundant ScrollViewer control (see comment in the question). In the end, my team-mate decided to go with a code-behind solution as follows.

    Here are the relevant attributes for the ListBox that has the ScrollViewer we need to access.

    <ListBox Name="_listBox" ScrollViewer.HorizontalScrollBarVisibility="Disabled"
             Loaded="InitializeListBoxScrollViewerProperty"/>
    

    Here is the code-behind to expose the scroll viewer for use by external controls.

    private static readonly DependencyPropertyKey ListBoxScrollViewerPropertyKey =
       DependencyProperty.RegisterReadOnly("ListBoxScrollViewer", typeof(ScrollViewer),
       typeof(MyEditView), new PropertyMetadata());
    
    protected static readonly DependencyProperty ListBoxScrollViewerProperty =
       ListBoxScrollViewerPropertyKey.DependencyProperty;
    
    protected ScrollViewer ListBoxScrollViewer
    {
       get { return (ScrollViewer)GetValue(ListBoxScrollViewerProperty); }
       private set { SetValue(ListBoxScrollViewerPropertyKey, value); }
    }
    
    private void InitializeListBoxScrollViewerProperty(object sender, RoutedEventArgs args)
    {
       if (ReferenceEquals(args.OriginalSource, _listBox))
       {
          var scrollViewer = _listBox.GetFirstDescendantBreadthFirst<ScrollViewer>();
          if (scrollViewer != null)
          {
             ListBoxScrollViewer = scrollViewer;
          }
       }
    }
    

    Here are the extension methods used:

    public static class DependencyObjectExtensions
    {
       public static TDescendant GetFirstDescendantBreadthFirst<TDescendant>
          (this DependencyObject dependencyObject) where TDescendant : DependencyObject
       {
          if (dependencyObject == null) { throw new ArgumentNullException(); }
          return GetFirstDescendantBreadthFirst<TDescendant>(GetAllChildren(dependencyObject));
       }
    
       private static TDescendant GetFirstDescendantBreadthFirst<TDescendant>
          (IEnumerable<DependencyObject> descendants) where TDescendant : DependencyObject
       {
          if (!descendants.Any()) return null;
          var descendant = descendants.OfType<TDescendant>().FirstOrDefault();
          if (descendant != null) return descendant;
          return GetFirstDescendantBreadthFirst<TDescendant>(descendants.SelectMany(GetAllChildren));
       }
    
       private static IEnumerable<DependencyObject> GetAllChildren(DependencyObject dependencyObject)
       {
          return Enumerable
             .Range(0, VisualTreeHelper.GetChildrenCount(dependencyObject))
             .Select(i => VisualTreeHelper.GetChild(dependencyObject, i));
       }
    }
    

    And finally, the scroll viewer can be accessed in another part of the XAML.

    <Grid>
       <Grid.Style>
          <Style TargetType="Grid">
             <Style.Triggers>
                <DataTrigger Binding="{Binding ListBoxScrollViewer.ComputedVerticalScrollBarVisibility,
                   RelativeSource={RelativeSource AncestorType=l:MyEditView}}" Value="Visible">
                   <Setter Property="Margin" Value="{StaticResource myWidenedMargin}"/>
                </DataTrigger>
             </Style.Triggers>
          </Style>
       </Grid.Style>
    </Grid>