还原列表视图状态 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
(通过按钮切换为null
或ViewModel
实例(。
我添加了IsSelected
支持(尝试选择一些项目,隐藏/显示ListView
将恢复它(。
目的是:显示ListView
,垂直和/或水平滚动(它的大小100x100
,以便内容更大(,单击按钮隐藏,单击按钮显示,此时ListView
应该恢复其状态(即ScrollViewer
的位置(。
我认为您无法绕过必须手动将滚动查看器滚动到上一个位置 - 有或没有 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);
}
}