恢复ListView状态MVVM

时间:2016-02-01 11:27:15

标签: c# wpf listview mvvm scrollviewer

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

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

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

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

理想情况下,ScrollViewer ListView属性的ListView属于viewmodel可以做到,但我担心会直接问到XY问题(不确定this是否适用)。此外,在我看来这对于wpf来说是一个非常普遍的事情,但也许我没有正确地制定谷歌查询,因为我无法找到相关的ScrollViewer + MVVM + ScrollIntoView组合

这可能吗?

我遇到了ListView和数据模板(MVVM)的问题,而且工作方式相当丑陋。使用ScrollIntoView恢复ListView状态听起来有误。应该有另一种方式。今天谷歌引导我找到我自己未回答的问题。

我正在寻找恢复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; } 州的解决方案。请考虑关注mcve

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

XAML:

ContentControl

这是一个DataContext的窗口,其内容绑定到null(按钮切换为ViewModelIsSelected个实例)。

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

目标是:展示100x100,滚动(它的ListView大小,以便内容更大)垂直和/或水平,点击按钮隐藏,点击按钮显示和此时ScrollViewer应恢复其状态(即context.Configuration.LazyLoadingEnabled = false; )的位置。

2 个答案:

答案 0 :(得分:3)

我不认为你可以手动滚动滚动查看器到以前的位置 - 有或没有MVVM。 因此,您需要以某种方式存储scrollviewer的偏移量,并在加载视图时将其恢复。

您可以采用实用的MVVM方法并将其存储在viewmodel上,如下所示: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;
        }
    }
}

用于以“仅查看”方法保存值的视图类

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

答案 1 :(得分:0)

您可以尝试在ListView中添加SelectedValue并使用Behavior to Autoscroll。 这是代码:

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