18
votes

I currently have a ListBox whose ItemsSource collection is bound to a property on my viewmodel, of type IEnumerable. When that preoprty's reference changes, the ListBox updates as expected, however I have a problem in that if I have a large collection of items and scroll to the bottom of the ListBox, and then change the reference to another collection containing, say, 1 item, the ListBox view is blank and no scrollbar is displayed. I have to then scroll the listbox up with the mouse wheel, until the 1 item comes into view.

So, what I think I'm after, is a way of resetting the scroll position of the ListBox to the top, whenever the ItemsSource property changes, so that something is always displayed no matter how large or small the collection.

5
Did I understand your question correctly or were you looking for something else?Fredrik Hedblad
@Meleak Thanks! that approach will be useful. I've actually tracked the problem down to a VirtualizingTilePanel that my ListBox is using which I have taken from blogs.msdn.com/b/dancre/archive/2006/02/16/…. The problem can be replicated on the sample download by scrolling to the bottom, and then replacing the bound collection with one that contains a single item. The view goes blank until you scroll up with the mouse wheel.devdigital
Found an answer at social.msdn.microsoft.com/Forums/en/wpf/thread/… for the VirtualizingTilePanel, but I've switched to the virtualizing wrappanel from binarymission - binarymission.co.uk/binaryvirtwrappanelsl3.htmdevdigital

5 Answers

21
votes

I'm unable to reproduce your problem (for me, the ListBox is scrolled to the last item in the new collection when changing ItemsSource). Anyway, to scroll the ListBox to the top every time its ItemsSource changes you can use some code behind. First listen to changes in the ItemsSourceProperty and then scroll the ListBox to the top once its items has been generated

Update

Made an attached behavior that does this instead to avoid code behind. It can be used like this

<ListBox ...
         behaviors:ScrollToTopBehavior.ScrollToTop="True"/>

ScrollToTopBehavior

public static class ScrollToTopBehavior 
{
    public static readonly DependencyProperty ScrollToTopProperty = 
        DependencyProperty.RegisterAttached 
        (
            "ScrollToTop", 
            typeof(bool),
            typeof(ScrollToTopBehavior),
            new UIPropertyMetadata(false, OnScrollToTopPropertyChanged) 
        );
    public static bool GetScrollToTop(DependencyObject obj) 
    {
        return (bool)obj.GetValue(ScrollToTopProperty); 
    }
    public static void SetScrollToTop(DependencyObject obj, bool value) 
    {
        obj.SetValue(ScrollToTopProperty, value); 
    }
    private static void OnScrollToTopPropertyChanged(DependencyObject dpo, 
                                                     DependencyPropertyChangedEventArgs e) 
    {
        ItemsControl itemsControl = dpo as ItemsControl;
        if (itemsControl != null) 
        {
            DependencyPropertyDescriptor dependencyPropertyDescriptor =
                    DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, typeof(ItemsControl));
            if (dependencyPropertyDescriptor != null)
            {
                if ((bool)e.NewValue == true) 
                {
                    dependencyPropertyDescriptor.AddValueChanged(itemsControl, ItemsSourceChanged);
                }
                else 
                {
                    dependencyPropertyDescriptor.RemoveValueChanged(itemsControl, ItemsSourceChanged);
                }
            } 
        } 
    }
    static void ItemsSourceChanged(object sender, EventArgs e)
    {
        ItemsControl itemsControl = sender as ItemsControl;
        EventHandler eventHandler = null;
        eventHandler = new EventHandler(delegate
        {
            if (itemsControl.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
            {
                ScrollViewer scrollViewer = GetVisualChild<ScrollViewer>(itemsControl) as ScrollViewer;
                scrollViewer.ScrollToTop();
                itemsControl.ItemContainerGenerator.StatusChanged -= eventHandler;
            }
        });
        itemsControl.ItemContainerGenerator.StatusChanged += eventHandler;
    }
}

And an implementation of GetVisualChild

private T GetVisualChild<T>(DependencyObject parent) where T : Visual
{
    T child = default(T);
    int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
    for (int i = 0; i < numVisuals; i++)
    {
        Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
        child = v as T;
        if (child == null)
        {
            child = GetVisualChild<T>(v);
        }
        if (child != null)
        {
            break;
        }
    }
    return child;
}
7
votes

Late answer:

A simple solution is to add an event handler for the TargetUpdated event, and set NotifyOnTargetUpdated=True on the ItemsSource binding:

<ListBox x:Name="listBox" 
         ItemsSource="{Binding MySource, NotifyOnTargetUpdated=True}"
         TargetUpdated="ListBox_TargetUpdated"/>

and in the event handler, scroll to the top item:

private void ListBox_TargetUpdated(object sender, DataTransferEventArgs e)
{
    if (listBox.Items.Count > 0)
    {
        listBox.ScrollIntoView(listBox.Items[0]);
    }
}
1
votes

Try this:

if (listBox.Items.Count > 0) {
    listBox.ScrollIntoView(listBox.Items[0]); 
}
0
votes

Improved Fredrik Hedblad's answer to work with ObservableCollection:

public static class ItemsControlAttachedProperties
{
    #region ScrollToTopOnItemsSourceChange Property

    public static readonly DependencyProperty ScrollToTopOnItemsSourceChangeProperty =
        DependencyProperty.RegisterAttached(
            "ScrollToTopOnItemsSourceChange",
            typeof(bool),
            typeof(ItemsControlAttachedProperties),
            new UIPropertyMetadata(false, OnScrollToTopOnItemsSourceChangePropertyChanged));

    public static bool GetScrollToTopOnItemsSourceChange(DependencyObject obj)
    {
        return (bool) obj.GetValue(ScrollToTopOnItemsSourceChangeProperty);
    }

    public static void SetScrollToTopOnItemsSourceChange(DependencyObject obj, bool value)
    {
        obj.SetValue(ScrollToTopOnItemsSourceChangeProperty, value);
    }

    static void OnScrollToTopOnItemsSourceChangePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        var itemsControl = obj as ItemsControl;
        if (itemsControl == null)
        {
            throw new Exception("ScrollToTopOnItemsSourceChange Property must be attached to an ItemsControl based control.");
        }

        DependencyPropertyDescriptor descriptor =
            DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, typeof(ItemsControl));
        if (descriptor != null)
        {
            if ((bool) e.NewValue)
            {
                descriptor.AddValueChanged(itemsControl, ItemsSourceChanged);
            }
            else
            {
                descriptor.RemoveValueChanged(itemsControl, ItemsSourceChanged);
            }
        }
    }

    static void ItemsSourceChanged(object sender, EventArgs e)
    {
        var itemsControl = sender as ItemsControl;
        DoScrollToTop(itemsControl);

        var collection = itemsControl.ItemsSource as INotifyCollectionChanged;
        if (collection != null)
        {
            collection.CollectionChanged += (o, args) => DoScrollToTop(itemsControl);
        }
    }

    static void DoScrollToTop(ItemsControl itemsControl)
    {
        EventHandler eventHandler = null;
        eventHandler =
            delegate
            {
                if (itemsControl.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
                {
                    var scrollViewer = GetVisualChild<ScrollViewer>(itemsControl);
                    scrollViewer.ScrollToTop();
                    itemsControl.ItemContainerGenerator.StatusChanged -= eventHandler;
                }
            };
        itemsControl.ItemContainerGenerator.StatusChanged += eventHandler;
    }

    static T GetVisualChild<T>(DependencyObject parent) where T : Visual
    {
        T child = default(T);
        int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
        for (var i = 0; i < numVisuals; i++)
        {
            var v = (Visual) VisualTreeHelper.GetChild(parent, i);
            child = v as T ?? GetVisualChild<T>(v);
            if (child != null)
            {
                break;
            }
        }
        return child;
    }

    #endregion
}
0
votes

When you format the control, you select a range of cells as the selection choices which are then listed in the list box. You also select a cell as the link to the selected choices in which a number will be displayed depending on the position of the selection in the list. 1 for first in the list, 2 for second etc. The code is quite simply:-

Range("A1")Select

Selection = 1

Change ("A1") to the cell you have linked and change the 1 to the position in the list you want selected.

The cell reference being a link works both ways - if you change your selection, the number in the cell changes and if you change the number in the cell, the highlighted selection changes.