Here's a rough solution that I think accomplishes what you're looking for. I'm getting the virtualizing stack panel by listening to the loaded event in the XAML. If I were doing this in production code, I might factor this into a reusable attached behavior rather than throwing a bunch of code in the code-behind.
public partial class MainWindow
{
private VirtualizingStackPanel _panel;
public MainWindow()
{
InitializeComponent();
DataContext = new MyViewModel();
}
private IList<ChildViewModel> _snapshot = new List<ChildViewModel>();
private void OnPanelLoaded(object sender, RoutedEventArgs eventArgs)
{
_panel = (VirtualizingStackPanel)sender;
UpdateSnapshot();
_panel.ScrollOwner.ScrollChanged += (s,e) => UpdateSnapshot();
}
private void UpdateSnapshot()
{
var layoutBounds = LayoutInformation.GetLayoutSlot(_panel);
var onScreenChildren =
(from visualChild in _panel.GetChildren()
let childBounds = LayoutInformation.GetLayoutSlot(visualChild)
where layoutBounds.Contains(childBounds) || layoutBounds.IntersectsWith(childBounds)
select visualChild.DataContext).Cast<ChildViewModel>().ToList();
foreach (var removed in _snapshot.Except(onScreenChildren))
{
// TODO: Cancel pending calculations.
Console.WriteLine("{0} was removed.", removed.Value);
}
_snapshot = onScreenChildren;
}
}
Notice that there isn't really a property we can use here to find the on-screen children, so we look at the layout bounds of the parent compared to the children to determine which children are on screen. The code uses an extension method for getting the visual children of an item in the visual tree, included below:
public static class MyVisualTreeHelpers
{
public static IEnumerable<FrameworkElement> GetChildren(this DependencyObject dependencyObject)
{
var numberOfChildren = VisualTreeHelper.GetChildrenCount(dependencyObject);
return (from index in Enumerable.Range(0, numberOfChildren)
select VisualTreeHelper.GetChild(dependencyObject, index)).Cast<FrameworkElement>();
}
}
This code is using a very basic view model hierarchy I created for the purposes of testing this out. I'll include it just in case it's helpful in understanding the other code:
public class MyViewModel
{
public MyViewModel()
{
Children = new ObservableCollection<ChildViewModel>(GenerateChildren());
}
public ObservableCollection<ChildViewModel> Children { get; set; }
private static IEnumerable<ChildViewModel> GenerateChildren()
{
return from value in Enumerable.Range(1, 1000)
select new ChildViewModel {Value = value};
}
}
public class ChildViewModel
{
public int Value { get; set; }
}
XAML:
<Window x:Class="WpfTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfTest="clr-namespace:WpfTest"
Title="MainWindow" Height="500" Width="500">
<ListBox ItemsSource="{Binding Children}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Loaded="OnPanelLoaded" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="wpfTest:ChildViewModel">
<TextBlock Text="{Binding Value}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Window>