Restore ListView state MVVM Restore ListView state MVVM wpf wpf

Restore ListView state MVVM


I don't think you can get around having to manually scroll the scrollviewer to the previous position - with or without MVVM.As such you need to store the offsets of the scrollviewer, one way or another, and restore it when the view is loaded.

You could take the pragmatic MVVM approach and store it on the viewmodel as illustrated here: WPF & MVVM: Save ScrollViewer Postion And Set When Reloading. It could probably be decorated with an attached property/behavior for reusability if needed.

Alternatively you could completely ignore MVVM and keep it entirely on the view side:

EDIT: Updated the sample based on your code:

The view:

<Window x:Class="RestorableView.MainWindow"        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"        xmlns:local="clr-namespace:RestorableView"        mc:Ignorable="d"        Title="MainWindow" Height="350" Width="525">    <Grid>        <Grid>            <Grid.RowDefinitions>                <RowDefinition/>                <RowDefinition Height="Auto"/>            </Grid.RowDefinitions>            <ListView x:Name="list" ItemsSource="{Binding Items}" ScrollViewer.HorizontalScrollBarVisibility="Auto">                <ListView.ItemTemplate>                    <DataTemplate>                        <TextBlock Text="{Binding Text}" />                    </DataTemplate>                </ListView.ItemTemplate>                <ListView.ItemContainerStyle>                    <Style TargetType="ListViewItem">                        <Setter Property="IsSelected" Value="{Binding IsSelected}" />                    </Style>                </ListView.ItemContainerStyle>            </ListView>            <StackPanel Orientation="Horizontal" Grid.Row="1">                <Button Content="MVVM Based" x:Name="MvvmBased" Click="MvvmBased_OnClick"/>                <Button Content="View Based" x:Name="ViewBased" Click="ViewBased_OnClick" />            </StackPanel>        </Grid>    </Grid></Window>

The code-behind has two buttons to illustrate the MVVM and View-only approach respectively

public partial class MainWindow : Window{    ViewModel _vm = new ViewModel();    public MainWindow()    {        InitializeComponent();    }    private void MvvmBased_OnClick(object sender, RoutedEventArgs e)    {        var scrollViewer = list.GetChildOfType<ScrollViewer>();        if (DataContext != null)        {            _vm.VerticalOffset = scrollViewer.VerticalOffset;            _vm.HorizontalOffset = scrollViewer.HorizontalOffset;            DataContext = null;        }        else        {            scrollViewer.ScrollToVerticalOffset(_vm.VerticalOffset);            scrollViewer.ScrollToHorizontalOffset(_vm.HorizontalOffset);            DataContext = _vm;        }    }    private void ViewBased_OnClick(object sender, RoutedEventArgs e)    {        var scrollViewer = list.GetChildOfType<ScrollViewer>();        if (DataContext != null)        {            View.State[typeof(MainWindow)] = new Dictionary<string, object>()            {                { "ScrollViewer_VerticalOffset", scrollViewer.VerticalOffset },                { "ScrollViewer_HorizontalOffset", scrollViewer.HorizontalOffset },                // Additional fields here            };            DataContext = null;        }        else        {            var persisted = View.State[typeof(MainWindow)];            if (persisted != null)            {                scrollViewer.ScrollToVerticalOffset((double)persisted["ScrollViewer_VerticalOffset"]);                scrollViewer.ScrollToHorizontalOffset((double)persisted["ScrollViewer_HorizontalOffset"]);                // Additional fields here            }            DataContext = _vm;        }    }}

The view class to hold the values in the View-only approach

public class View{    private readonly Dictionary<string, Dictionary<string, object>> _views = new Dictionary<string, Dictionary<string, object>>();    private static readonly View _instance = new View();    public static View State => _instance;    public Dictionary<string, object> this[string viewKey]    {        get        {            if (_views.ContainsKey(viewKey))            {                return _views[viewKey];            }            return null;        }        set        {            _views[viewKey] = value;        }    }    public Dictionary<string, object> this[Type viewType]    {        get        {            return this[viewType.FullName];        }        set        {            this[viewType.FullName] = value;        }    }}public static class Extensions{    public static T GetChildOfType<T>(this DependencyObject depObj)where T : DependencyObject    {        if (depObj == null) return null;        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)        {            var child = VisualTreeHelper.GetChild(depObj, i);            var result = (child as T) ?? GetChildOfType<T>(child);            if (result != null) return result;        }        return null;    }}

For the MVVM based approach the VM has a Horizontal/VerticalOffset property

 public class ViewModel{    public class Item    {        public string Text { get; set; }        public bool IsSelected { get; set; }        public static implicit operator Item(string text) => new Item() { Text = text };    }    public ViewModel()    {        for (int i = 0; i < 50; i++)        {            var text = "";            for (int j = 0; j < i; j++)            {                text += "Item " + i;            }            Items.Add(new Item() { Text = text });        }    }    public double HorizontalOffset { get; set; }    public double VerticalOffset { get; set; }    public ObservableCollection<Item> Items { get; } = new ObservableCollection<Item>();}

So the difficult thing is actually getting access to the offset properties of the ScrollViewer, which required introducing an extension method which walks the visual tree. I didn't realize this when writing the original answer.


You can try to add SelectedValue in ListView and use the Behavior to Autoscroll.Here is code:

For ViewModel:

public class ViewModel{    public ViewModel()    {        // select something        SelectedValue = Items[5];    }    public ObservableCollection<Item> Items { get; } = new ObservableCollection<Item>    {        "Item 1",        "Item 2",        "Item 3 long enough to use horizontal scroll",        "Item 4",        "Item 5",        "Item 6",         "Item 7",        "Item 8",        "Item 9"    };    // To save which item is selected    public Item SelectedValue { get; set; }    public class Item    {        public string Text { get; set; }        public bool IsSelected { get; set; }        public static implicit operator Item(string text) => new Item {Text = text};    }}

For XAML:

<ListView Width="100" Height="100" ItemsSource="{Binding Items}" SelectedValue="{Binding SelectedValue}" local:ListBoxAutoscrollBehavior.Autoscroll="True">

For Behavior:

public static class ListBoxAutoscrollBehavior{    public static readonly DependencyProperty AutoscrollProperty = DependencyProperty.RegisterAttached(        "Autoscroll", typeof (bool), typeof (ListBoxAutoscrollBehavior),        new PropertyMetadata(default(bool), AutoscrollChangedCallback));    private static readonly Dictionary<ListBox, SelectionChangedEventHandler> handlersDict =        new Dictionary<ListBox, SelectionChangedEventHandler>();    private static void AutoscrollChangedCallback(DependencyObject dependencyObject,        DependencyPropertyChangedEventArgs args)    {        var listBox = dependencyObject as ListBox;        if (listBox == null)        {            throw new InvalidOperationException("Dependency object is not ListBox.");        }        if ((bool) args.NewValue)        {            Subscribe(listBox);            listBox.Unloaded += ListBoxOnUnloaded;            listBox.Loaded += ListBoxOnLoaded;        }        else        {            Unsubscribe(listBox);            listBox.Unloaded -= ListBoxOnUnloaded;            listBox.Loaded -= ListBoxOnLoaded;        }    }    private static void Subscribe(ListBox listBox)    {        if (handlersDict.ContainsKey(listBox))        {            return;        }        var handler = new SelectionChangedEventHandler((sender, eventArgs) => ScrollToSelect(listBox));        handlersDict.Add(listBox, handler);        listBox.SelectionChanged += handler;        ScrollToSelect(listBox);    }    private static void Unsubscribe(ListBox listBox)    {        SelectionChangedEventHandler handler;        handlersDict.TryGetValue(listBox, out handler);        if (handler == null)        {            return;        }        listBox.SelectionChanged -= handler;        handlersDict.Remove(listBox);    }    private static void ListBoxOnLoaded(object sender, RoutedEventArgs routedEventArgs)    {        var listBox = (ListBox) sender;        if (GetAutoscroll(listBox))        {            Subscribe(listBox);        }    }    private static void ListBoxOnUnloaded(object sender, RoutedEventArgs routedEventArgs)    {        var listBox = (ListBox) sender;        if (GetAutoscroll(listBox))        {            Unsubscribe(listBox);        }    }    private static void ScrollToSelect(ListBox datagrid)    {        if (datagrid.Items.Count == 0)        {            return;        }        if (datagrid.SelectedItem == null)        {            return;        }        datagrid.ScrollIntoView(datagrid.SelectedItem);    }    public static void SetAutoscroll(DependencyObject element, bool value)    {        element.SetValue(AutoscrollProperty, value);    }    public static bool GetAutoscroll(DependencyObject element)    {        return (bool) element.GetValue(AutoscrollProperty);    }}