Condividi tramite


Ottimizzazione dell'interfaccia utente in ListView e GridView

NotaPer altri dettagli, vedi la sessione di //build/ dedicata a come migliorare sensibilmente le prestazioni quando gli utenti interagiscono con grandi quantità di dati in GridView e ListView.

Migliorare le prestazioni e il tempo di avvio di ListView e GridView tramite la virtualizzazione dell'interfaccia utente, la riduzione degli elementi e l'aggiornamento progressivo degli elementi. Per le tecniche di virtualizzazione dei dati, vedere Virtualizzazione dati di ListView e GridView.

Due fattori chiave per le prestazioni della raccolta

La modifica delle raccolte è uno scenario comune. Un visualizzatore foto include raccolte di foto, un lettore dispone di raccolte di articoli/libri/storie e un'app per lo shopping include raccolte di prodotti. Questo argomento illustra le operazioni che è possibile eseguire per rendere efficiente l'app durante la modifica delle raccolte.

Esistono due fattori chiave nelle prestazioni quando si tratta di raccolte: uno è il tempo impiegato dal thread dell'interfaccia utente che crea elementi; l'altro è la memoria usata sia dal set di dati non elaborato sia dagli elementi dell'interfaccia utente usati per eseguire il rendering di tali dati.

Per una panoramica/scorrimento uniforme, è fondamentale che il thread dell'interfaccia utente esegua un processo efficiente e intelligente di creazione di istanze, data binding e disposizione degli elementi.

Virtualizzazione dell'interfaccia utente

La virtualizzazione dell'interfaccia utente è il miglioramento più importante che puoi apportare. Ciò significa che gli elementi dell'interfaccia utente che rappresentano gli elementi vengono creati su richiesta. Per un controllo elementi associato a una raccolta di 1000 elementi sarebbe uno spreco di risorse creare l'interfaccia utente per tutti gli elementi in una sola volta, perché non possono tutti essere visualizzati contemporaneamente. ListView e GridView (e altri controlli standard derivati da ItemsControl) eseguono automaticamente la virtualizzazione dell'interfaccia utente. Quando durante lo scorrimento gli elementi stanno per essere visualizzati (mancano poche pagine), il framework genera l'interfaccia utente per gli elementi e li memorizza nella cache. Quando è improbabile che gli elementi vengano visualizzati di nuovo, il framework recupera la memoria.

Se si fornisce un modello di pannello degli elementi personalizzato (vedi ItemsPanel), assicurarsi di usare un pannello di virtualizzazione, ad esempio ItemsWrapGrid o ItemsStackPanel. Se si usa VariableSizedWrapGrid, WrapGrido StackPanel, non si otterrà la virtualizzazione. Inoltre, gli eventi ListView seguenti vengono generati solo quando si utilizza un ItemsWrapGrid o un ItemsStackPanel: ChoosingGroupHeaderContainer, ChoosingItemContainere ContainerContentChanging.

Il concetto di riquadro di visualizzazione è fondamentale per la virtualizzazione dell'interfaccia utente, in quanto il framework deve creare gli elementi che probabilmente verranno visualizzati. In generale, il riquadro di un oggetto ItemsControl è l’extent del controllo logico. Ad esempio, il viewport di un elemento ListView è dato dalla larghezza e dall’altezza dell’elemento ListView. Alcuni pannelli consentono spazio illimitato agli elementi figlio, come nel caso di ScrollViewer e una griglia, con righe e colonne ridimensionate automaticamente. Quando un oggetto ItemsControl virtualizzato viene inserito in un pannello di questo tipo occupa tutto lo spazio necessario per visualizzare tutti gli elementi e questo ostacola la virtualizzazione. Ripristina la virtualizzazione impostando una larghezza e un'altezza sull'elemento ItemsControl.

Riduzione degli elementi per elemento

Mantenere il numero di elementi dell'interfaccia utente usati per eseguire il rendering degli elementi a un minimo ragionevole.

Quando viene visualizzato un controllo elementi, vengono creati tutti gli elementi necessari per eseguire il rendering di un riquadro di visualizzazione pieno di elementi. Inoltre, a mano a mano che gli elementi si avvicinano al viewport, il framework aggiorna gli elementi dell'interfaccia utente nei modelli di elemento memorizzati nella cache con gli oggetti dati associati. Ridurre al minimo la complessità del markup all'interno dei modelli paga in memoria e nel tempo dedicato al thread dell'interfaccia utente, migliorando la velocità di risposta soprattutto durante la panoramica/lo scorrimento. I modelli in questione sono il modello di elemento (vedere ItemTemplate) e il modello di controllo di un ListViewItem o un GridViewItem (modello di controllo elemento, o ItemContainerStyle). Il vantaggio di una piccola riduzione del numero di elementi viene moltiplicato per il numero di elementi visualizzati.

Per esempi di riduzione degli elementi, vedere Ottimizzare il markup XAML.

I modelli di controllo predefiniti per ListViewItem e GridViewItem contengono un elemento ListViewItemPresenter. Questo relatore è un singolo elemento ottimizzato che visualizza oggetti visivi complessi per lo stato attivo, la selezione e altri stati di visualizzazione. Se si dispone già di modelli di controllo elemento personalizzati (ItemContainerStyle) o se in futuro si modifica una copia di un modello di controllo elemento, è consigliabile usare un ListViewItemPresenter perché tale elemento darà un equilibrio ottimale tra prestazioni e personalizzazione nella maggior parte dei casi. È possibile personalizzare il relatore impostando le proprietà su di esso. Ad esempio, ecco il markup che rimuove il segno di spunta visualizzato per impostazione predefinita quando viene selezionato un elemento e modifica il colore di sfondo dell'elemento selezionato in arancione.

...
<ListView>
    ...
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ListViewItem">
                        <ListViewItemPresenter SelectionCheckMarkVisualEnabled="False" SelectedBackground="Orange"/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ListView.ItemContainerStyle>
</ListView>
<!-- ... -->

Esistono circa 25 proprietà con nomi autodescrittura simili a SelectionCheckMarkVisualEnabled e SelectedBackground. Se i tipi di relatore non sono sufficientemente personalizzabili per il caso d'uso, è possibile modificare una copia del modello di controllo ListViewItemExpanded o GridViewItemExpanded. Questi sono disponibili in \Program Files (x86)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\<version>\Generic\generic.xaml. Tenere presente che l'uso di questi modelli implica il trading di alcune prestazioni per l'aumento della personalizzazione.

Aggiornare gli elementi ListView e GridView in modo progressivo

Se si usa la virtualizzazione dei dati, è possibile mantenere velocità di risposta elevata diListView e GridView configurando il controllo per eseguire il rendering degli elementi temporanei dell'interfaccia utente per gli elementi ancora inattivi. Gli elementi temporanei vengono quindi sostituiti progressivamente con l'interfaccia utente effettiva durante il caricamento dei dati.

Inoltre, indipendentemente dal punto di provenienza del caricamento dati (disco locale, rete o cloud)— un utente può eseguire una panoramica/scorrimento di un ListView o GridView in modo tanto rapido che non è possibile eseguire rapidamente il rendering di ogni elemento con massima fedeltà mantenendo una panoramica/uno scorrimento uniforme. Per mantenere la panoramica o lo scorrimento uniforme, è possibile scegliere di eseguire il rendering di un elemento in più fasi oltre a usare segnaposto.

Un esempio di queste tecniche è spesso visto nelle app di visualizzazione foto: anche se non tutte le immagini sono state caricate e visualizzate, l'utente può comunque eseguire la panoramica/lo scorrimento e interagire con la raccolta. In alternativa, per un elemento "movie", è possibile visualizzare il titolo nella prima fase, la classificazione nella seconda fase e un'immagine del poster nella terza fase. L'utente vede i dati più importanti su ogni elemento il prima possibile e ciò significa che è in grado di intervenire contemporaneamente. Quindi le informazioni meno importanti vengono compilate come tempo consentito. Ecco le funzionalità della piattaforma che è possibile usare per implementare queste tecniche.

Segnaposto

La funzionalità degli oggetti visivi segnaposto temporanei è attivata per impostazione predefinita ed è controllata con la proprietà ShowsScrollingPlaceholders. Durante la panoramica/lo scorrimento rapido, questa funzionalità fornisce all'utente un suggerimento visivo mostrando che vi sono ancora più elementi da visualizzare completamente mantenendo allo stesso tempo la fluidità. Se si usa una delle tecniche seguenti, è possibile impostare ShowsScrollingPlaceholders su false se si preferisce non avere i segnaposto di rendering del sistema.

Aggiornamenti progressivi del modello di dati tramite x:Phase

Ecco come usare l'attributo x:Phase con binding{x:Bind} per implementare gli aggiornamenti progressivi dei modelli di dati.

  1. Ecco l'aspetto dell'origine del binding (questa è l'origine dati a cui verrà associato).

    namespace LotsOfItems
    {
        public class ExampleItem
        {
            public string Title { get; set; }
            public string Subtitle { get; set; }
            public string Description { get; set; }
        }
    
        public class ExampleItemViewModel
        {
            private ObservableCollection<ExampleItem> exampleItems = new ObservableCollection<ExampleItem>();
            public ObservableCollection<ExampleItem> ExampleItems { get { return this.exampleItems; } }
    
            public ExampleItemViewModel()
            {
                for (int i = 1; i < 150000; i++)
                {
                    this.exampleItems.Add(new ExampleItem(){
                        Title = "Title: " + i.ToString(),
                        Subtitle = "Sub: " + i.ToString(),
                        Description = "Desc: " + i.ToString()
                    });
                }
            }
        }
    }
    
  2. Ecco il markup che DeferMainPage.xaml contiene. La visualizzazione griglia contiene un modello di elemento con elementi associati alle proprietà Titolo, Sottotitoloe Descrizione della classe MyItem. Si noti che per impostazione predefinita x:Phase è 0. In questo caso, il rendering degli elementi verrà inizialmente eseguito con solo il titolo visibile. L'elemento del sottotitolo sarà quindi associato ai dati e reso visibile per tutti gli elementi e così via fino a quando non vengono elaborate tutte le fasi.

    <Page
        x:Class="LotsOfItems.DeferMainPage"
        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:lotsOfItems="using:LotsOfItems"
        mc:Ignorable="d">
    
        <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
            <GridView ItemsSource="{x:Bind ViewModel.ExampleItems}">
                <GridView.ItemTemplate>
                    <DataTemplate x:DataType="lotsOfItems:ExampleItem">
                        <StackPanel Height="100" Width="100" Background="OrangeRed">
                            <TextBlock Text="{x:Bind Title}"/>
                            <TextBlock Text="{x:Bind Subtitle}" x:Phase="1"/>
                            <TextBlock Text="{x:Bind Description}" x:Phase="2"/>
                        </StackPanel>
                    </DataTemplate>
                </GridView.ItemTemplate>
            </GridView>
        </Grid>
    </Page>
    
  3. Se si esegue l'app ora e si scorre rapidamente attraverso la visualizzazione griglia, si noterà che quando ogni nuovo elemento viene visualizzato sullo schermo, in un primo momento viene eseguito il rendering come rettangolo grigio scuro (grazie alla proprietà ShowsScrollingPlaceholders predefinita per true), quindi viene visualizzato il titolo, seguito dal sottotitolo, seguito dalla descrizione.

Aggiornamenti progressivi del modello di dati tramite ContainerContentChanging

La strategia generale per l'evento ContainerContentChanging consiste nell'usare Opacity per nascondere gli elementi che non devono essere immediatamente visibili. Quando gli elementi vengono riciclati, manterranno i valori precedenti in modo da nascondere tali elementi fino a quando tali valori non verranno aggiornati dal nuovo elemento dati. Per determinare quali elementi aggiornare e visualizzare, viene usata la proprietà Phase sugli argomenti dell'evento. Se sono necessarie fasi aggiuntive, si registra un callback.

  1. Verrà usata la stessa origine di associazione di x:Phase.

  2. Ecco il markup che MainPage.xaml contiene. La visualizzazione griglia dichiara un gestore all'evento ContainerContentChanging e contiene un modello di elemento con elementi utilizzati per visualizzare le proprietà Titolo, Sottotitolo e Descrizione della classeMyItem. Per ottenere i vantaggi massimi delle prestazioni dell'uso di ContainerContentChanging, non vengono usate associazioni nel markup, ma vengono assegnati valori a livello di codice. L'eccezione qui è l'elemento che visualizza il titolo, che consideriamo essere nella fase 0.

    <Page
        x:Class="LotsOfItems.MainPage"
        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:lotsOfItems="using:LotsOfItems"
        mc:Ignorable="d">
    
        <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
            <GridView ItemsSource="{x:Bind ViewModel.ExampleItems}" ContainerContentChanging="GridView_ContainerContentChanging">
                <GridView.ItemTemplate>
                    <DataTemplate x:DataType="lotsOfItems:ExampleItem">
                        <StackPanel Height="100" Width="100" Background="OrangeRed">
                            <TextBlock Text="{x:Bind Title}"/>
                            <TextBlock Opacity="0"/>
                            <TextBlock Opacity="0"/>
                        </StackPanel>
                    </DataTemplate>
                </GridView.ItemTemplate>
            </GridView>
        </Grid>
    </Page>
    
  3. Infine, ecco l'implementazione del gestore eventi ContainerContentChanging. Questo codice illustra anche come aggiungere una proprietà di tipo RecordingViewModel a MainPage per esporre la classe di origine di binding dalla classe che rappresenta la pagina di markup. Se non si dispone di alcun binding {Binding} nel modello di dati, contrassegnare quindi l'oggetto argomenti evento come gestito nella prima fase del gestore per suggerire all'elemento che non è necessario impostare un contesto dati.

    namespace LotsOfItems
    {
        /// <summary>
        /// An empty page that can be used on its own or navigated to within a Frame.
        /// </summary>
        public sealed partial class MainPage : Page
        {
            public MainPage()
            {
                this.InitializeComponent();
                this.ViewModel = new ExampleItemViewModel();
            }
    
            public ExampleItemViewModel ViewModel { get; set; }
    
            // Display each item incrementally to improve performance.
            private void GridView_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
            {
                if (args.Phase != 0)
                {
                    throw new System.Exception("We should be in phase 0, but we are not.");
                }
    
                // It's phase 0, so this item's title will already be bound and displayed.
    
                args.RegisterUpdateCallback(this.ShowSubtitle);
    
                args.Handled = true;
            }
    
            private void ShowSubtitle(ListViewBase sender, ContainerContentChangingEventArgs args)
            {
                if (args.Phase != 1)
                {
                    throw new System.Exception("We should be in phase 1, but we are not.");
                }
    
                // It's phase 1, so show this item's subtitle.
                var templateRoot = args.ItemContainer.ContentTemplateRoot as StackPanel;
                var textBlock = templateRoot.Children[1] as TextBlock;
                textBlock.Text = (args.Item as ExampleItem).Subtitle;
                textBlock.Opacity = 1;
    
                args.RegisterUpdateCallback(this.ShowDescription);
            }
    
            private void ShowDescription(ListViewBase sender, ContainerContentChangingEventArgs args)
            {
                if (args.Phase != 2)
                {
                    throw new System.Exception("We should be in phase 2, but we are not.");
                }
    
                // It's phase 2, so show this item's description.
                var templateRoot = args.ItemContainer.ContentTemplateRoot as StackPanel;
                var textBlock = templateRoot.Children[2] as TextBlock;
                textBlock.Text = (args.Item as ExampleItem).Description;
                textBlock.Opacity = 1;
            }
        }
    }
    
  4. Se si esegue l'app ora e si scorre rapidamente la visualizzazione griglia, si noterà lo stesso comportamento di x:Phase.

Riciclo del contenitore con raccolte eterogenee

In alcune applicazioni è necessario avere un'interfaccia utente diversa per diversi tipi di elemento all'interno di una raccolta. Ciò può creare una situazione in cui è impossibile virtualizzare i pannelli per riutilizzare/riciclare gli elementi visivi usati per visualizzare gli elementi. Ricreare gli elementi visivi per un elemento durante la panoramica annulla molte delle prestazioni offerte dalla virtualizzazione. Tuttavia, una piccola pianificazione può consentire la virtualizzazione dei pannelli per riutilizzare gli elementi. Gli sviluppatori hanno un paio di opzioni a seconda dello scenario: l'evento ChoosingItemContainer o un selettore di modelli di elemento. L'approccio ChoosingItemContainer offre prestazioni migliori.

Evento ChoosingItemContainer

ChoosingItemContainer è un evento che consente di fornire un elemento (ListViewItem/GridViewItem) a ListView/GridView ogni volta che è necessario un nuovo elemento durante l'avvio o il riciclo. È possibile creare un contenitore in base al tipo di elemento di dati visualizzato dal contenitore (illustrato nell'esempio seguente). ChoosingItemContainer è il modo più efficiente per usare modelli di dati diversi per elementi diversi. La memorizzazione nella cache dei contenitori è un risultato che può essere ottenuto usando ChoosingItemContainer. Ad esempio, se si dispone di cinque modelli diversi, con un modello che si verifica un ordine di grandezza più spesso rispetto agli altri, ChoosingItemContainer consente non solo di creare elementi in base alle proporzioni necessarie, ma anche di mantenere un numero appropriato di elementi memorizzati nella cache e disponibili per il riciclo. ChoosingGroupHeaderContainer offre le stesse funzionalità per le intestazioni di gruppo.

// Example shows how to use ChoosingItemContainer to return the correct
// DataTemplate when one is available. This example shows how to return different 
// data templates based on the type of FileItem. Available ListViewItems are kept
// in two separate lists based on the type of DataTemplate needed.
private void ListView_ChoosingItemContainer
    (ListViewBase sender, ChoosingItemContainerEventArgs args)
{
    // Determines type of FileItem from the item passed in.
    bool special = args.Item is DifferentFileItem;

    // Uses the Tag property to keep track of whether a particular ListViewItem's 
    // datatemplate should be a simple or a special one.
    string tag = special ? "specialFiles" : "simpleFiles";

    // Based on the type of datatemplate needed return the correct list of 
    // ListViewItems, this could have also been handled with a hash table. These 
    // two lists are being used to keep track of ItemContainers that can be reused.
    List<UIElement> relevantStorage = special ? specialFileItemTrees : simpleFileItemTrees;

    // args.ItemContainer is used to indicate whether the ListView is proposing an 
    // ItemContainer (ListViewItem) to use. If args.Itemcontainer, then there was a 
    // recycled ItemContainer available to be reused.
    if (args.ItemContainer != null)
    {
        // The Tag is being used to determine whether this is a special file or 
        // a simple file.
        if (args.ItemContainer.Tag.Equals(tag))
        {
            // Great: the system suggested a container that is actually going to 
            // work well.
        }
        else
        {
            // the ItemContainer's datatemplate does not match the needed 
            // datatemplate.
            args.ItemContainer = null;
        }
    }

    if (args.ItemContainer == null)
    {
        // see if we can fetch from the correct list.
        if (relevantStorage.Count > 0)
        {
            args.ItemContainer = relevantStorage[0] as SelectorItem;
        }
        else
        {
            // there aren't any (recycled) ItemContainers available. So a new one 
            // needs to be created.
            ListViewItem item = new ListViewItem();
            item.ContentTemplate = this.Resources[tag] as DataTemplate;
            item.Tag = tag;
            args.ItemContainer = item;
        }
    }
}

Selettore del modello di elemento

Un selettore di modelli di elemento (DataTemplateSelector) consente a un'app di restituire un modello di elemento diverso in fase di esecuzione in base al tipo dell'elemento di dati che verrà visualizzato. Questo rende lo sviluppo più produttivo, ma rende più difficile la virtualizzazione dell'interfaccia utente perché non tutti i modelli di elemento possono essere riutilizzati per ogni elemento di dati.

Quando si ricicla un elemento (ListViewItem/GridViewItem), il framework deve decidere se gli elementi disponibili per l'uso nella coda di riciclo (la coda di riciclo è una cache di elementi che non vengono attualmente usati per visualizzare i dati) hanno un modello di elemento che corrisponde a quello desiderato dall'elemento di dati corrente. Se nella coda di riciclo non sono presenti elementi con il modello di elemento appropriato, viene creato un nuovo elemento e viene creata un'istanza del modello di elemento appropriato. Se, invece, la coda di riciclo contiene un elemento con il modello di elemento appropriato, tale elemento viene rimosso dalla coda di riciclo e viene usato per l'elemento di dati corrente. Un selettore di modelli di elemento funziona in situazioni in cui vengono usati solo un numero ridotto di modelli di elemento ed è presente una distribuzione flat in tutta la raccolta di elementi che usano modelli di elementi diversi.

Quando si verifica una distribuzione non uniforme degli elementi che usano modelli di elemento diversi, è probabile che sia necessario creare nuovi modelli di elemento durante la panoramica e questo nega molti dei vantaggi offerti dalla virtualizzazione. Inoltre, un selettore di modelli di elemento considera solo cinque possibili candidati quando si valuta se un determinato contenitore può essere riutilizzato per l'elemento di dati corrente. È quindi consigliabile valutare attentamente se i dati sono appropriati per l'uso con un selettore di modelli di elemento prima di usarne uno nell'app. Se la raccolta è per lo più omogenea, il selettore restituisce lo stesso tipo più (possibilmente tutto) del tempo. Tieni presente solo il prezzo che stai pagando per le rare eccezioni a tale omogeneità e valuta se usare ChoosingItemContainer (o due controlli elementi) è preferibile.