次の方法で共有


ListView と GridView のパフォーマンスを最適化する

メモ 詳細については、「//build/ セッション」を参照してください。ユーザーが GridView と ListView で大量のデータを操作すると、パフォーマンスが大幅に向上します。

UI の仮想化、要素の削減、項目の段階的な更新により、 ListViewGridView のパフォーマンスと起動時間を向上させます。 データ仮想化の手法については、 WinUI の ListView と GridView のデータ仮想化に関する説明を参照してください。

収集パフォーマンスの 2 つの重要な要因

コレクションの操作は一般的なシナリオです。 フォト ビューアーには写真のコレクションがあり、リーダーには記事、書籍、またはストーリーのコレクションがあり、ショッピング アプリには製品のコレクションがあります。 このトピックでは、コレクションの操作で WinUI アプリを効率的にするためにできることについて説明します。

コレクションに関しては、パフォーマンスに 2 つの重要な要因があります。1 つは、UI スレッドが項目を作成するために費やした時間です。もう 1 つは、生データ セットと、そのデータのレンダリングに使用される UI 要素の両方で使用されるメモリです。

スムーズなパンとスクロールを行うには、UI スレッドが、項目のインスタンス化、データ バインディング、レイアウトの効率的でスマートなジョブを実行することが重要です。

UI 仮想化

UI 仮想化は、可能な最も重要な改善点です。 つまり、項目を表す UI 要素は必要に応じて作成されます。 1000 項目コレクションにバインドされた項目コントロールの場合、すべての項目を同時に表示できないため、すべての項目の UI を同時に作成するのはリソースの無駄になります。 ListViewGridView (およびその他の標準 の ItemsControl 派生コントロール) は、UI 仮想化を実行します。 項目がスクロールされに近づくと (数ページ先)、フレームワークによって項目の UI が生成され、キャッシュされます。 項目が再び表示される可能性が低い場合は、フレームワークによってメモリが再利用されます。

カスタム項目パネル テンプレート ( ItemsPanel を参照) を指定する場合は、 ItemsWrapGridItemsStackPanel などの仮想化パネルを使用してください。 VariableSizedWrapGridWrapGrid、または StackPanel を使用する場合、仮想化は行われません。 さらに、ItemsWrapGrid または ItemsStackPanel を使用する場合にのみ、次の ListView イベントが発生します。ChoosingGroupHeaderContainerChoosingItemContainerContainerContentChanging です。 Windows App SDK のカスタム レイアウトの場合、組み込みの項目パネルがニーズを満たしていない場合の VirtualizingLayout ベースの実装が最新の同等です。

ビューポートの概念は、フレームワークが表示される可能性が高い要素を作成する必要があるため、UI 仮想化にとって重要です。 一般に、 ItemsControl のビューポートは論理コントロールの範囲です。 たとえば、 ListView のビューポートは、 ListView 要素の幅と高さです。 一部のパネルでは、 ScrollViewer や自動サイズの行または列を含む グリッド など、子要素に無制限の領域を使用できます。 仮想化された ItemsControl がこのようなパネルに配置されると、そのすべての項目を表示するのに十分なスペースが必要になります。これは仮想化を損ないます。 ItemsControl で幅と高さを設定して仮想化を復元します。

項目ごとの要素の削減

項目のレンダリングに使用される UI 要素の数は、妥当な最小値のままにします。

項目コントロールが最初に表示されると、項目でいっぱいのビューポートをレンダリングするために必要なすべての要素が作成されます。 また、項目がビューポートに近づくと、フレームワークはキャッシュされた項目テンプレートの UI 要素をバインドされたデータ オブジェクトで更新します。 テンプレート内のマークアップの複雑さを最小限に抑えることで、メモリと UI スレッドに費やされた時間が短縮され、特にパンとスクロール中の応答性が向上します。 問題のテンプレートは、項目テンプレート (ItemTemplate を参照) と、ListViewItem または GridViewItem (項目コントロール テンプレートまたは ItemContainerStyle) のコントロール テンプレートです。 要素数のわずかな減少による利点は、表示される項目の数に比例して増大します。

要素の削減の例については、「 WinUI と Windows App SDK の XAML 読み込みの最適化」を参照してください。

ListViewItem および GridViewItem の既定のコントロール テンプレートには、ListViewItemPresenter 要素が含まれています。 この発表者は、フォーカス、選択、およびその他の表示状態の複雑なビジュアルを表示する 1 つの最適化された要素です。 既にカスタム項目コントロール テンプレート (ItemContainerStyle) がある場合、または今後項目コントロール テンプレートのコピーを編集する場合は、ほとんどの場合、その要素によってパフォーマンスとカスタマイズ性のバランスが最適になるため、 ListViewItemPresenter を使用することをお勧めします。 発表者をカスタマイズする場合は、発表者にプロパティを設定します。 たとえば、項目が選択されたときに既定で表示されるチェック マークを削除し、選択した項目の背景色をオレンジ色に変更するマークアップを次に示します。

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

SelectionCheckMarkVisualEnabledSelectedBackground に似た自己記述型の名前を持つプロパティは約 25 個あります。 発表者の種類がユース ケースに十分にカスタマイズできないことが証明された場合は、代わりに ListViewItemExpanded または GridViewItemExpanded コントロール テンプレートのコピーを編集できます。 WinUI アプリで、現在の既定のテンプレートの Windows App SDK パッケージに付属している generic.xaml ファイルを確認します。 これらのテンプレートを使用すると、カスタマイズの向上と引き換えにパフォーマンスが低下することに注意してください。

ListView と GridView の項目を段階的に更新する

データ仮想化を使用している場合は、ダウンロード中の項目の一時的な UI 要素をレンダリングするようにコントロールを構成することで、 ListViewGridView の応答性を高く保つことができます。 一時要素は、データの読み込み時に実際の UI に徐々に置き換えられます。

また、(ローカル ディスク、ネットワーク、またはクラウド) からデータを読み込んでいる場所に関係なく、ユーザーは ListView または GridView を迅速にパンまたはスクロールできるため、スムーズなパンとスクロールを維持しながら、各項目を完全に忠実にレンダリングすることはできません。 スムーズなパンとスクロールを維持するために、プレースホルダーを使用するだけでなく、複数のフェーズで項目をレンダリングすることもできます。

これらの手法の例は、写真表示アプリでよく見られます。すべての画像が読み込まれて表示されていない場合でも、ユーザーはコレクションをパン、スクロール、操作できます。 または、映画アイテムの場合は、第 1 フェーズのタイトル、第 2 フェーズの評価、および第 3 フェーズのポスターの画像を表示できます。 ユーザーはできるだけ早く各項目に関する最も重要なデータを見て、一度にアクションを実行できることを意味します。 その後、時間の許す限り、重要度の低い情報が入力されます。 これらの手法を実装するために使用できるプラットフォーム機能を次に示します。

プレースホルダー

一時的なプレースホルダービジュアル機能は既定でオンであり、 ShowsScrollingPlaceholders プロパティを使用して制御されます。 高速パンとスクロール中に、この機能を使用すると、滑らかさを維持しながら、さらに多くの項目がまだ完全に表示されていないという視覚的ヒントがユーザーに提供されます。 以下のいずれかの手法を使用する場合は、システム レンダリング プレースホルダーを使用しない場合は 、ShowsScrollingPlaceholders を false に設定できます。

x:Phase を使用したプログレッシブ データ テンプレートの更新

x:Phase属性は WinUI で引き続き機能し、項目のコンテンツを段階的にレンダリングするための優れた方法です。

x:Phase 属性{x:Bind} バインドと共に使用して、プログレッシブ データ テンプレートの更新を実装する方法を次に示します。

  1. バインド ソースの外観を次に示します (これはバインド先のデータ ソースです)。

    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. DeferMainPage.xamlに含まれるマークアップを次に示します。 グリッド ビューには、MyItem クラスの TitleSubtitleDescription プロパティにバインドされた要素を含む項目テンプレートが含まれています。 x:Phase の既定値は 0 であることに注意してください。 ここでは、項目は最初はタイトルだけが表示された状態でレンダリングされます。 その後、サブタイトル要素はデータバインドされ、すべての項目に対して表示されます。また、すべてのフェーズが処理されるまで表示されます。

    <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. アプリを今すぐ実行し、グリッド ビューをすばやくパンまたはスクロールすると、新しい項目が画面に表示されると、最初は濃い灰色の四角形としてレンダリングされます ( ShowsScrollingPlaceholders プロパティの既定値が true に設定されているため)、タイトルが表示され、その後にサブタイトルが続いて説明が表示されます。

ContainerContentChanging を使用したプログレッシブ データ テンプレートの更新

ContainerContentChanging イベントの一般的な戦略は、Opacity を使用して、すぐに表示する必要のない要素を非表示にすることです。 要素がリサイクルされると、古い値が保持されるため、新しいデータ項目からこれらの値が更新されるまで、それらの要素を非表示にしたいと考えています。 イベント引数の Phase プロパティを使用して、更新して表示する要素を決定します。 追加のフェーズが必要な場合は、コールバックを登録します。

  1. x:Phase の場合と同じバインディング ソースを使用します。

  2. MainPage.xamlに含まれるマークアップを次に示します。 グリッド ビューは、ContainerContentChanging イベントのハンドラーを宣言し、MyItem クラスの TitleSubtitleDescription プロパティを表示するために使用される要素を含む項目テンプレートを含みます。 ContainerContentChanging を使用する場合のパフォーマンス上の最大の利点を得るために、マークアップではバインドを使用せず、代わりにプログラムで値を割り当てます。 ここでの例外は、タイトルを表示する要素であり、フェーズ 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. 最後に、 ContainerContentChanging イベント ハンドラーの実装を次に示します。 このコードでは、 ExampleItemViewModel 型のプロパティを MainPage に追加して、マークアップのページを表すクラスからバインディング ソース クラスを公開する方法も示します。 データ テンプレートに {Binding} バインドがない限り、イベント引数オブジェクトをハンドラーの最初のフェーズで処理済みとしてマークし、データ コンテキストを設定する必要がない項目をヒントします。

    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. ここでアプリを実行し、グリッド ビューをすばやくパンまたはスクロールすると、 x:Phase の場合と同じ動作が表示されます。

異種コレクションを使用したコンテナーのリサイクル

一部のアプリケーションでは、コレクション内のさまざまな種類の項目に対して異なる UI を使用する必要があります。 これにより、項目の表示に使用されるビジュアル要素を再利用またはリサイクルするために、パネルを仮想化できない状況が発生する可能性があります。 パン操作中に項目のビジュアル要素を再作成すると、仮想化によって提供される多くのパフォーマンス向上が失われます。 ただし、少し計画すると、パネルを仮想化して要素を再利用できます。 開発者には、シナリオに応じて、 ChoosingItemContainer イベントまたは項目テンプレート セレクターという 2 つのオプションがあります。 ChoosingItemContainer アプローチの方がパフォーマンスが向上します。

ChoosingItemContainer イベント

ChoosingItemContainer は、起動時またはリサイクル中に新しい項目が必要になったときに、項目 (ListViewItem または GridViewItem) を ListView または GridView に 提供できるイベントです。 次の例に示すように、コンテナーに表示されるデータ項目の種類に基づいてコンテナーを作成できます。 ChoosingItemContainer は、項目ごとに異なるデータ テンプレートを使用する、より高いパフォーマンスの方法です。 コンテナー のキャッシュは、 ChoosingItemContainer を使用して実現できます。 たとえば、5 つの異なるテンプレートがあり、1 つのテンプレートが他のテンプレートよりも 1 桁多く発生している場合、 ChoosingItemContainer を使用すると、必要な比率で項目を作成できるだけでなく、適切な数の要素をキャッシュし、リサイクルに使用できるようにします。 ChoosingGroupHeaderContainer は、グループ ヘッダーに対して同じ機能を提供します。

// 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 is not null, 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;
        }
    }
}

項目テンプレート セレクター

項目テンプレート セレクター (DataTemplateSelector) を使用すると、アプリは、表示されるデータ項目の種類に基づいて、実行時に別の項目テンプレートを返すことができます。 これにより開発の生産性が向上しますが、すべての項目テンプレートをすべてのデータ項目に再利用できるわけではないため、UI 仮想化がより困難になります。

項目 (ListViewItem または GridViewItem) をリサイクルする場合、フレームワークは、リサイクル キューで使用できる項目に、現在のデータ項目で必要なものと一致する項目テンプレートがあるかどうかを決定する必要があります。 リサイクル キューに適切な項目テンプレートを含む項目がない場合は、新しい項目が作成され、適切な項目テンプレートがインスタンス化されます。 一方、リサイクル キューに適切な項目テンプレートを含む項目が含まれている場合、その項目はリサイクル キューから削除され、現在のデータ項目に使用されます。 項目テンプレート セレクターは、少数の項目テンプレートのみが使用され、異なる項目テンプレートを使用する項目のコレクション全体にフラットな分布がある場合に機能します。   異なる項目テンプレートを使用する項目の分布が不均等な場合は、パン中に新しい項目テンプレートを作成する必要がある可能性が高く、仮想化によって提供される多くの利点を否定します。 さらに、項目テンプレート セレクターでは、特定のコンテナーを現在のデータ項目に再利用できるかどうかを評価する際に考慮できる候補は 5 つだけです。 そのため、WinUI アプリでデータを使用する前に、項目テンプレート セレクターで使用するデータが適切かどうかを慎重に検討する必要があります。 コレクションがほぼ同種の場合、セレクターはほとんどの場合、またはすべての時間で同じ型を返します。 同質性に対するまれな例外にかかるコストに注意し、ChoosingItemContainer または 2 つの項目コントロールの使用のどちらが望ましいかを考慮してください。