2016-02-01 12 views
6

Quando si utilizza MVVM stiamo smaltire vista (mentre viewmodel persiste).Restore stato ListView MVVM

La mia domanda è come ripristinare ListView stato durante la creazione di nuova vista il più vicino possibile a uno quando vista è stato eliminato?

ScrollIntoView funziona solo parzialmente. Posso solo scorrere fino a un singolo elemento e può essere in alto o in basso, non c'è controllo su dove apparirà l'elemento nella vista.

Ho multi-selection (e barra di scorrimento orizzontale, ma questo è piuttosto poco importante) e qualcuno può selezionare diversi elementi e forse scorrere ulteriormente (senza modificare la selezione).

Idealmente vincolanti ScrollViewer di ListView oggetti da ViewModel farebbe, ma ho paura di cadere sotto problema XY chiedere che direttamente (non so se this è ancora applicabile). Inoltre questo mi sembra essere una cosa molto comune per WPF, ma forse non riesco a formulare query di google correttamente come non riesco a trovare legati ListView + ScrollViewer + MVVM combinata.

È possibile?


ho problemi con ScrollIntoView e data-modelli (MVVM) con piuttosto brutti soluzioni alternative. Ripristinare ListView stato con ScrollIntoView suona male. Ci dovrebbe essere un altro modo. Oggi google mi porta alla mia domanda senza risposta.


Sto cercando una soluzione per ripristinare lo stato ListView. Considerare quanto segue come 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> 

Questa è una finestra con ContentControl cui contenuto è destinato a DataContext (commutato dal tasto essere o null o ViewModel esempio).

Ho aggiunto il supporto IsSelected (prova a selezionare alcuni elementi, nascondere/mostrare ListView ripristinerà quello).

Lo scopo è: mostra ListView, rotolo (è 100x100 dimensioni, in modo che il contenuto è più grande) verticalmente e/o orizzontalmente, pulsante per nascondere clic, premere il pulsante per mostrare e in questo momento ListView dovrebbe ripristinare lo stato (cioè posizione di ScrollViewer).

risposta

3

Non penso che si possa evitare di dover scorrere manualmente lo scrollviewer alla posizione precedente, con o senza MVVM. Come tale è necessario memorizzare gli offset del ScrollViewer, in un modo o nell'altro, e ripristinarlo quando la vista viene caricato.

È possibile adottare l'approccio pragmatico MVVM e memorizzarlo sul viewmodel come illustrato qui: WPF & MVVM: Save ScrollViewer Postion And Set When Reloading. Si potrebbe probabilmente essere decorata con una proprietà/comportamento allegato per la riusabilità, se necessario.

In alternativa si potrebbe ignorare completamente MVVM e tenerlo interamente sulla vista laterale:

EDIT: Aggiornato il campione in base al codice:

La vista:

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

Il code- dietro ha due pulsanti per illustrare rispettivamente MVVM e approccio solo visualizzazione

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

La classe al fine di contenere i valori nella sola visualizzazione avvicinarsi

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

Per l'approccio basato MVVM la VM ha una proprietà orizzontale/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>(); 
} 

Quindi la cosa difficile è in realtà sempre l'accesso ai le proprietà offset di ScrollViewer, che richiedeva l'introduzione di un metodo di estensione che percorre l'albero visivo. Non me ne sono reso conto quando ho scritto la risposta originale.

+0

I don vediamo come usare qualsiasi cosa da questa risposta. Vedi modifica, ho aggiunto MCVE, potresti farlo funzionare (ripristino dello stato)? – Sinatr

+0

Aggiornamento la risposta in base al campione. Ho anche capito che c'erano una serie di errori di compilazione nella mia risposta originale - mi scuso per questo. Era scritto nel blocco note, perché non avevo accesso a Visual Studio. Questo è scritto e testato in VS però :) – sondergard

+0

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

0

È possibile provare ad aggiungere SelectedValue in ListView e utilizzare il comportamento per lo scorrimento automatico. Ecco il codice:

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

Per XAML:

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

Per Comportamento:

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

Non l'ho provato (mi dispiace), ma avevo un'idea simile in passato. E il problema era il seguente: selezionare qualcosa, quindi scorrere (su o giù) e selezionare più voci. Non appena selezioni la prima voce all'esterno, la logica di scorrimento viene attivata e, in base alla strategia (scorri verso il primo o l'ultimo elemento?) Accade qualcosa. Questo è molto fastidioso per l'utente. Un'altra cosa qui stai usando il dizionario, perché? Basta iscriversi a "SelectedChanged" in modo simile a come lo si fa con 'Loaded'. E la variabile locale 'datagrid' indica da dove è stata rubata. – Sinatr

+0

In realtà non ho considerato selezionare più elementi. Lo provo di nuovo e scorrerà fino al ** primo oggetto **. Il dizionario è usato per annullare l'iscrizione a 'SelectionChangedEventHandler' perché la funzione lambda [link] (http://stackoverflow.com/questions/183367/unsubscribe-anonymous-method-in-c-sharp). 'datagrid' è un errore perché inizialmente uso questo comportamento nel Datagrid e l'ho cambiato in Listbox. – zzczzc004