还原列表视图状态 MVVM

本文关键字:MVVM 视图状态 列表 还原 | 更新日期: 2023-09-27 18:32:40

使用 MVVM 时,我们正在处理视图(而视图模型仍然存在(。

我的问题是,在创建新视图时,如何恢复ListView尽可能接近处置视图时的状态?

ScrollIntoView 只能部分工作。我只能滚动到单个项目,它可以在顶部或底部,无法控制项目在视图中的显示位置。

我有多项选择(和水平滚动条,但这并不重要(,有人可以选择几个项目,也许可以进一步滚动(不更改选择(。

理想情况下,将ListView属性的ScrollViewer绑定到 viewmodel 就可以了,但我害怕直接陷入 XY 问题要求这样做(不确定这是否适用(。此外,在我看来,这对于 wpf 来说很常见,但也许我无法正确制定谷歌查询,因为我找不到相关的ListView + ScrollViewer + MVVM组合。

这可能吗?


我在ScrollIntoView和数据模板 (MVVM( 方面遇到了问题,解决方法相当丑陋。用ScrollIntoView恢复ListView状态听起来是错误的。应该有另一种方法。今天谷歌把我带到了我自己未回答的问题。


我正在寻找恢复ListView状态的解决方案。考虑以下作为 mcve:

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 ObservableCollection<Item> Items { get; } = new ObservableCollection<Item>
    {
        "Item 1",
        "Item 2",
        "Item 3 long enough to use horizontal scroll",
        "Item 4",
        "Item 5",
        new Item {Text = "Item 6", IsSelected = true }, // select something
        "Item 7",
        "Item 8",
        "Item 9",
    };
}
public partial class MainWindow : Window
{
    ViewModel _vm = new ViewModel();
    public MainWindow()
    {
        InitializeComponent();
    }
    void Button_Click(object sender, RoutedEventArgs e) => DataContext = DataContext == null ? _vm : null;
}

XAML:

<StackPanel>
    <ContentControl Content="{Binding}">
        <ContentControl.Resources>
            <DataTemplate DataType="{x:Type local:ViewModel}">
                <ListView Width="100" Height="100" ItemsSource="{Binding Items}">
                    <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>
            </DataTemplate>
        </ContentControl.Resources>
    </ContentControl>
    <Button Content="Click"
            Click="Button_Click" />
</StackPanel>

这是一个窗口,其中包含ContentControl内容绑定到DataContext(通过按钮切换为nullViewModel实例(。

我添加了IsSelected支持(尝试选择一些项目,隐藏/显示ListView将恢复它(。

目的是:显示ListView,垂直和/或水平滚动(它的大小100x100,以便内容更大(,单击按钮隐藏,单击按钮显示,此时ListView应该恢复其状态(即ScrollViewer的位置(。

还原列表视图状态 MVVM

我认为您无法绕过必须手动将滚动查看器滚动到上一个位置 - 有或没有 MVVM。因此,您需要以一种或另一种方式存储滚动查看器的偏移量,并在加载视图时将其还原。

您可以采用实用的 MVVM 方法并将其存储在视图模型上,如下所示:WPF 和 MVVM:保存 ScrollViewer 位置并在重新加载时设置。如果需要,可以使用附加的属性/行为来装饰它以实现可重用性。

或者,您可以完全忽略 MVVM 并将其完全保留在视图端:

编辑:根据您的代码更新了示例:

观点:

<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>

代码隐藏有两个按钮,分别用于说明 MVVM 和仅查看方法

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;
        }
    }
}

用于保存仅查看方法中的值的视图类

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;
    }
}

对于基于 MVVM 的方法,VM 具有"水平/垂直偏移"属性

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

因此,困难的事情实际上是访问 ScrollViewer 的偏移量属性,这需要引入一种遍历可视化树的扩展方法。我在写原始答案时没有意识到这一点。

您可以尝试在 ListView 中添加 SelectedValue,并使用 Behavior 进行自动滚动。这是代码:

对于视图模型:

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};
    }
}

对于 XAML

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

对于行为:

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);
    }
}