2016-02-01 5 views
6

При использовании MVVM мы располагаем представлением (в то время как режим просмотра сохраняется).Восстановить состояние ListView MVVM

Мой вопрос в том, как восстановить состояние ListView при создании нового вида как можно ближе к одному при отображении вида?

ScrollIntoView работает только частично. Я могу только прокрутить до одного элемента, и он может быть сверху или снизу, нет контроля над тем, где элемент будет отображаться в представлении.

У меня есть multi-selection (и горизонтальная полоса прокрутки, но это неважно), и кто-то может выбрать несколько элементов и, возможно, прокрутить дальше (без изменения выбора).

В идеале связывание ScrollViewer из ListView свойств ViewModel будет делать, но я боюсь, чтобы подпадать под проблемой XY просить, что непосредственно (не уверен, что если this даже применимо). Кроме того, мне кажется, что это очень распространенная проблема для wpf, но, возможно, я не могу правильно сформулировать запрос Google, поскольку не могу найти связанный ListView + ScrollViewer + MVVM комбо.

Возможно ли это?


У меня есть проблемы с ScrollIntoView и данными-шаблонами (MVVM) с довольно уродливыми обходными путями. Восстановление ListView состояние с ScrollIntoView звучит неправильно. Должен быть другой путь. Сегодня google приводит меня к моему собственному неотвеченному вопросу.


Я ищу решение для восстановления 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).

ответ

3

Я не думаю, что вам может понадобиться вручную прокрутить scrollviewer до предыдущей позиции - с MVVM или без него. Таким образом вам необходимо сохранить смещения scrollviewer, так или иначе, и восстановить его при загрузке представления.

Вы можете использовать прагматичный подход MVVM и хранить его на модели, как показано здесь: WPF & MVVM: Save ScrollViewer Postion And Set When Reloading. Возможно, он может быть украшен приложенным свойством/поведением для повторного использования, если это необходимо.

В качестве альтернативы вы можете полностью игнорировать MVVM и держать его целиком на стороне вид:

EDIT: Обновленный образец на основе кода:

Вид:

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

code- сзади есть две кнопки для иллюстрации подхода MVVM и View-only, соответственно

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 ВМ имеет свойство по горизонтали/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, которые требуют введения метода расширения, который перемещается по визуальному дереву. Я не понимал этого, когда писал оригинальный ответ.

+0

Я не Посмотрите, как использовать что-либо из этого ответа. См. Edit, я добавил MCVE, вы могли бы заставить его работать (восстановление состояния)? – Sinatr

+0

Я обновляю ответ в соответствии с вашим образцом. Я также понял, что в моем первоначальном ответе была куча ошибок компиляции - я прошу прощения за это. Это было написано в блокноте, поскольку у меня не было доступа к Visual Studio. Это написано и протестировано в VS, хотя :) – sondergard

+0

[GetChildOfType] (http://stackoverflow.com/a/10279201/1997232)? – Sinatr

0

Вы можете попробовать добавить SelectedValue в ListView и использовать 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); 
    } 
} 
+0

Я не тестировал его (извините), но у меня была аналогичная идея в прошлом. И проблема была в следующем: выберите что-нибудь, затем прокрутите (вверх или вниз) и выберите больше предметов. Как только вы выберете первый элемент снаружи, у вас будет вызвана логика прокрутки и в зависимости от стратегии (вы прокрутите до первого или последнего элемента?) Что-то произойдет. Это очень раздражает пользователя. Другое дело, что вы используете словарь, почему? Просто подпишитесь на 'SelectedChanged' аналогично тому, как вы это делаете с' Loaded'. И локальная переменная 'datagrid' рассказывает о том, откуда это было сорвано. – Sinatr

+0

На самом деле я не рассматривал возможность выбора большего количества предметов. Я снова проверю его, и он перейдет к первому элементу **. Словарь используется для отмены подписки 'SelectionChangedEventHandler', потому что функция лямбда [link] (http://stackoverflow.com/questions/183367/unsubscribe-anonymous-method-in-c-sharp). 'datagrid' является ошибкой, потому что я начал использовать это поведение в Datagrid, и я изменил его на Listbox. – zzczzc004