GridViewのスクロール位置を復元するとある方法
@ITのGridViewのスクロール位置を復元するには?の記事を読んでいて、うーむ正攻法だなぁと感心していました。GetTemplateChildメソッドがprotectedなので、正攻法ではGridViewを継承したコントロールを作らざるを得ないのも事実です。ちょっとしたアプリに、新しいコントロールを作りたくない私の場合は、別の方策を考えてみます。最初に考えたのが、何はなくともReflectionです。リフレクションは、Windowsストアアプリだとprotectedメンバーを残念ながら取得することができません。じゃあ、どうしようかと考えていてXAMLのVisualTreeを辿れれば、何とかなるのではなかろいうかという考えです。試しにGridViewコントロールに対して、VisualTreeを調べてみるとScrollViewerコントロールのインスタンスが存在しています。後は、このインスタンスを取り出せば、何とかなるだろうと考えて作成したのが、以下のコードになります。
static class VisualTreeExtension
{
// 指定したChildオブジェト型のインスタンスを返します
public static T GetChildObject<T>(DependencyObject start)
{
var children = GetDescendants(start);
var x = children.OfType<T>().ToList();
var i = x.FirstOrDefault();
return i;
}
// Childコレクションを作成します
internal static IEnumerable<DependencyObject> GetDescendants(
DependencyObject start)
{
var queue = new Queue<DependencyObject>();
var count = VisualTreeHelper.GetChildrenCount(start);
for (int i = 0; i < count; i++) // 1レベルの子要素を取得します
{
var child = VisualTreeHelper.GetChild(start, i);
yield return child;
queue.Enqueue(child);
}
while (queue.Count > 0)
{
var parent = queue.Dequeue();
var childCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childCount; i++) // 2レベル以降の子要素を取得します
{
var child = VisualTreeHelper.GetChild(parent, i);
yield return child;
queue.Enqueue(child);
}
}
}
}
このGetChildObjectメソッドをGetChild<ScrollViewer>(itemGridViewer)のように呼び出せば、ScrollViewerのインスタンスを取得することができます。 それでは、GroupedItemsPage.xaml.csにどのように組み込むかを説明します。
基本的な考え方は、他のページへ遷移するタイミングでScrollViewer.HorizontalOffsetを保存しておいて、GroupedItemsPageへ戻ってきた時に保存したHorizontalOffsetを読みだして、移動するというものです。このために、メンバー変数を追加し、とItemGridViewのLoadedイベントとSizeChangedイベントに次のコードを記述します。
ScrollViewer _sv;
double? _position;
// 起動時に移動させるロジック
private void itemGridView_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (_position.HasValue)
{
_sv = VisualTreeExtension.GetChildObject<ScrollViewer>(itemGridView);
_sv.ScrollToHorizontalOffset(_position.Value);
_position = null;
}
}
// SaveStateで移動量を取得するためにScrollViewerのインスタンスを取得
private void itemGridView_Loaded(object sender, RoutedEventArgs e)
{
_sv = VisualTreeExtension.GetChildObject<ScrollViewer>(itemGridView);
}
メンバー変数(_sv)にLoadSateメソッドではなく、LoadedイベントでScrollViewerのインスタンスを設定していることに注意してください。この理由は、OnNavigatedToイベントハンドラより呼び出されるLoadSateメソッドのタイミングでは、ScrollViewerなどのインスタンスが作成されていない場合があるためです。次に、ScrollViewerの移動量を保存するコードをSaveStateメソッドに記述し、LoadStateメソッドで保存した移動量を読みだすコードを記述します。
protected override void LoadState(Object navigationParameter,
Dictionary<String, Object> pageState)
{
// TODO: 問題のドメインでサンプル データを置き換えるのに適したデータ モデルを作成します
var sampleDataGroups = SampleDataSource.GetGroups((String)navigationParameter);
this.DefaultViewModel["Groups"] = sampleDataGroups;
if (pageState != null)
{
if (pageState.ContainsKey("position"))
{
_position = pageState["position"] as double?;
}
}
}
protected override void SaveState(Dictionary<string, object> pageState)
{
base.SaveState(pageState);
if (_sv != null)
{
_position = _sv.HorizontalOffset;
pageState["position"] = _position;
}
}
これでScrollViewerの移動量を復元できるようになります。しかし、実際に色々と試した結果として、グリッドアプリケーションテンプレートはうまく動作しますが、NewsReaderテンプレートのようにデータモデルを非同期で読み込むパターンだと、データの読み込みが遅延している関係とUIの仮想化との組み合わせで、うまく動作しないことが多々あります。このような場合は、移動したいアイテムとなるデータオブジェクトのインスタンスを取得して、itemGridViewのSizeChangedイベントで、GridView.ScrollIntoView(アイテムオブジェクト)メソッドを使った方が良いでしょう。
また、VisualTreeを使ってオブジェクトを探すコードを自分で記述しない場合は、WinRT XAML Toolkitの GetFirstDescendantOfType<T>拡張メソッドを使うのも良いでしょう。
Comments
Anonymous
April 11, 2013
記事にお褒めを戴き、恐縮です。 私も、最初は WinRT XAML Toolkit の VisualTreeHelperExtensions http://tinyurl.com/c52s8ow の GetChildren() メソッドとかを使って、GridView の中身を探し回りました。 コントロールの中身をいじりたい人は、WinRT XAML Toolkit を使うと楽が出来ますね。Anonymous
April 11, 2013
そうなんですよね。 XAMLでは、VisualTreeを思い出すとインスタンスの中身を探し回るのが基本なんですよね。