Automatically focus on the newest item in a ListView using WinRT, MVVM and Behaviors SDK

The Behaviors SDK was introduced recently for WinRT. Here’s how you could use it to add functionality to a Listview to scroll to the newest item.

ListView

To start with, here’s the listview XAML:

            <ListView ItemsSource="{Binding DataLog}" x:Name="StatusUpdate">                
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding}" />
                    </DataTemplate>
                </ListView.ItemTemplate>

            </ListView>

And here’s the ViewModel that updates it:

        private void UpdateStatusLog(string msg)
        {
            Log.Add(msg);
        }

If you add to the listview, what you should notice is that once you’ve filled it, you’ll start to add items off the bottom.

Behaviors (sic)

The Behaviors SDK is now available for WinRT. Once you’ve included it in your project, you get a number of build in behaviours and, you can create your own. Before you build your own, a base class will probably make you feel more at home (Reference):

    public abstract class Behaviour<T> : DependencyObject, IBehavior where T : DependencyObject
    {

        [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
        public T AssociatedObject { get; set; }

        protected virtual void OnAttached()
        {
        }

        protected virtual void OnDetaching()
        {
        }

        public void Attach(Windows.UI.Xaml.DependencyObject associatedObject)
        {
            this.AssociatedObject = (T)associatedObject;
            OnAttached();
        }

        public void Detach()
        {
            OnDetaching();
        }

        DependencyObject IBehavior.AssociatedObject
        {
            get { return this.AssociatedObject; }
        }
    }

Not least, because you can correct the spelling of Behaviour 🙂

ListView Behaviour

    public sealed class AutoScrollToLastItemBehavior : Behaviour<ListView>
    {
        // Need to track whether we've attached to the collection changed event
        bool _collectionChangedSubscribed = false;

        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.SelectionChanged += SelectionChanged;

            // The ItemSource of the listView will not be set yet, 
            // so get a method that we can hook up to later
            AssociatedObject.DataContextChanged += DataContextChanged;
        }

        private void SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            ScrollIntoView();
        }

        private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            ScrollIntoView();
        }

        private void DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
        {
            // The ObservableCollection implements the INotifyCollectionChanged interface
            // However, if this is bound to something that doesn't then just don't hook the event
            var collection = AssociatedObject.ItemsSource as INotifyCollectionChanged;
            if (collection != null && !_collectionChangedSubscribed)
            {
                // The data context has been changed, so now hook 
                // into the collection changed event
                collection.CollectionChanged += CollectionChanged;
                _collectionChangedSubscribed = true;
            }

        }

        private void ScrollIntoView()
        {
            int count = AssociatedObject.Items.Count;
            if (count > 0)
            {
                var last = AssociatedObject.Items[count - 1];
                AssociatedObject.ScrollIntoView(last);
            }
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            AssociatedObject.SelectionChanged -= SelectionChanged;
            AssociatedObject.DataContextChanged -= DataContextChanged;

            // Detach from the collection changed event
            var collection = AssociatedObject.ItemsSource as INotifyCollectionChanged;
            if (collection != null && _collectionChangedSubscribed)
            {
                collection.CollectionChanged -= CollectionChanged;
                _collectionChangedSubscribed = false;

            }
        }

Attaching the behaviour

Finally, change the listview to include a call to the behaviour:

            <ListView ItemsSource="{Binding Log}" x:Name="StatusUpdate">                
                <i:Interaction.Behaviors>
                    <behaviours:AutoScrollToLastItemBehavior />
                </i:Interaction.Behaviors>
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding}"/>
                    </DataTemplate>
                </ListView.ItemTemplate>

            </ListView>

Also, you need to reference Interactivity, and your behaviours:

    xmlns:behaviours="using:KingMaker.Behaviours"    
    xmlns:i="using:Microsoft.Xaml.Interactivity" 

The listview should now behave as expected.

Leave a Reply

Your email address will not be published. Required fields are marked *