14

I have a collection of Visuals in a ListBox. I need to find the XPosition of an element inside it and then animate the HorizontalOffset of the ListBox's ScrollViewer. Essentially I want to created an animated ScrollIntoView method.

This gives me a couple of problems. Firstly, how can I get a reference to the ListBoxs scrollviewer? Secondly, how can i get the relative XPosition or HozintalOfffset of an arbitrary element in the ListBox?

I'm not reponding to any input on the ListBox itself so I can't use Mouse related properties.

WendiKidd
  • 4,333
  • 4
  • 33
  • 50
EightyOne Unite
  • 11,665
  • 14
  • 79
  • 105

2 Answers2

33

I don't think you will be able to use a WPF storyboard for the animation because storyboards animate WPF dependency properties. You will need to call ScrollViewer.ScrollToHorizontalOffset(double) to scroll.

You could try creating a custom dependency property that calls SetHorizontalOffset in the OnDependencyPropertyChanged() function. Then you could animate this property.

public static readonly DependencyProperty ScrollOffsetProperty =
   DependencyProperty.Register("ScrollOffset", typeof(double), typeof(YOUR_TYPE),
   new FrameworkPropertyMetadata(0.0, new PropertyChangedCallback(OnScrollOffsetChanged)));


public double ScrollOffset
{
   get { return (double)GetValue(ScrollOffsetProperty); }
   set { SetValue(ScrollOffsetProperty, value); }
}

private static void OnScrollOffsetChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
   YOUR_TYPE myObj = obj as YOUR_TYPE;

   if (myObj != null)
      myObj.SCROLL_VIEWER.ScrollToHorizontalOffset(myObj.ScrollOffset);
}

To get the scroll viewer you can use the VisualTreeHelper to search the visual children of the ListBox. Save a reference to the ScrollViewer because you will need it later. Try this:

public static childItem FindVisualChild<childItem>(DependencyObject obj)
   where childItem : DependencyObject
{
   // Iterate through all immediate children
   for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
   {
      DependencyObject child = VisualTreeHelper.GetChild(obj, i);

      if (child != null && child is childItem)
         return (childItem)child;

      else
      {
         childItem childOfChild = FindVisualChild<childItem>(child);

         if (childOfChild != null)
            return childOfChild;
      }
   }

   return null;
}

This function returns the first visual child of the parameter type. Call FindVisualChild<ScrollViewer>(ListBox) to get the ScrollViewer.

Finally, try using UIElement.TranslatePoint(Point, UIElement) to get the X position of the item. Call this function on the item, pass in 0,0 for the point, and pass in the ScrollViewer.

Hope this helps.

Josh G
  • 14,068
  • 7
  • 62
  • 74
  • My Lord, that's quite a job! Thanks for the help Josh, it's pointed me in the right direction at least. – EightyOne Unite Mar 20 '09 at 12:27
  • Do you mean ScrollToHorizontalOffset instead of SetHorizontalOffset? – Ashley Davis Aug 04 '09 at 08:39
  • Great idea. It helped me out! – John Chuckran Dec 17 '09 at 23:01
  • It's possible that this property could be an attached DependencyProperty. That would allow this code to be reused on any control that has a ScrollView in it's visual tree. – Josh G Dec 28 '09 at 13:49
  • You actually want to find the ItemsPresenter when translating the position of the item. e.g. var itemContainer = listBox.ItemContainerGenerator.ContainerFromItem(listBox.SelectedItem); itemContainer.TranslatePoint(new Point(0, 0), FindVisualChild(scrollViewer)); – Flatliner DOA May 11 '10 at 00:54
  • Thanks a lot to Josh! I wrote a behavior that listens for the PreviewKeyDown-Event on a ListBox and if the user pressed Ctrl+Key.Left or Ctrl+Key.Right the ScrollViewer of the ListBox should scroll to start or end. Finding the ScrollViewer worked like a charm with the VisualTreeHelper solution. – toolsche Mar 15 '11 at 09:28
  • FindVisualChild method implements classical depth-first search; the comment is misleading. – bohdan_trotsenko Mar 10 '15 at 20:25
  • That is correct. The point of the comment, however, was to clarify that the outer loop iterates through all of the item's children. Updated to remove the confusion about breadth-first since the recursive algorithm clearly is depth-first. – Josh G Mar 11 '15 at 12:36
1

I'm not sure if my method is good practice but for the limited time I had it seemed to work okay. Instead of using a story board I just used a DispatcherTimer instead.

ScrollLeftButtonCommand = new DelegateCommand(
    o =>
       {
           var scrollViewer = (ScrollViewer)o;

           scrollTimer = new DispatcherTimer();

           scrollTimer.Start();

           scrollTimer.Interval = TimeSpan.FromMilliseconds(30);

           scrollTimer.Tick += (s, e) =>
           {          
               scrollViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset - 50);

               if (scrollViewer.HorizontalOffset <= 0)
               {
                   scrollTimer.Stop();
               }
           };
       });

Make sure it's a DispatchTimer so the thread is able to take control of the UI element

Also remember to bind to your object in your view!

<Button CommandParameter="{Binding ElementName=MyScrollViewer }"
        Command="{Binding ScrollLeftButtonCommand }"/>
RagedMilkMan
  • 425
  • 5
  • 16