Reputation: 11
I'm currently working on a kind of VirtualizedWrapPanel to use as the ItemsPanel in a ListView.
After following this guy's instructions, and borrowing heavily from this guy's implementation found on codeproject but I don't have the reputation to post the link so sorry..., I have something that is nicely shaping up to be exactly what I need.
The item size is fixed so the scrolling is pixel based. the orientation is always horizontal.
the ListView :
<ListView Name="lv"
ItemsSource="{Binding CV}"
IsSynchronizedWithCurrentItem="True"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<i:Interaction.Behaviors>
<local:ScrollToSelectionListViewBehavior/> <!-- Behavior calling ScrollIntoView whenever the selection changes -->
<local:ListViewSelectedItemsBehavior SelectedItems="{Binding SelectedItems}"/> <!-- Behavior exposing the attached ListView's SelectedItems array -->
</i:Interaction.Behaviors>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Height="100" Width="100" Orientation="Vertical">
<Border BorderBrush="Black" BorderThickness="1" CornerRadius="5" Width="90" Height="90">
<TextBlock Text ="{Binding ItemText}" FontWeight="Bold" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<local:VirtualizingWrapPanel ItemHeight="100" ItemWidth="110" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
the local:VirtualizingWrapPanel :
public class VirtualizingWrapPanel : VirtualizingPanel, IScrollInfo
{
private ScrollViewer _owner;
private const bool _canHScroll = false;
private bool _canVScroll = false;
private Size _extent = new Size(0, 0);
private Size _viewport = new Size(0, 0);
private Point _offset;
UIElementCollection _children;
ItemsControl _itemsControl;
IItemContainerGenerator _generator;
Dictionary<UIElement, Rect> _realizedChildLayout = new Dictionary<UIElement, Rect>();
#region Properties
private Size ChildSlotSize
{
get { return new Size(ItemWidth, ItemHeight); }
}
#endregion
#region Dependency Properties
[TypeConverter(typeof(LengthConverter))]
public double ItemHeight
{
get { return (double)base.GetValue(ItemHeightProperty); }
set { base.SetValue(ItemHeightProperty, value); }
}
[TypeConverter(typeof(LengthConverter))]
public double ItemWidth
{
get { return (double)base.GetValue(ItemWidthProperty); }
set { base.SetValue(ItemWidthProperty, value); }
}
public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register("ItemHeight", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(double.PositiveInfinity));
public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register("ItemWidth", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(double.PositiveInfinity));
private int LineCapacity
{ get { return Math.Max((_viewport.Width != 0) ? (int)(_viewport.Width / ItemWidth) : 0, 1); } }
private int LinesCount
{ get { return (ItemsCount > 0) ? ItemsCount / LineCapacity : 0 ; } }
private int ItemsCount
{ get { return _itemsControl.Items.Count; } }
public int FirstVisibleLine
{ get { return (int)(_offset.Y / ItemHeight); } }
public int FirstVisibleItemVPos
{ get { return (int)((FirstVisibleLine * ItemHeight) - _offset.Y); } }
public int FirstVisibleIndex
{ get { return (FirstVisibleLine * LineCapacity); } }
#endregion
#region VirtualizingPanel overrides
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
_itemsControl = ItemsControl.GetItemsOwner(this);
_children = InternalChildren;
_generator = ItemContainerGenerator;
this.SizeChanged += new SizeChangedEventHandler(this.Resizing);
}
protected override Size MeasureOverride(Size availableSize)
{
if (_itemsControl == null || _itemsControl.Items.Count == 0)
return availableSize;
if (availableSize != _viewport)
{
_viewport = availableSize;
if (_owner != null)
_owner.InvalidateScrollInfo();
}
Size childSize = new Size(ItemWidth, ItemHeight);
Size extent = new Size(availableSize.Width, LinesCount * ItemHeight);
if (extent != _extent)
{
_extent = extent;
if (_owner != null)
_owner.InvalidateScrollInfo();
}
foreach (UIElement child in this.InternalChildren)
{
child.Measure(childSize);
}
_realizedChildLayout.Clear();
Size realizedFrameSize = availableSize;
int firstVisibleIndex = FirstVisibleIndex;
GeneratorPosition startPos = _generator.GeneratorPositionFromIndex(firstVisibleIndex);
int childIndex = (startPos.Offset == 0) ? startPos.Index : startPos.Index + 1;
int current = firstVisibleIndex;
using (_generator.StartAt(startPos, GeneratorDirection.Forward, true))
{
bool stop = false;
double currentX = 0;
double currentY = FirstVisibleItemVPos;
while (current < ItemsCount)
{
bool newlyRealized;
// Get or create the child
UIElement child = _generator.GenerateNext(out newlyRealized) as UIElement;
if (newlyRealized)
{
// Figure out if we need to insert the child at the end or somewhere in the middle
if (childIndex >= _children.Count)
{
base.AddInternalChild(child);
}
else
{
base.InsertInternalChild(childIndex, child);
}
_generator.PrepareItemContainer(child);
child.Measure(ChildSlotSize);
}
else
{
// The child has already been created, let's be sure it's in the right spot
Debug.Assert(child == _children[childIndex], "Wrong child was generated");
}
childSize = child.DesiredSize;
Rect childRect = new Rect(new Point(currentX, currentY), childSize);
if (childRect.Right > realizedFrameSize.Width) //wrap to a new line
{
currentY = currentY + ItemHeight;
currentX = 0;
childRect.X = currentX;
childRect.Y = currentY;
}
if (currentY > realizedFrameSize.Height)
stop = true;
currentX = childRect.Right;
_realizedChildLayout.Add(child, childRect);
if (stop)
break;
current++;
childIndex++;
}
}
CleanUpItems(firstVisibleIndex, current - 1);
return availableSize;
}
public void CleanUpItems(int minDesiredGenerated, int maxDesiredGenerated)
{
for (int i = _children.Count - 1; i >= 0; i--)
{
GeneratorPosition childGeneratorPos = new GeneratorPosition(i, 0);
int itemIndex = _generator.IndexFromGeneratorPosition(childGeneratorPos);
if (itemIndex < minDesiredGenerated || itemIndex > maxDesiredGenerated)
{
//var c = _children[i] as ListViewItem;
//if(c!= null && c.IsSelected)
//{
//}
_generator.Remove(childGeneratorPos, 1);
RemoveInternalChildRange(i, 1);
}
}
}
protected override Size ArrangeOverride(Size finalSize)
{
if (finalSize != _viewport)
{
_viewport = finalSize;
if (_owner != null)
_owner.InvalidateScrollInfo();
}
Size childSize = new Size(ItemWidth, ItemHeight);
Size extent = new Size(finalSize.Width, LinesCount * ItemHeight);
if (extent != _extent)
{
_extent = extent;
if (_owner != null)
_owner.InvalidateScrollInfo();
}
if (_children != null)
{
foreach (UIElement child in _children)
{
var layoutInfo = _realizedChildLayout[child];
child.Arrange(layoutInfo);
}
}
return finalSize;
}
protected override void BringIndexIntoView(int index)
{
SetVerticalOffset((index / LineCapacity) * ItemHeight);
}
protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
{
base.OnItemsChanged(sender, args);
_offset.X = 0;
_offset.Y = 0;
switch (args.Action)
{
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Replace:
RemoveInternalChildRange(args.Position.Index, args.ItemUICount);
break;
case NotifyCollectionChangedAction.Move:
RemoveInternalChildRange(args.OldPosition.Index, args.ItemUICount);
break;
}
}
#endregion
#region EventHandlers
public void Resizing(object sender, EventArgs e)
{
var args = e as SizeChangedEventArgs;
if(args.WidthChanged)
{
int lineCapacity = LineCapacity;
int previousLineCapacity = (int)(args.PreviousSize.Width / ItemWidth);
if (previousLineCapacity != lineCapacity)
{
int previousFirstItem = ((int)(_offset.Y / ItemHeight) <= 0) ? 0 : ((int)(_offset.Y / ItemHeight) * previousLineCapacity);
BringIndexIntoView(previousFirstItem);
}
}
if (_viewport.Width != 0)
{
MeasureOverride(_viewport);
}
}
#endregion
#region IScrollInfo Implementation
public ScrollViewer ScrollOwner
{
get { return _owner; }
set { _owner = value; }
}
public bool CanHorizontallyScroll
{
get { return false; }
set { if (value == true) throw (new ArgumentException("VirtualizingWrapPanel does not support Horizontal scrolling")); }
}
public bool CanVerticallyScroll
{
get { return _canVScroll; }
set { _canVScroll = value; }
}
public double ExtentHeight
{
get { return _extent.Height;}
}
public double ExtentWidth
{
get { return _extent.Width; }
}
public double HorizontalOffset
{
get { return _offset.X; }
}
public double VerticalOffset
{
get { return _offset.Y; }
}
public double ViewportHeight
{
get { return _viewport.Height; }
}
public double ViewportWidth
{
get { return _viewport.Width; }
}
public Rect MakeVisible(Visual visual, Rect rectangle)
{
var gen = (ItemContainerGenerator)_generator.GetItemContainerGeneratorForPanel(this);
var element = (UIElement)visual;
int itemIndex = gen.IndexFromContainer(element);
while (itemIndex == -1)
{
element = (UIElement)VisualTreeHelper.GetParent(element);
itemIndex = gen.IndexFromContainer(element);
}
Rect elementRect = _realizedChildLayout[element];
if (elementRect.Bottom > ViewportHeight)
{
double translation = elementRect.Bottom - ViewportHeight;
_offset.Y += translation;
}
else if (elementRect.Top < 0)
{
double translation = elementRect.Top;
_offset.Y += translation;
}
InvalidateMeasure();
return elementRect;
}
public void LineDown()
{
SetVerticalOffset(VerticalOffset + 50);
}
public void LineUp()
{
SetVerticalOffset(VerticalOffset - 50);
}
public void MouseWheelDown()
{
SetVerticalOffset(VerticalOffset + 50);
}
public void MouseWheelUp()
{
SetVerticalOffset(VerticalOffset - 50);
}
public void PageDown()
{
int fullyVisibleLines = (int)(_viewport.Height / ItemHeight);
SetVerticalOffset(VerticalOffset + (fullyVisibleLines * ItemHeight));
}
public void PageUp()
{
int fullyVisibleLines = (int)(_viewport.Height / ItemHeight);
SetVerticalOffset(VerticalOffset - (fullyVisibleLines * ItemHeight));
}
public void SetVerticalOffset(double offset)
{
if (offset < 0 || _viewport.Height >= _extent.Height)
{
offset = 0;
}
else
{
if (offset + _viewport.Height >= _extent.Height)
{
offset = _extent.Height - _viewport.Height;
}
}
_offset.Y = offset;
if (_owner != null)
_owner.InvalidateScrollInfo();
InvalidateMeasure();
}
public void LineLeft() { throw new NotImplementedException(); }
public void LineRight() { throw new NotImplementedException(); }
public void MouseWheelLeft() { throw new NotImplementedException(); }
public void MouseWheelRight() { throw new NotImplementedException(); }
public void PageLeft() { throw new NotImplementedException(); }
public void PageRight() { throw new NotImplementedException(); }
public void SetHorizontalOffset(double offset) { throw new NotImplementedException(); }
#endregion
#region methods
#endregion
}
Now my problem is : An Item Selection should always Deselect the previously selected item, when using a normal WrapPanel, the previously selected ListViewItem's IsSelected property is always set to false before the new selected ListViewItem's IsSelected is set to true.
This deselection does not happen with my VirtualizingPanel when the previously selected item is no longer realized (when it is not visible in the viewport), so I end up with two or more selected items at once and the panel's behavior becomes really weird. Sometimes it even settles into an infinite loop, the two selected items yanking visibility from each other in a never ending battle.
I searched a bit for a solution to this problem but I don't really know where to start or what to search for.
Here is a test project if you want to experiment with it. Thanks
Upvotes: 1
Views: 549
Reputation: 11
I found a way by preventing the virtualization of selected items. Now the control behaves correctly.
public void CleanUpItems(int minDesiredGenerated, int maxDesiredGenerated)
{
for (int i = _children.Count - 1; i >= 0; i--)
{
GeneratorPosition childGeneratorPos = new GeneratorPosition(i, 0);
int itemIndex = _generator.IndexFromGeneratorPosition(childGeneratorPos);
if (itemIndex < minDesiredGenerated || itemIndex > maxDesiredGenerated)
{
//I don't much like this cast
// how do I access the IsSelectedProperty
// of a UIElement of any type ( ListBoxItem, TreeViewItem...)?
var c = _children[i] as ListViewItem;
c.IsEnabled = false;
if (c != null && c.IsSelected)
{
var layoutInfo = new Rect(0, 0, 0, 0);
c.Arrange(layoutInfo);
}
else
{
_generator.Remove(childGeneratorPos, 1);
RemoveInternalChildRange(i, 1);
}
}
}
}
In ArrangeOverride :
if (_children != null)
{
foreach (UIElement child in _children)
{
if (child.IsEnabled)
{
var layoutInfo = _realizedChildLayout[child];
child.Arrange(layoutInfo);
}
}
}
In MeasureOverride:
while (current < ItemsCount)
{
bool newlyRealized;
// Get or create the child
UIElement child = _generator.GenerateNext(out newlyRealized) as UIElement;
child.IsEnabled = true;
I'm still curious to know if there's a better way. In the meantime this will do. btw I'll update the test project with this fix in case anyone wants a virtualizing wrappanel.
Upvotes: 0