How to get ItemsControl scrollbar position programmatically?
Asked Answered
S

2

0

I have an attached behavior to an ItemsControl that scrolls down to the bottom whenever a new item is added. Since I am working on a chat type program, I don't want it to scroll if the user has the scrollbar anywhere other than the very bottom as that would be very annoying otherwise(Some chat programs do this and it's awful).

How do I accomplish this? I don't know how to access the wrapping ScrollViewer, or otherwise figure out if I need to bring it into view or not.

This is the behavior class that I actually got from someone on StackOverflow. I'm still learning about behaviors myself.

public class ScrollOnNewItem : Behavior<ItemsControl>
{
    protected override void OnAttached()
    {
        AssociatedObject.Loaded += OnLoaded;
        AssociatedObject.Unloaded += OnUnLoaded;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.Loaded -= OnLoaded;
        AssociatedObject.Unloaded -= OnUnLoaded;
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
        if (incc == null) return;

        incc.CollectionChanged += OnCollectionChanged;
    }

    private void OnUnLoaded(object sender, RoutedEventArgs e)
    {
        var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
        if (incc == null) return;

        incc.CollectionChanged -= OnCollectionChanged;
    }

    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            int count = AssociatedObject.Items.Count;
            if (count == 0)
                return;

            var item = AssociatedObject.Items[count - 1];

            var frameworkElement = AssociatedObject.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
            if (frameworkElement == null) return;

            frameworkElement.BringIntoView();
        }
    }
}
Stately answered 3/9, 2012 at 23:28 Comment(0)
S
3

Okay, here's the answer I came up with for myself.

I figured out that there is a GetSelfAndAncestors method for dependency objects. Using that, I am able to get the ScrollViewer ancestor(if there is one) of my AssociatedObject(the ItemsControl) and manipulate it with that.

So I added this field to my behavior

private ScrollViewer scrollViewer;
private bool isScrollDownEnabled;

And in the OnLoaded event handler I assigned it with the following code

scrollViewer = AssociatedObject.GetSelfAndAncestors().Where(a => a.GetType().Equals(typeof(ScrollViewer))).FirstOrDefault() as ScrollViewer;

And in the OnCollectionChanged event handler, I went ahead and wrapped all the logic in an if statement as follows

        if (scrollViewer != null)
        {
            isScrollDownEnabled = scrollViewer.ScrollableHeight > 0 && scrollViewer.VerticalOffset + scrollViewer.ViewportHeight < scrollViewer.ExtentHeight;

            if (e.Action == NotifyCollectionChangedAction.Add && !isScrollDownEnabled)
            {
                 // Do stuff
            }
        }

So all together, the code looks like the following

public class ScrollOnNewItem : Behavior<ItemsControl>
{
    private ScrollViewer scrollViewer;
    private bool isScrollDownEnabled;

    protected override void OnAttached()
    {
        AssociatedObject.Loaded += OnLoaded;
        AssociatedObject.Unloaded += OnUnLoaded;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.Loaded -= OnLoaded;
        AssociatedObject.Unloaded -= OnUnLoaded;
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
        if (incc == null) return;

        incc.CollectionChanged += OnCollectionChanged;
        scrollViewer = AssociatedObject.GetSelfAndAncestors().Where(a => a.GetType().Equals(typeof(ScrollViewer))).FirstOrDefault() as ScrollViewer;
    }

    private void OnUnLoaded(object sender, RoutedEventArgs e)
    {
        var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
        if (incc == null) return;

        incc.CollectionChanged -= OnCollectionChanged;
    }

    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (scrollViewer != null)
        {
            isScrollDownEnabled = scrollViewer.ScrollableHeight > 0 && scrollViewer.VerticalOffset + scrollViewer.ViewportHeight < scrollViewer.ExtentHeight;

            if (e.Action == NotifyCollectionChangedAction.Add && !isScrollDownEnabled)
            {
                int count = AssociatedObject.Items.Count;
                if (count == 0)
                    return;

                var item = AssociatedObject.Items[count - 1];

                var frameworkElement = AssociatedObject.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
                if (frameworkElement == null) return;

                frameworkElement.BringIntoView();
            }
        }
    }
}

As asked in the comments, to use a behavior, I just need to add a new xmlns to my xaml file of the area in code that contains my behavior.

             xmlns:behaviors="clr-namespace:Infrastructure.Behaviors;assembly=Infrastructure"

Then on the control I just add on the behavior.

    <ScrollViewer VerticalScrollBarVisibility="Auto">
        <ItemsControl Name="Blah" ItemsSource="{Binding Messages}" ItemTemplate="{StaticResource MessageTemplate}">
            <i:Interaction.Behaviors>
                <behaviors:ScrollOnNewItem />
            </i:Interaction.Behaviors>
        </ItemsControl>
    </ScrollViewer>

The i class is just the interactivity namespace. xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

Stately answered 4/9, 2012 at 0:17 Comment(3)
Would be better if you provide the code how to use this class!May
I went ahead and edited the bottom to show how to use it. It's just a matter of adding a behavior to the item in question in the view.Stately
This is a rather bad implementation in that placing the ItemsControl inside of a ScrollViewer like this has significant performance implications. I wouldn't do this unless you know your list will only have a handful of items in it.Volscian
M
1

There is another way to implement this behavior. This way is easier than above. All you should do is invoke a method like below:

public void AppendText(RichTextBox richTextBox, string data){       
   richTextBox.AppendText(data);
   bool isScrollDownEnabled = richTextBox.VerticalOffset == 0 ||
        richTextBox.VerticalOffset + richTextBox.ViewportHeight == richTextBox.ExtentHeight;
   if (isScrollDownEnabled)
       richTextBox.ScrollToEnd();
}

It is suitable for TextBox too.

May answered 24/1, 2013 at 2:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.