【问题标题】:Restore ListView state MVVM恢复 ListView 状态 MVVM
【发布时间】:2016-05-09 20:03:45
【问题描述】:

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

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

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

我有multi-selection(和水平滚动条,但这并不重要),有人可能会选择几个项目并且可能会进一步滚动(不改变选择)。

理想情况下,将 ScrollViewerListView 属性绑定到 viewmodel 就可以了,但我害怕直接要求这个 XY 问题(不确定 this 是否适用)。此外,在我看来,这对于 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的位置)。

【问题讨论】:

  • 您可以使用System.Windows.Interactivity。检查this方式。
  • @EgoistDeveloper,主动滚动到所选项目会产生一些副作用,并且与在接受的答案中恢复子 ScrollViewer 偏移量相比不可靠。

标签: c# wpf listview mvvm scrollviewer


【解决方案1】:

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

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

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

【讨论】:

  • 我没有测试过(对不起),但我过去也有类似的想法。问题在于:选择一些东西,然后滚动(向上或向下)并选择更多项目。一旦您选择外面的第一个项目,您的滚动逻辑就会触发,并且取决于策略(您滚动到第一个项目还是最后一个项目?)会发生一些事情。这对用户来说非常烦人。你在这里使用字典的另一件事,为什么?只需订阅SelectedChanged,就像使用Loaded 一样。而局部变量datagrid 则说明了它是从哪里被窃取的。
  • 我其实没有考虑选择更多的项目。我再次测试它,它会滚动到第一项。该字典用于取消订阅SelectionChangedEventHandler,因为 lambda 函数linkdatagrid 是一个错误,因为我最初在 Datagrid 中使用此行为并将其更改为 Listbox。
【解决方案2】:

我认为您无法避免手动将滚动查看器滚动到上一个位置 - 无论是否使用 MVVM。 因此,您需要以一种或另一种方式存储滚动查看器的偏移量,并在加载视图时恢复它。

您可以采用实用的 MVVM 方法并将其存储在视图模型中,如下所示:WPF & MVVM: Save ScrollViewer Postion And Set When Reloading。 如果需要,它可能会用附加的属性/行为进行装饰以实现可重用性。

或者,您可以完全忽略 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;
        }
    }
}

在 View-only 方法中保存值的视图类

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 具有 Horizo​​ntal/VerticalOffset 属性

 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 的偏移属性,这需要引入一个遍历可视化树的扩展方法。写原始答案时我没有意识到这一点。

【讨论】:

  • 我看不到如何使用此答案中的任何内容。查看编辑,我添加了 MCVE,你能让它工作(恢复状态)吗?
  • 我根据您的示例更新答案。我还意识到我的原始答案中有一堆编译错误 - 我为此道歉。它是用记事本写的,因为我无法访问 Visual Studio。这个是在 VS 中编写和测试的 :)
  • 它在类 Extensions 中,在示例中被捆绑到 View 类中。
  • 没注意到。我只对 MVVM 方法感兴趣。您的代码似乎有效。让我用真实的项目试试。
猜你喜欢
  • 2014-10-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多