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();
}
}
}
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"
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.