ScrollViewer内の特定のControlを画面上に固定する方法


指定したIDを持つControlをScrollViewerの先頭にピン止めしたいのですが、どのような方法を取れば対象となるUIElementの表示位置を固定できるでしょうか？

動きの例としては、以下の様なイメージです。

  1. `ItemsSource`に`ObservableCollection<int>`をBindingした`ListView`に、1～1000の数値が`TextBlock`で縦方向のリストとして表示されている
  2. 1～1000が表示された`TextBlock`は、一定時間ごとにランダムに順番が入れ替わる
  3. ユーザーは特定の番号を`TextBox`への入力などで指定すると、その番号の表示された`TextBlock`がぴったり`ListView`の一番上に表示され、その番号の順番が入れ替わっても、その状況が保持されつづける(=自動的に適切な位置にスクロールする)

`ListView.ItemContainerGenerator.ContainerFromItem(...)`で取得した`Visual`を`Visual.PointToScreen(...)`で変換して、その位置に`ScrollViewer.ScrollToVerticalOffset(...)`をすれば…  
という事を考えたのですが、順番入れ替えを検知する方法が`INotifyCollectionChanged`しか見つからず、`CollectionChanged`イベントを起点にUIElementを取ろうとすると`VisualTree`の作成途中なのか、上手くコントロールを取得できませんでした。

このような場合に上手く機能を実現するには、どのような方法があるでしょうか？




`VirtualizingStackPanel`を取り出して、[SetVerticalOffset](https://msdn.microsoft.com/ja-
jp/library/system.windows.controls.virtualizingstackpanel.setverticaloffset.aspx)を使ってスクロールできます。`StackPanel`の場合でも同名のメソッドがあります  
`VirtualizingPanel.ScrollUnit`をPixelにしている場合は対応できませんが、この質問の目的であればItemで十分でしょう。

また、表示したい項目がリストの下位にあると最上部にスクロールは不可能なので、`CompositeCollection`でダミーの項目を追加しておくことで対処します。

[code]

    <DockPanel>
        <TextBox DockPanel.Dock="Top" x:Name="textBox" TextChanged="textBox_TextChanged"/>
        <ListView   x:Name="listView" ItemsSource="{Binding}">          
            <ListView.View>
                <GridView >
                    <GridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}" Width="50"/>
                </GridView>
            </ListView.View>
        </ListView>          
    </DockPanel>
    
[/code]

* * *
[code]

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Media;
    using System.Collections.ObjectModel;
    namespace WpfApplication1
    {
    
        public partial class MainWindow : Window
        {
            private System.Windows.Threading.DispatcherTimer timer;
            private ObservableCollection<Item> items;
            private VirtualizingStackPanel virtualPanel;
    
            public MainWindow()
            {
                InitializeComponent();
    
                this.Loaded += (s, e) =>
                    {
                        //スクロールさせるパネルを取得
                        virtualPanel = FindChild<VirtualizingStackPanel>(this.listView);
    
                        //一番上にという条件のために下部にダミーの要素を追加する
                        //ダミー追加はCompositeCollectionで合成する
                        CollectionContainer c1 = new CollectionContainer();
                        BindingOperations.SetBinding(c1, CollectionContainer.CollectionProperty, new Binding("DataContext") { Source = listView });
    
                        CollectionContainer c2 = new CollectionContainer();
                        c2.Collection = new Item[100];//適当な数
    
                        CompositeCollection cmp = new CompositeCollection();
                        cmp.Add(c1);
                        cmp.Add(c2);
    
                        this.listView.ItemsSource = cmp;
    
                        timer = new System.Windows.Threading.DispatcherTimer();
                        timer.Interval = TimeSpan.FromSeconds(1);
                        timer.Tick += (x, y) =>
                        {
                            //ランダムソート
                            List<Item> buff = this.items.ToList();
                            Random rnd = new Random();
                            this.items.Clear();
                            foreach (Item i in buff.OrderBy(_ => rnd.Next())) { items.Add(i); }
                        };
                        timer.Start();
    
                        this.textBox.Text = "10";
                    };
    
                items = Item.Create(1000);
                items.CollectionChanged += items_CollectionChanged;
    
                this.DataContext = items;
            }
    
            void items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
            {
                Scroll();
            }
            private void textBox_TextChanged(object sender, TextChangedEventArgs e)
            {
                Scroll();
            }
            private void Scroll()
            {
                int id; if (!int.TryParse(this.textBox.Text, out id)) { return; }
    
                if (virtualPanel != null && virtualPanel.CanVerticallyScroll)
                {
                    for (int i = 0; i < items.Count; i++)
                    {
                        if (items[i].ID == id)
                        {
                            virtualPanel.SetVerticalOffset(i);
                            break;
                        }
                    }
                }
            }
    
            private T FindChild<T>(DependencyObject d) where T : DependencyObject
            {
                if (d is T) { return (T)d; }
                int count = VisualTreeHelper.GetChildrenCount(d);
                for (int i = 0; i < count; i++)
                {
                    var si = FindChild<T>(VisualTreeHelper.GetChild(d, i));
                    if (si != null) { return si; }
                }
                return null;
            }
        }
    
        class Item
        {
            public int ID { get; set; }
    
            public static ObservableCollection<Item> Create(int count)
            {
                ObservableCollection<Item> retval = new ObservableCollection<Item>();
    
                for (int i = 1; i <= count; i++)
                {
                    Item item = new Item() { ID = i };
                    retval.Add(item);
                }
                return retval;
            }
        }
    }
    
[/code]




`VirtualizingStackPanel`を取り出して、[SetVerticalOffset](https://msdn.microsoft.com/ja-
jp/library/system.windows.controls.virtualizingstackpanel.setverticaloffset.aspx)を使ってスクロールできます。`StackPanel`の場合でも同名のメソッドがあります  
`VirtualizingPanel.ScrollUnit`をPixelにしている場合は対応できませんが、この質問の目的であればItemで十分でしょう。

また、表示したい項目がリストの下位にあると最上部にスクロールは不可能なので、`CompositeCollection`でダミーの項目を追加しておくことで対処します。

[code]

    <DockPanel>
        <TextBox DockPanel.Dock="Top" x:Name="textBox" TextChanged="textBox_TextChanged"/>
        <ListView   x:Name="listView" ItemsSource="{Binding}">          
            <ListView.View>
                <GridView >
                    <GridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}" Width="50"/>
                </GridView>
            </ListView.View>
        </ListView>          
    </DockPanel>
    
[/code]

* * *
[code]

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Media;
    using System.Collections.ObjectModel;
    namespace WpfApplication1
    {
    
        public partial class MainWindow : Window
        {
            private System.Windows.Threading.DispatcherTimer timer;
            private ObservableCollection<Item> items;
            private VirtualizingStackPanel virtualPanel;
    
            public MainWindow()
            {
                InitializeComponent();
    
                this.Loaded += (s, e) =>
                    {
                        //スクロールさせるパネルを取得
                        virtualPanel = FindChild<VirtualizingStackPanel>(this.listView);
    
                        //一番上にという条件のために下部にダミーの要素を追加する
                        //ダミー追加はCompositeCollectionで合成する
                        CollectionContainer c1 = new CollectionContainer();
                        BindingOperations.SetBinding(c1, CollectionContainer.CollectionProperty, new Binding("DataContext") { Source = listView });
    
                        CollectionContainer c2 = new CollectionContainer();
                        c2.Collection = new Item[100];//適当な数
    
                        CompositeCollection cmp = new CompositeCollection();
                        cmp.Add(c1);
                        cmp.Add(c2);
    
                        this.listView.ItemsSource = cmp;
    
                        timer = new System.Windows.Threading.DispatcherTimer();
                        timer.Interval = TimeSpan.FromSeconds(1);
                        timer.Tick += (x, y) =>
                        {
                            //ランダムソート
                            List<Item> buff = this.items.ToList();
                            Random rnd = new Random();
                            this.items.Clear();
                            foreach (Item i in buff.OrderBy(_ => rnd.Next())) { items.Add(i); }
                        };
                        timer.Start();
    
                        this.textBox.Text = "10";
                    };
    
                items = Item.Create(1000);
                items.CollectionChanged += items_CollectionChanged;
    
                this.DataContext = items;
            }
    
            void items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
            {
                Scroll();
            }
            private void textBox_TextChanged(object sender, TextChangedEventArgs e)
            {
                Scroll();
            }
            private void Scroll()
            {
                int id; if (!int.TryParse(this.textBox.Text, out id)) { return; }
    
                if (virtualPanel != null && virtualPanel.CanVerticallyScroll)
                {
                    for (int i = 0; i < items.Count; i++)
                    {
                        if (items[i].ID == id)
                        {
                            virtualPanel.SetVerticalOffset(i);
                            break;
                        }
                    }
                }
            }
    
            private T FindChild<T>(DependencyObject d) where T : DependencyObject
            {
                if (d is T) { return (T)d; }
                int count = VisualTreeHelper.GetChildrenCount(d);
                for (int i = 0; i < count; i++)
                {
                    var si = FindChild<T>(VisualTreeHelper.GetChild(d, i));
                    if (si != null) { return si; }
                }
                return null;
            }
        }
    
        class Item
        {
            public int ID { get; set; }
    
            public static ObservableCollection<Item> Create(int count)
            {
                ObservableCollection<Item> retval = new ObservableCollection<Item>();
    
                for (int i = 1; i <= count; i++)
                {
                    Item item = new Item() { ID = i };
                    retval.Add(item);
                }
                return retval;
            }
        }
    }
    
[/code]

