// Copyright (C) Microsoft Corporation. All Rights Reserved.
// This code released under the terms of the Microsoft Public License
// (Ms-PL, http://opensource.org/licenses/ms-pl.html).
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Media;
namespace Juick.Classes
{
///
/// Implements a subclass of ListBox based on a StackPanel that defers the
/// loading of off-screen items until necessary in order to minimize impact
/// to the UI thread.
///
public class DeferredLoadListBox : ListBox
{
private enum OverlapKind { Overlap, ChildAbove, ChildBelow };
private ScrollViewer _scrollViewer;
private ItemContainerGenerator _generator;
private bool _queuedUnmaskVisibleContent;
private bool _inOnApplyTemplate;
///
/// Handles the application of the Control's Template.
///
public override void OnApplyTemplate()
{
// Unhook from old Template elements
_inOnApplyTemplate = true;
ClearValue(VerticalOffsetShadowProperty);
_scrollViewer = null;
_generator = null;
// Apply new Template
base.OnApplyTemplate();
// Hook up to new Template elements
_scrollViewer = FindFirstChildOfType(this);
if (null == _scrollViewer)
{
throw new NotSupportedException("Control Template must include a ScrollViewer (wrapping ItemsHost).");
}
_generator = ItemContainerGenerator;
SetBinding(VerticalOffsetShadowProperty, new Binding { Source = _scrollViewer, Path = new PropertyPath("VerticalOffset") });
_inOnApplyTemplate = false;
}
///
/// Determines if the specified item is (or is eligible to be) its own item container.
///
/// The specified item.
/// true if the item is its own item container; otherwise, false.
protected override bool IsItemItsOwnContainerOverride(object item)
{
// Check container type
return item is DeferredLoadListBoxItem;
}
///
/// Creates or identifies the element used to display a specified item.
///
/// A DeferredLoadListBoxItem corresponding to a specified item.
protected override DependencyObject GetContainerForItemOverride()
{
// Create container (matches ListBox implementation)
var item = new DeferredLoadListBoxItem();
if (ItemContainerStyle != null)
{
item.Style = ItemContainerStyle;
}
return item;
}
///
/// Prepares the specified element to display the specified item.
///
/// The element used to display the specified item.
/// The item to display.
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
// Perform base class preparation
base.PrepareContainerForItemOverride(element, item);
// Mask the container's content
var container = (DeferredLoadListBoxItem)element;
if (!DesignerProperties.IsInDesignTool)
{
container.MaskContent();
}
// Queue a (single) pass to unmask newly visible content on the next tick
if (!_queuedUnmaskVisibleContent)
{
_queuedUnmaskVisibleContent = true;
Dispatcher.BeginInvoke(() =>
{
_queuedUnmaskVisibleContent = false;
UnmaskVisibleContent();
});
}
}
private static readonly DependencyProperty VerticalOffsetShadowProperty =
DependencyProperty.Register("VerticalOffsetShadow", typeof(double), typeof(DeferredLoadListBox), new PropertyMetadata(-1.0, OnVerticalOffsetShadowChanged));
private static void OnVerticalOffsetShadowChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
// Handle ScrollViewer VerticalOffset change by unmasking newly visible content
((DeferredLoadListBox)o).UnmaskVisibleContent();
}
private void UnmaskVisibleContent()
{
// Capture variables
var count = Items.Count;
// Find index of any container within view using (1-indexed) binary search
var index = -1;
var l = 0;
var r = count + 1;
while (-1 == index)
{
var p = (r - l) / 2;
if (0 == p)
{
break;
}
p += l;
var c = (DeferredLoadListBoxItem)_generator.ContainerFromIndex(p - 1);
if (null == c)
{
if (_inOnApplyTemplate)
{
// Applying template; don't expect to have containers at this point
return;
}
// Should always be able to get the container
var presenter = FindFirstChildOfType(_scrollViewer);
var panel = (null == presenter) ? null : FindFirstChildOfType(presenter);
if (panel is VirtualizingStackPanel)
{
throw new NotSupportedException("Must change ItemsPanel to be a StackPanel (via the ItemsPanel property).");
}
else
{
throw new NotSupportedException("Couldn't find container for item (ItemsPanel should be a StackPanel).");
}
}
switch (Overlap(_scrollViewer, c, 0))
{
case OverlapKind.Overlap:
index = p - 1;
break;
case OverlapKind.ChildAbove:
l = p;
break;
case OverlapKind.ChildBelow:
r = p;
break;
}
}
if (-1 != index)
{
// Unmask visible items below the current item
for (var i = index; i < count; i++)
{
if (!UnmaskItemContent(i))
{
break;
}
}
// Unmask visible items above the current item
for (var i = index - 1; 0 <= i; i--)
{
if (!UnmaskItemContent(i))
{
break;
}
}
}
}
private bool UnmaskItemContent(int index)
{
var container = (DeferredLoadListBoxItem)_generator.ContainerFromIndex(index);
if (null != container)
{
// Return quickly if not masked (but periodically check visibility anyway so we can stop once we're out of range)
if (!container.Masked && (0 != (index % 16)))
{
return true;
}
// Check necessary conditions
if (0 == container.ActualHeight)
{
// In some cases, ActualHeight will be 0 here, but can be "fixed" with an explicit call to UpdateLayout
container.UpdateLayout();
if (0 == container.ActualHeight)
{
throw new NotSupportedException("All containers must have a Height set (ex: via ItemContainerStyle), though the heights do not all need to be the same.");
}
}
// If container overlaps the "visible" area (i.e. on or near the screen), unmask it
if (OverlapKind.Overlap == Overlap(_scrollViewer, container, 2 * _scrollViewer.ActualHeight))
{
container.UnmaskContent();
return true;
}
}
return false;
}
private static bool Overlap(double startA, double endA, double startB, double endB)
{
return (((startA <= startB) && (startB <= endA)) ||
((startB <= startA) && (startA <= endB)));
}
private static OverlapKind Overlap(ScrollViewer parent, FrameworkElement child, double padding)
{
// Get child transform relative to parent
//var transform = child.TransformToVisual(parent); // Unreliable on Windows Phone 7; throws ArgumentException sometimes
var layoutSlot = LayoutInformation.GetLayoutSlot(child);
var transform = new TranslateTransform { /*X = layoutSlot.Left - parent.HorizontalOffset,*/ Y = layoutSlot.Top - parent.VerticalOffset };
// Get child bounds relative to parent
var bounds = new Rect(transform.Transform(new Point()), transform.Transform(new Point(/*child.ActualWidth*/ 0, child.ActualHeight)));
// Return kind of overlap
if (Overlap(0 - padding, parent.ActualHeight + padding, bounds.Top, bounds.Bottom))
{
return OverlapKind.Overlap;
}
else if (bounds.Top < 0)
{
return OverlapKind.ChildAbove;
}
else
{
return OverlapKind.ChildBelow;
}
}
private static T FindFirstChildOfType(DependencyObject root) where T : class
{
// Enqueue root node
var queue = new Queue();
queue.Enqueue(root);
while (0 < queue.Count)
{
// Dequeue next node and check its children
var current = queue.Dequeue();
for (var i = VisualTreeHelper.GetChildrenCount(current) - 1; 0 <= i; i--)
{
var child = VisualTreeHelper.GetChild(current, i);
var typedChild = child as T;
if (null != typedChild)
{
return typedChild;
}
// Enqueue child
queue.Enqueue(child);
}
}
// No children match
return null;
}
}
}