Xamarin.Forms の大きな SQLite テーブルをシームレスにスクロールし、メモリ オーバーヘッドを抑える
InfragisticsのXamarin.Forms/Xamarin.Android/Xamarin.iOS DataGrid(XamDataGrid)をすでに使用している場合は、非常に優れたトリックを知っていることに気付くでしょう。
リモート OData サービスにバインドして行をスクロールすると、移動速度を使用してデータをフェッチする必要があるタイミングを予測し、そこに到達する前にシームレスに読み込むことができます。このトリックをまだ見ていない場合は、間違いなくサンプルブラウザをチェックしてくださいUltimate UI for Xamarin、これをよく示しています。
Running the Sample
この記事で作成するサンプルは、こちらから入手できます。サンプルを開いたら、ローカル リポジトリに試用版または RTM Nuget パッケージがあることを確認し、Nuget パッケージを復元する必要があります。
他のデータソースへのアクセスの仮想化
サンプルブラウザとドキュメント内のリモートデータサンプルには、リモートODataサービスをグリッドにロードする方法について多くの詳細が記載されています。しかしそれだけにとどまりません。さらに、他の種類のリモートやローカルデータへのアクセスを仮想化するために、自分だけのカスタムVirtualDataSourceバージョンを作成することも可能です。実際、最近ではお客様から、テーブルの全データをメモリに読み込まずにSQLiteデータベースでデータグリッドを使えるかどうか尋ねられました。グリッド上のItemsSourceプロパティに直接コレクションを提供したい場合は必要ですが、拡張すればより良い方法がありますVirtualDataSource。でも運がいいな、もうやったよ。
そのプロジェクトを作れば、私たちのSQLite専用バージョンができVirtualDataSource。これにより、テーブルや連結されたテーブルのセットにリンクし、まるで大きく途切れない連続したコレクションをスクロールするかのようにシームレスにページをめくることができます。さらに良いのは、データソースが一度にメモリに保持するデータページ数を制限することで、モバイルアプリケーションでのメモリ使用量に上限を設けられることです。
SQLite Database Setup
では、実践に移しましょう。Xamarin.FormsプロジェクトがXamDataGridを使って構築されているなら、まずAndroidアプリとiOSアプリにSQLiteデータベースを追加する必要があります。Androidアプリについては、以下のアセットに入ります:
データベースのBuild Actionは次のようにマークしてくださいAndroidAsset:
このロジックをXamarin.Formsの初期化直前、メインアプリが作成される直前にMainActivity.cs置くと、実行時にSQLiteデータベースがアプリケーションにアクセスできるようにします。
string targetPath =
System.Environment.GetFolderPath(
System.Environment.SpecialFolder.Personal
);
var path = Path.Combine(
targetPath, "chinook.db");
if (!File.Exists(path))
{
using (Stream input =
Assets.Open("chinook.db"))
{
using (var fs = new FileStream(
path,
FileMode.Create))
{
input.CopyTo(fs);
}
}
}
iOSの場合は、データベースファイルをアプリケーションのResourcesに入れてください:
そして、Build ActionがBundleResourceに設定されていることを確認してください:
データベースファイルが正しく含まれていると、このロジックはXamarin.Formsの初期化直前でメインアプリが作成される前にAppDelegate.csに入れることで、実行時にiOSアプリケーションにアクセス可能であることを保証します。
var targetPath = Environment.GetFolderPath(
Environment.SpecialFolder.Personal);
targetPath = Path.Combine(targetPath, "..", "Library");
var path = Path.Combine(targetPath, "chinook.db");
if (!File.Exists(path))
{
var bundlePath = NSBundle.MainBundle.PathForResource(
"chinook",
"db"
);
File.Copy(bundlePath, path);
}
両プラットフォームにおいて、SQLiteデータベースへのファイルパスはXamarin.FormsApp作成時に渡すことができます。
LoadApplication(new App(path));
その後、アプリは、使用するページへのパスが利用可能であることを確認します。
public App(string dbPath)
{
InitializeComponent();
MainPage = new SQLDemo.MainPage(dbPath);
}
SQLiteテーブルをライブ仮想スクロール
SQLiteデータベースからデータを読み取るには、まずPCL(ポータブルクラスライブラリ)やXamarin.Android/Xamarin.iOSに対応したSQLiteクライアントが必要で、sqlite-net-pcl Nugetパッケージをインストールします。
SQLite.NET ライブラリには、POCOタイプに読み取られるデータをハイドレートするために使用する軽量のORMツールが含まれているため、最初に関心のあるテーブルのPOCOタイプを作成する必要があります。
using SQLite;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SQLDemo.Data
{
[Table("tracks")]
public class Track
{
[PrimaryKey, AutoIncrement]
public int TrackId { get; set; }
[MaxLength(200)]
public string Name { get; set; }
public int AlbumId { get; set; }
[Column("Title")]
public string AlbumTitle { get; set; }
public int MediaTypeId { get; set; }
public int GenreId { get; set; }
[MaxLength(220)]
public string Composer { get; set; }
public int Milliseconds { get; set; }
public int Bytes { get; set; }
public decimal UnitPrice { get; set; }
}
}
このタイプは、人気音楽アルバムの様々なトラックのサンプルデータを保存するChinook SQLiteサンプルデータベースのtracksテーブルにマッピングされます。ここでは属性を通じて、主キーや一部の文字列列の最大長さなど、テーブルに関する様々なメタ情報を示しています。
今やtracksテーブルからデータを読み込めるようになり、XamDataGridでそのテーブルをスクロールする設定が整いました。
まず、XAML でグリッドをレイアウトできます。
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:SQLDemo"
x:Class="SQLDemo.MainPage"
xmlns:igGrid="clr-namespace:Infragistics.XamarinForms.Controls.Grids;assembly=Infragistics.XF.DataGrid">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<igGrid:XamDataGrid x:Name="grid" RowHeight="90"
SelectionMode="MultipleRow"
HeaderClickAction="SortByMultipleColumnsTriState"
AutoGenerateColumns="False">
<igGrid:XamDataGrid.Columns>
<igGrid:TextColumn PropertyPath="Name"
LineBreakMode="WordWrap"
Width="1*"
/>
<igGrid:TextColumn PropertyPath="Composer"
LineBreakMode="Ellipsis"
Width="1.25*"/>
<igGrid:TextColumn PropertyPath="AlbumTitle"
HeaderText="Album Title"
LineBreakMode="WordWrap"
Width="1*"/>
<igGrid:NumericColumn PropertyPath="UnitPrice"
HeaderText="Unit Price"
MinFractionDigits="2"
Width="1*"/>
</igGrid:XamDataGrid.Columns>
</igGrid:XamDataGrid>
</Grid>
</ContentPage>
XAMLでは、メモリ内のデータをグリッドに割り当てるかのように、いくつかの列を定義XamDataGrid設定しています。列の定義を飛ばして自動生成させることもできたはずですが、tracksテーブルには列が十分にあるため、かなり混雑してしまいます。
では、SQLiteテーブルに対してグリッドをどのようにバインドしますか?まず、SQLiteデータベースと通信するための接続を作成する必要があります。
_connection = new SQLiteAsyncConnection(dbPath);
ここで、dbPath は、前に渡した SQLite データベースへのファイルパスです。次に、SQLiteVirtualDataSource を作成し、構成してグリッドに割り当てるだけです。
var dataSource = new SQLiteVirtualDataSource();
dataSource.Connection = _connection;
dataSource.TableExpression =
"tracks left outer join albums on tracks.AlbumId = albums.AlbumId";
dataSource.ProjectionType = typeof(Track);
grid.ItemsSource = dataSource;
ここでは、
- 作成した接続を仮想データ ソースに提供します。
- 仮想データソースにテーブル式を提供して、データをプルするテーブルを示します。
- データ行をハイドレートするために作成した POCO タイプを指定します。
TableExpressionwe could had providedtracksでは、この例ではアルバムタイトルを調べてAlbumTitleプロパティに入力できるように、アルバムテーブルに対してジョインを作成します。
以上です!アプリを実行すると、テーブルを 1 つの長い連続したレコード セットであるかのようにスクロールできることがわかります。ただし、実際には、テーブルのごく一部が一度にデバイスのメモリに存在します。ロードされる前に一部のレコードに到達するシナリオを見るのに十分な速さでスクロールするのが難しい場合があります。これは、グリッドが実際に予測的にレコードをロードするためです。次のように表示されます。

ただし、列ヘッダーをタップしてグリッドの種類を変更すると、グリッドが追いついていることがわかります。これにより、現在のクライアント側のデータは無効になり、要求どおりにソートされた新しいデータがフェッチされますが、ここでも必要な分だけフェッチされます。
フィルタリングの追加
では、それを受けて、もう少しおしゃれにしましょう。まず、ページの XAML のグリッドに以下を追加します。
<StackLayout Orientation="Horizontal" Grid.Row="1">
<Label Text="Filter" />
<Entry TextChanged="Entry_TextChanged" WidthRequest="300" />
</StackLayout>
そのマークアップにより、表示しているテーブルをフィルタリングするためのフィルター値を収集できるように、入力フィールドが追加されました。エントリのテキストが変更されるたびにイベントが発生します。それでは、そのためのハンドラを分離コードに追加しましょう。
private void Entry_TextChanged(object sender, TextChangedEventArgs e)
{
if (String.IsNullOrEmpty(e.NewTextValue))
{
grid.FilterExpressions.Clear();
}
else
{
grid.FilterExpressions.Clear();
grid.FilterExpressions.Add(FilterFactory.Build(
(f) =>
{
return f.Property("Name").Contains(e.NewTextValue)
.Or(f.Property("AlbumTitle").Contains(e.NewTextValue))
.Or(f.Property("Composer").Contains(e.NewTextValue));
}));
}
}
このコードは、入力フィールドが空白になった場合はグリッド フィルターをクリアしますが、それ以外の場合は、Name、AlbumTitle、または Composer が指定された文字列と一致するかどうかを確認し、そのフィルターが SQLite に渡されるクエリで使用されることを確認するフィルターを作成します。
サンプルは次のようになります。

ご覧のとおり、文字を入力するたびに、ローカル グリッドは新しいフィルター処理されたコンテンツでそのコンテンツを更新する必要があり、その後、全体をスクロールできます。
詳細については、「Write Fast」と「Run Fast」のレッスンとビデオをご覧ください。また、Infragistics Ultimate UI for Xamarinの無料トライアルも必ずダウンロードすることをお勧めします。
Graham Murrayは、ソフトウェア アーキテクトであり、著者です。彼は、デスクトップ、Web、モバイルにまたがる Infragistics 向けの高性能クロスプラットフォーム UI コンポーネントを構築しています。Twitterで@the_grahamをフォローしてください。