10
votes

I am using the ScrollViewer with the MVVM pattern, and a list of items is wrapped by the ScrollViewer, such as

<ScrollViewer>
  <ListView>
    <ListView.View>
        <GridView>
            <GridViewColumn
                Header = "Name"
                DisplayMemberBinding="{Binding Path=Name}"
            />              
        </GridView>
    </ListView.View>
  </ListView>
</ScrollViewer>

The items of the listview are bound to a collection of objects in the viewmodel. I want the scrollviewer to scroll to the top whenever a item is added or removed from the collection.
I need the viewmodel to trigger the event, rather than using the ScrollToTop() method in the code-behind of the view.

4

4 Answers

24
votes

IMHO, the clearest way to do this is using a "Behavior" via an AttachedProperty. An AttachedProperty is a mechanism to extend existing controls functionality.

First, create a class to hold the AtachedProperty, for instance:

public class ScrollViewerBehavior
{
    public static bool GetAutoScrollToTop(DependencyObject obj)
    {
        return (bool)obj.GetValue(AutoScrollToTopProperty);
    }

    public static void SetAutoScrollToTop(DependencyObject obj, bool value)
    {
        obj.SetValue(AutoScrollToTopProperty, value);
    }

    public static readonly DependencyProperty AutoScrollToTopProperty =
        DependencyProperty.RegisterAttached("AutoScrollToTop", typeof(bool), typeof(ScrollViewerBehavior), new PropertyMetadata(false, (o, e) =>
            {
                var scrollViewer = o as ScrollViewer;
                if (scrollViewer == null)
                {
                    return;
                }
                if ((bool)e.NewValue)
                {
                    scrollViewer.ScrollToTop();
                    SetAutoScrollToTop(o, false);
                }
            }));
}

This attached property allows a ScrollViewer having "magically" a new property of type Boolean, acting like a DependencyProperty in your XAML. If you bind this property to a standard property in your ViewModel, for instance:

private bool _reset;
public bool Reset
{
    get { return _reset; }
    set
    {
        _reset = value;
        if(PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs("Reset"));
    }
}

(again, the name is up to you) and then you set this Reset property to true, your ScrollViewer will scroll to top. I have named the AtachedPropertyas AutoScrollToTop, but the name is not important for this purpose.

The XAML will be something like:

<ScrollViewer my:ScrollViewerBehavior.AutoScrollToTop="{Binding Reset, Mode=TwoWay}">
    <ListView>
        <ListView.View>
            <GridView>
                <GridViewColumn
                    Header = "Name"
                    DisplayMemberBinding="{Binding Path=Name}"
                />
            </GridView>
        </ListView.View>
    </ListView>
</ScrollViewer>

Note: my is the namespace where your ScrollViewerBehavior class lives. For example: xmlns:my="clr-namespace:MyApp.Behaviors"

Finally, the only thing you have to do in your ViewModel is to set Reset = true when you like, in your case, when you add or remove an element from the collection.

2
votes

Create a new ListView control which extend Listview and use this new one instead

public class ScrollListView : ListView
    {
        protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {

            if (e.OldItems.Count > 0)
                this.ScrollIntoView(e.OldItems[e.OldStartingIndex]);

            base.OnItemsChanged(e);
        }
    }
-1
votes

I have also faced a similar scenario where I needed to assign ScrollViewer's HorizontalOffset and VerticalOffset programmatically. I am afraid there is no direct binding mechanism for this. What I did was a way around (believe me, I still do not like the approach I followed, but I did not find any other option). Here is what I suggest:

Hook the ScrollViewer's Loaded event, cast the sender object to ScrollViewer and assign it to a property in DataContext (Means you need to keep a ScrollViewer propery in DataContext which will hold the reference of ScrollViewer in the UI). Hook up ObservableCollection's CollectionChanged events in ViewModel and using the ScrollViewer property, you can call methods like ScrollToTop() etc.

This is just a way around. I am still looking for better solution.

-1
votes

The simplest correct way to do this in MVVM is by creating an event in your viewmodel and subscribing to it from your view. And then, in the event handler, call ScrollToTop.

You fire the event from your viewmodel every time your collection is modified, for instance, and then it's up to the view to react to that event and scroll the list to the top.

Even if this involves some code-behind and demands that the view knows part of its viewmodel, it doesn't violate the MVVM pattern, unlike other workarounds.

public interface IMyViewModel
{
    event EventHandler MyCollectionChanged;
}

public class MyViewModel : IMyViewModel
{
    public event EventHandler MyCollectionChanged;

    // More viewmodel related stuff

    protected virtual void OnMyCollectionChanged(EventArgs e)
    {
        if (MyCollectionChanged != null)
            MyCollectionChanged(this, e);
    }
}

public class MyWindow : Window
{
    public MyWindow(IMyViewModel viewModel)
    {
        this.DataContext = viewModel;
        InitializeComponent();
        (this.DataContext as IViewModel).MyCollectionChanged+= MyCollectionChangedEventHandler;
    }

    private void MyCollectionChangedEventHandler(object sender, EventArgs e)
    {
        // Do view related stuff
        scrollViewer.ScrollToTop();
    }

}

EDIT: But it can be refined a lot more, of course. If you want to avoid using code-behind, look for DataEventTriggers. If you don't mind about code-behind but are concerned about memory leaks, look for weak events.

And finally, since the logic you want is 100% view-related (have the ListView scroll every time an item is added or removed to it), you could also implement it as a Behavior / attached property, or extending the ListView. That could get a tad more convoluted, but I encourage you to give those options some thought.