@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>拡張メソッドを使うのも良いでしょう。