【実務・中級編】【中級者・実務】WPFのDataGridパフォーマンス最適化:大量行データを扱う際の仮想化(UI Virtualization)設定 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク
スポンサーリンク

数万件のデータを「秒」で捌く。WPF DataGrid仮想化の極意と実装戦略

業務効率化ツールの開発において、避けて通れないのが「大量データの表示」だ。
DBから抽出した数万件のレコードを`DataGrid`に放り込み、動作を確認して絶望する。スクロールがカクつき、メモリ消費量が跳ね上がり、最悪の場合はアプリケーションがフリーズする。

多くの開発者がここで「PCのスペック不足」や「VB.NETの限界」だと勘違いするが、それは大きな間違いだ。

原因は単純。「ビジュアルツリーの爆発」を起こしているからだ。
WPFのDataGridは、デフォルトで仮想化機能を持っているが、ある「あるあるな設計ミス」を一つ犯しただけで、その機能は完全に死ぬ。

本記事では、プロのアーキテクトが実践している、メモリを圧迫せず、極めてスムーズに動作するDataGridの最適化手法を伝授する。

1. なぜDataGridは重くなるのか?(ビジュアルツリーの正体)

WPFにおいて、画面に見えているすべての要素は「ビジュアルツリー」という階層構造で管理されている。
もし10,000行のデータを仮想化なしで表示しようとすれば、WPFは内部的に10,000個の`DataGridRow`オブジェクトと、その中のセル、テキストブロックなどの数万個のUI要素をメモリ上に生成しようとする。

これが「ビジュアルツリーの爆発」だ。CPUは描画計算に追われ、GC(ガベージコレクション)は大量のオブジェクト管理に悲鳴を上げる。

解決策:UI仮想化(UI Virtualization)

UI仮想化とは、「今、画面に見えている範囲のUI要素だけを生成し、画面外に出た要素は使い回す(リサイクルする)」技術だ。
10,000行あっても、画面に20行しか映っていないなら、UI要素は20〜30個分あれば十分。これがプロの設計だ。

2. 【致命的な罠】仮想化を殺す「禁忌」の設計

ここが最重要だ。多くのエンジニアがやりがちな、仮想化を完全に無効化してしまう実装がある。

禁忌:DataGridを `ScrollViewer` や `StackPanel` で囲むこと

`DataGrid`を`StackPanel`に入れたり、親要素に`ScrollViewer`を配置して「全体のスクロール」を実現しようとしていないか?

これをやった瞬間、仮想化は死ぬ。

なぜか。`StackPanel`や`ScrollViewer`は、子要素に対して「無限の高さ(または幅)」を割り当てる。`DataGrid`は「無限の高さがある」と判断し、全行を一度に描画しようとする。結果、仮想化が機能せず、アプリは死ぬ。

正解: `DataGrid`は`Grid`の`RowDefinition Height=”*”` など、明確な制約(境界)があるコンテナに配置せよ。

3. 実装:プロダクションレベルの最適化コード

それでは、具体的にどう書くべきか。保守性が高く、パフォーマンスを極限まで引き出す実装例を示す。

XAML: 仮想化設定を完全に掌握する

単に`DataGrid`を置くのではなく、`VirtualizingStackPanel`のプロパティを明示的に指定せよ。

<!-- 
    ポイント:
    1. VirtualizingStackPanel.IsVirtualizing="True" : 仮想化を強制有効化
    2. VirtualizingStackPanel.VirtualizationMode="Recycling" : 
       StandardではなくRecyclingを使う。これにより、UI要素を破棄せず再利用し、GC負荷を激減させる。
    3. ScrollUnit="Pixel" : 
       デフォルトのItem単位ではなくPixel単位にすることで、滑らかなスクロールを実現する。
-->
<Grid>
    <DataGrid x:Name="dgMain" 
              ItemsSource="{Binding LargeDataSet}" 
              AutoGenerateColumns="False"
              EnableRowVirtualization="True"
              EnableColumnVirtualization="True"
              VirtualizingStackPanel.IsVirtualizing="True"
              VirtualizingStackPanel.VirtualizationMode="Recycling"
              VirtualizingStackPanel.ScrollUnit="Pixel"
              CanUserAddRows="False"
              IsReadOnly="True">
        
        <DataGrid.Columns>
            <DataGridTextColumn Header="ID" Binding="{Binding Id}" Width="100" />
            <DataGridTextColumn Header="名称" Binding="{Binding Name}" Width="200" />
            <DataGridTextColumn Header="更新日時" Binding="{Binding UpdatedAt}" Width="150" />
            <DataGridTextColumn Header="詳細データ" Binding="{Binding Detail}" Width="*" />
        </DataGrid.Columns>
    </DataGrid>
</Grid>

VB.NET: 高効率なデータバインディング

ViewModel側では、`ObservableCollection(Of T)` を使用する。また、大量データを一気に流し込む際は、UIスレッドをブロックしない配慮が必要だ。

Imports System.Collections.ObjectModel
Imports System.ComponentModel
Imports System.Runtime.CompilerServices

''' <summary>
''' 基底ViewModel:通知プロパティの共通化
''' </summary>
Public Class BaseViewModel
    Implements INotifyPropertyChanged

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Protected Sub OnPropertyChanged(<CompilerServices.CallerMemberName> Optional propertyName As String = Nothing)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub
End Class

''' <summary>
''' データモデル:軽量に保つことが肝要
''' </summary>
Public Class RecordModel
    Public Property Id As Integer
    Public Property Name As String
    Public Property UpdatedAt As String
    Public Property Detail As String
End Class

''' <summary>
''' メインViewModel:大量データを制御する
''' </summary>
Public Class MainViewModel
    Inherits BaseViewModel

    ' 外部からバインドされるコレクション
    Public Property LargeDataSet As ObservableCollection(Of RecordModel)

    Public Sub New()
        LargeDataSet = New ObservableCollection(Of RecordModel)()
    End Sub

    ''' <summary>
    ''' データの読み込み処理
    ''' </summary>
    Public Async Function LoadHugeDataAsync() As Task
        ' DBやファイルからの読み込みをシミュレート(非同期で実行しUIを固めない)
        Dim data = await Task.Run(Function()
                                      Dim list As New List(Of RecordModel)()
                                      For i As Integer = 1 To 50000
                                          list.Add(New RecordModel With {
                                              .Id = i,
                                              .Name = $"アイテム {i}",
                                              .UpdatedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
                                              .Detail = $"これは{i}番目の詳細なデータです。メモリ効率を最大化します。"
                                          })
                                      Next
                                      Return list
                                  End Function)

        ' コレクションを一気に更新
        ' ※大量追加時は、一旦Listで作成してからObservableCollectionに入れるのが定石
        LargeDataSet.Clear()
        For Each item In data
            LargeDataSet.Add(item)
        Next
    End Function
End Class

4. さらに上を目指すための「極限チューニング」

上記の実装で十分高速に動作するはずだが、さらに数百万件レベルのデータを扱う場合や、メモリ制約が厳しい場合は以下の戦略を検討せよ。

① データ仮想化(Data Virtualization)

UI仮想化は「見た目」をサボる技術だが、`ObservableCollection`に5万件入っていれば、その分メモリ(RAM)は消費する。
真のプロは、`IList`インターフェースを自作実装し、インデクサ(`this[index]`)が呼ばれた瞬間にDBから必要な範囲だけをフェッチする「データ仮想化」を実装する。これにより、メモリ消費量をほぼゼロに抑えられる。

② 列の仮想化(Column Virtualization)

行だけでなく、列数が極端に多い(100列など)場合は、`EnableColumnVirtualization=”True”` を設定せよ。行と同様に、画面外の列は生成されない。

③ `Auto`サイズの排除

`Width=”Auto”` は、WPFに「全要素のサイズを計算して、最大幅を決定せよ」という命令を下す。これは仮想化と相性が最悪だ。可能な限り `Width=”100″`(固定)か `Width=”*”`(比率)を使用せよ。

5. まとめ:アーキテクトの視点

WPFの`DataGrid`パフォーマンス最適化の本質は、「いかにしてWPFに余計な仕事をさせないか」にある。

1. 境界を明確にせよ:`StackPanel`などで囲わず、`Grid`でサイズを拘束せよ。
2. リサイクルせよ:`VirtualizationMode=”Recycling”` でオブジェクト生成コストを削減せよ。
3. 計算をさせない:`Auto`サイズを避け、固定幅かスターサイズを使え。
4. 非同期で捌け:データロードでUIスレッドを止めるな。

この原則さえ守れば、VB.NETであってもモダンで爆速なUIを構築することは十分可能だ。ツールを作る者は、単に機能を実現するだけでなく、その裏側で動く「オブジェクトのライフサイクル」に責任を持て。それが、プロとアマチュアを分ける境界線である。

タイトルとURLをコピーしました