2016-02-01 6 views
6

Wenn MVVM verwendet wird, wird die Ansicht entfernt (solange Viewmodel beibehalten wird).Restore ListView-Status MVVM

Meine Frage ist, wie ListView Zustand wiederherzustellen, wenn neue Ansicht so nah wie möglich zu einer, wenn Ansicht wurde entsorgt erstellt?

ScrollIntoView funktioniert nur teilweise. Ich kann nur zu einem einzelnen Bild blättern und es kann oben oder unten sein, es gibt keine Kontrolle darüber, wo das Element in der Ansicht angezeigt wird.

Ich habe multi-selection (und horizontale Bildlaufleiste, aber das ist eher unwichtig) und jemand kann mehrere Elemente auswählen und vielleicht weiter scrollen (ohne Auswahl zu ändern).

Idealer Bindung ScrollViewer von ListView Eigenschaften tun würde, zu Ansichtsmodell, aber ich habe Angst, unter XY Problem für die direkt fragen zu fallen (nicht sicher, ob this auch anwendbar ist). Darüber hinaus scheint mir dies eine sehr häufige Sache für wpf, aber vielleicht schaffe ich es, google query richtig zu formulieren, da ich keine verwandten ListView + ScrollViewer + MVVM Combo finden kann.

Ist das möglich?


Ich habe Probleme mit ScrollIntoView und Daten-Vorlagen (MVVM) mit ziemlich hässlich Abhilfen. Wiederherstellen ListView Zustand mit ScrollIntoView klingt falsch. Es sollte einen anderen Weg geben. Heute führt mich Google zu meiner eigenen unbeantworteten Frage.


Ich bin auf der Suche nach einer Lösung zur Wiederherstellung ListView Zustand. Betrachten folgenden als 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> 

Dies ist ein Fenster mit ContentControl die Inhalte DataContext (getoggelt durch Taste entweder null oder ViewModel Instanz zu sein) gebunden ist.

Ich habe hinzugefügt IsSelected Unterstützung (versuchen Sie, einige Elemente auszuwählen, versteckt/zeigt ListView wird das wiederherstellen).

Ziel ist: Show ListView, rollt (es ist 100x100 Größe, so dass der Inhalt größer ist) vertikal und/oder horizontal, Schaltfläche klicken, um zu verbergen, klicken um zu zeigen, und zu diesem Zeitpunkt ListView sollte seinen Zustand wiederherstellen (nämlich Position von ScrollViewer).

Antwort

3

Ich glaube nicht, dass Sie den Scrollviewer manuell zur vorherigen Position scrollen müssen - mit oder ohne MVVM. Als solche müssen Sie die Offsets des Scrollviewer auf die eine oder andere Weise speichern und wiederherstellen, wenn die Ansicht geladen wird.

Sie könnten den pragmatischen MVVM-Ansatz verwenden und auf dem Ansichtsmodell speichern, wie hier dargestellt: WPF & MVVM: Save ScrollViewer Postion And Set When Reloading. Es könnte wahrscheinlich mit einer angehängten Eigenschaft/Verhalten zur Wiederverwendbarkeit geschmückt werden, falls erforderlich.

Alternativ können Sie ganz MVVM ignorieren und es vollständig auf der Sichtseite halten:

EDIT: Aktualisiert die Probe auf der Grundlage Ihrer Code:

Aussicht:

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

Die Code- hinter hat zwei Tasten, um die MVVM und View-only-Ansatz zu veranschaulichenDie Ansichtsklasse nähern die Werte in der Ansicht nur für die VM hat eine horizontale/Vertical Eigenschaft

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

Für die MVVM basierten Ansatz zu halten

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

So ist die schwierige Sache ist eigentlich immer Zugang zu die Offset-Eigenschaften des ScrollViewer, die die Einführung einer Erweiterungsmethode erforderten, die den visuellen Baum durchläuft. Ich habe das nicht bemerkt, als ich die ursprüngliche Antwort geschrieben habe.

+0

I don nicht sehen, wie man irgendetwas von dieser Antwort benutzt. Siehe Bearbeiten, ich habe MCVE hinzugefügt. Könnten Sie es zum Laufen bringen (Wiederherstellungsstatus)? – Sinatr

+0

Ich aktualisiere die Antwort entsprechend Ihrer Probe. Ich erkannte auch, dass in meiner ursprünglichen Antwort einige Kompilierungsfehler auftraten - dafür entschuldige ich mich. Es wurde in Notepad geschrieben, da ich keinen Zugriff auf Visual Studio hatte. Dieser ist geschrieben und getestet in VS obwohl :) – sondergard

+0

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

0

Sie können versuchen, SelectedValue in ListView hinzuzufügen und das Verhalten zu Autoscroll verwenden. Hier ist Code:

Für Ansichtsmodell:

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

Für XAML:

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

Für Verhalten:

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

Ich habe es nicht getestet (Entschuldigung), aber ich hatte eine ähnliche Idee in der Vergangenheit. Und das Problem war mit folgenden: Wählen Sie etwas, dann scrollen (nach oben oder unten) und wählen Sie weitere Elemente. Sobald Sie das erste Element außerhalb auswählen, wird Ihre Scroll-Logik ausgelöst und je nach Strategie (scrollen Sie zum ersten oder letzten Element?) Wird etwas passieren. Dies ist sehr ärgerlich für den Benutzer. Eine andere Sache hier verwenden Sie Wörterbuch, warum? Abonnieren Sie 'SelectedChanged' ähnlich wie Sie es mit' Loaded' machen. Und die lokale Variable 'datagrid' gibt an, woher sie weggerissen wurde. – Sinatr

+0

Ich habe tatsächlich nicht erwogen, weitere Artikel auszuwählen. Ich teste es noch einmal und es scrollt zum ** ersten Gegenstand **. Das Wörterbuch wird verwendet, um den 'SelectionChangedEventHandler' wegen der Lambda-Funktion [link] abzumelden (http://stackoverflow.com/questions/183367/unsubscribe-anonymous-method-in-c-sharp). 'Datagrid' ist ein Fehler, weil ich dieses Verhalten zuerst im Datagrid benutze und es in Listbox geändert habe. – zzczzc004