Мне нужен совет. Я пытаюсь создать ListBox с расширенным элементом детализации. Я также создал VirtualizingTilePanel, но, к сожалению, элементы в ListBox не перемещаются, так что детали этого элемента видны. Я пробовал это с ElasticWrapPanel, но нет виртуализации. Мне нужно поддерживать виртуализацию. Буду рад любым советам.
Исходный код: ExpandItemVirtualizingTilePanel
ListBox с ElasticWrapPanel без виртуализации:
ListBox с VirtualizingTilePanel с виртуализацией, но без прокрутки элементов, вы не можете видеть детали элемента:
<ListBox ItemsSource="{Binding MoviesCvs.View,IsAsync=True}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.CanContentScroll="True"
VirtualizingPanel.CacheLengthUnit="Pixel"
VirtualizingPanel.CacheLength="100,100"
VirtualizingPanel.ScrollUnit="Pixel"
VirtualizingPanel.VirtualizationMode="Recycling">
<ListBox.Resources>
<DataTemplate x:Key="DetailTempalte">
<DataTemplate.Resources>
<DropShadowEffect x:Key="z-depth3" BlurRadius="14" ShadowDepth="4.5" Direction="270" Opacity="0.6" Color="Black"/>
</DataTemplate.Resources>
<Grid Height="300" Effect="{StaticResource z-depth3}" Background="White" Margin="10">
<cachedImage:Image Stretch="UniformToFill" ImageUrl="{Binding BackgropPath}">
</cachedImage:Image>
</Grid>
</DataTemplate>
<ControlTemplate TargetType="{x:Type ListBoxItem}" x:Key="withDetailTemplate">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
<!-- **************** -->
<Canvas Grid.Row="1" x:Name="detailCanvas"
Width="0"
Height="{Binding ElementName=detailGrid,Path=ActualHeight}"
HorizontalAlignment="Left" VerticalAlignment="Top" Visibility="Collapsed">
<Grid x:Name="detailGrid" Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type ScrollContentPresenter}},Path=ActualWidth}"
Canvas.Left="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type ListBoxItem}},Path=(panelVirtualizing:VirtualizingTilePanel.ItemLocation).LocationN.X}">
<ContentPresenter ContentTemplate="{DynamicResource ResourceKey=DetailTempalte}" />
</Grid>
</Canvas>
<!-- **************** -->
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="true">
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
<Setter TargetName="detailCanvas" Property="Visibility" Value="Visible"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="true"/>
<Condition Property="Selector.IsSelectionActive" Value="false"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}}"/>
</MultiTrigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ListBox.Resources>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template" Value="{StaticResource withDetailTemplate}" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<!--I need to leave VirtualizingTilePanel there-->
<panelVirtualizing:VirtualizingTilePanel
x:Name="VirtualizingTilePanel"
IsVirtualizing="True"
VirtualizingPanel.CacheLengthUnit="Pixel"
VirtualizingPanel.CacheLength="100,100"
VirtualizingPanel.ScrollUnit="Pixel"
VirtualizingPanel.VirtualizationMode="Recycling"
ChildWidth="260" ChildHeight="455"/>
<!--<local:WrapPaneEx Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type ScrollContentPresenter}},Path=ActualWidth}"/>-->
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<DataTemplate.Resources>
<DropShadowEffect x:Key="z-depth3" BlurRadius="14" ShadowDepth="4.5" Direction="270" Opacity="0.6" Color="Black"/>
</DataTemplate.Resources>
<Grid Opacity="1" Effect="{StaticResource z-depth3}" Width="250" Height="445" Background="White" Margin="5">
<StackPanel VerticalAlignment="Top">
<cachedImage:Image Stretch="Uniform" ImageUrl="{Binding PosterPath}" >
</cachedImage:Image>
<TextBlock Margin="5,10,5,0" Text="{Binding MovieTitle}" TextTrimming="CharacterEllipsis" Foreground="Black" TextWrapping="Wrap" MaxHeight="50" FontSize="17"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
VirtualizingTilePanel
public class VirtualizingTilePanel : System.Windows.Controls.VirtualizingPanel, IScrollInfo
{
public VirtualizingTilePanel()
{
// For use in the IScrollInfo implementation
RenderTransform = _trans;
}
// Dependency property that controls the size of the child elements
public static readonly DependencyProperty ChildWidthProperty
= DependencyProperty.RegisterAttached("ChildWidth", typeof(double), typeof(VirtualizingTilePanel),
new FrameworkPropertyMetadata(250d, FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange));
public static readonly DependencyProperty ChildHeightProperty
= DependencyProperty.RegisterAttached("ChildHeight", typeof(double), typeof(VirtualizingTilePanel),
new FrameworkPropertyMetadata(445d, FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange));
// Accessor for the child size dependency property
public double ChildWidth
{
get { return (double)GetValue(ChildWidthProperty); }
set { SetValue(ChildWidthProperty, value); }
}
public double ChildHeight
{
get { return (double)GetValue(ChildHeightProperty); }
set { SetValue(ChildHeightProperty, value); }
}
/// <summary>
/// The panel's number of columns
/// </summary>
private int _columns;
/// <summary>
/// Measure the children
/// </summary>
/// <param name="availableSize">Size available</param>
/// <returns>Size desired</returns>
protected override Size MeasureOverride(Size availableSize)
{
try
{
if (availableSize.Width == double.PositiveInfinity || availableSize.Height == double.PositiveInfinity)
{
return Size.Empty;
}
_columns = (int)(availableSize.Width / ChildWidth);
UpdateScrollInfo(availableSize);
// Figure out range that's visible based on layout algorithm
int firstVisibleItemIndex, lastVisibleItemIndex;
GetVisibleRange(out firstVisibleItemIndex, out lastVisibleItemIndex);
// We need to access InternalChildren before the generator to work around a bug
var children = InternalChildren;
var generator = ItemContainerGenerator;
// Get the generator position of the first visible data item
var startPos = generator.GeneratorPositionFromIndex(firstVisibleItemIndex);
// Get index where we'd insert the child for this position. If the item is realized
// (position.Offset == 0), it's just position.Index, otherwise we have to add one to
// insert after the corresponding child
var childIndex = (startPos.Offset == 0) ? startPos.Index : startPos.Index + 1;
using (generator.StartAt(startPos, GeneratorDirection.Forward, true))
{
for (var itemIndex = firstVisibleItemIndex;
itemIndex <= lastVisibleItemIndex;
++itemIndex, ++childIndex)
{
bool newlyRealized;
// Get or create the child
var child = generator.GenerateNext(out newlyRealized) as UIElement;
if (child == null) continue;
if (newlyRealized)
{
// Figure out if we need to insert the child at the end or somewhere in the middle
if (childIndex >= children.Count)
{
AddInternalChild(child);
}
else
{
InsertInternalChild(childIndex, child);
}
generator.PrepareItemContainer(child);
}
//var itemLocation = GetItemLocation(child);
//if (itemLocation == null)
//{
// itemLocation = new ItemLocation(this, child);
// SetItemLocation(child, itemLocation);
//}
//itemLocation.OnLocationPropertyChanged();
// Measurements will depend on layout algorithm
child.Measure(GetChildSize());
}
}
// Note: this could be deferred to idle time for efficiency
CleanUpItems(firstVisibleItemIndex, lastVisibleItemIndex);
return availableSize;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
/// <summary>
/// Arrange the children
/// </summary>
/// <param name="finalSize">Size available</param>
/// <returns>Size used</returns>
protected override Size ArrangeOverride(Size finalSize)
{
try
{
//var size = base.ArrangeOverride(finalSize);
var generator = ItemContainerGenerator;
UpdateScrollInfo(finalSize);
for (var i = 0; i < Children.Count; i++)
{
var child = Children[i];
// Map the child offset to an item offset
var itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0));
ArrangeChild(itemIndex, child, finalSize);
var itemLocation = GetItemLocation(child);
if (itemLocation == null)
{
itemLocation = new ItemLocation(this, child);
SetItemLocation(child, itemLocation);
}
itemLocation.OnLocationPropertyChanged();
}
return finalSize;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
//protected override Size ArrangeOverride(Size finalSize)
//{
// var size = base.ArrangeOverride(finalSize);
// var generator = ItemContainerGenerator;
// UpdateScrollInfo(finalSize);
// foreach (UIElement fe in this.Children)
// {
// var itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(0, 0));
// ArrangeChild(itemIndex, fe, finalSize);
// var itemLocation = GetItemLocation(fe);
// if (itemLocation == null)
// {
// itemLocation = new ItemLocation(this, fe);
// SetItemLocation(fe, itemLocation);
// }
// itemLocation.OnLocationPropertyChanged();
// }
// return size;
//}
/// <summary>
/// Revirtualize items that are no longer visible
/// </summary>
/// <param name="minDesiredGenerated">first item index that should be visible</param>
/// <param name="maxDesiredGenerated">last item index that should be visible</param>
private void CleanUpItems(int minDesiredGenerated, int maxDesiredGenerated)
{
try
{
var children = InternalChildren;
var generator = ItemContainerGenerator;
for (var i = children.Count - 1; i >= 0; i--)
{
GeneratorPosition childGeneratorPos = new GeneratorPosition(i, 0);
var itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPos);
if (itemIndex < minDesiredGenerated || itemIndex > maxDesiredGenerated)
{
generator.Remove(childGeneratorPos, 1);
RemoveInternalChildRange(i, 1);
}
}
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
/// <summary>
/// When items are removed, remove the corresponding UI if necessary
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
{
try
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
RemoveInternalChildRange(args.Position.Index, args.ItemUICount);
break;
}
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
#region Layout specific code
/// <summary>
/// Calculate the extent of the view based on the available size
/// </summary>
/// <param name="availableSize">available size</param>
/// <param name="itemCount">number of data items</param>
/// <returns></returns>
private Size CalculateExtent(Size availableSize, int itemCount)
{
try
{
var childrenPerRow = CalculateChildrenPerRow(availableSize);
// See how big we are
return new Size(childrenPerRow * ChildWidth,
ChildHeight * Math.Ceiling((double)itemCount / childrenPerRow));
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
/// <summary>
/// Get the range of children that are visible
/// </summary>
/// <param name="firstVisibleItemIndex">The item index of the first visible item</param>
/// <param name="lastVisibleItemIndex">The item index of the last visible item</param>
private void GetVisibleRange(out int firstVisibleItemIndex, out int lastVisibleItemIndex)
{
try
{
var childrenPerRow = CalculateChildrenPerRow(_extent);
firstVisibleItemIndex = (int)Math.Floor(_offset.Y / ChildHeight) * childrenPerRow;
lastVisibleItemIndex =
(int)Math.Ceiling((_offset.Y + _viewport.Height) / ChildHeight) * childrenPerRow - 1;
var itemsControl = ItemsControl.GetItemsOwner(this);
var itemCount = itemsControl.HasItems ? itemsControl.Items.Count : 0;
if (lastVisibleItemIndex >= itemCount)
lastVisibleItemIndex = itemCount - 1;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
/// <summary>
/// Get the size of the children. We assume they are all the same
/// </summary>
/// <returns>The size</returns>
private Size GetChildSize()
{
return new Size(ChildWidth, ChildHeight + 350);
}
/// <summary>
/// Position a child
/// </summary>
/// <param name="itemIndex">The data item index of the child</param>
/// <param name="child">The element to position</param>
/// <param name="finalSize">The size of the panel</param>
private void ArrangeChild(int itemIndex, UIElement child, Size finalSize)
{
try
{
int childrenPerRow = CalculateChildrenPerRow(finalSize);
int row = itemIndex / childrenPerRow;
int column = itemIndex % childrenPerRow;
var columnWidth = Math.Floor(finalSize.Width / _columns);
child.Arrange(new Rect(columnWidth * column, row * ChildHeight, columnWidth,
child.DesiredSize.Height));
//var itemLocation = GetItemLocation(child);
//if (itemLocation == null)
//{
// itemLocation = new ItemLocation(this, child);
// SetItemLocation(child, itemLocation);
//}
//itemLocation.OnLocationPropertyChanged();
}
catch (Exception e)
{
}
}
/// <summary>
/// Helper function for tiling layout
/// </summary>
/// <param name="availableSize">Size available</param>
/// <returns></returns>
private int CalculateChildrenPerRow(Size availableSize)
{
try
{
// Figure out how many children fit on each row
int childrenPerRow;
if (double.IsPositiveInfinity(availableSize.Width))
childrenPerRow = Children.Count;
else
childrenPerRow = Math.Max(1, (int)Math.Floor(availableSize.Width / ChildWidth));
return childrenPerRow;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
#endregion
#region IScrollInfo implementation
// See Ben Constable's series of posts at http://blogs.msdn.com/bencon/
private void UpdateScrollInfo(Size availableSize)
{
try
{
// See how many items there are
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
int itemCount = itemsControl.HasItems ? itemsControl.Items.Count : 0;
Size extent = CalculateExtent(availableSize, itemCount);
// Update extent
if (extent != _extent)
{
_extent = extent;
ScrollOwner?.InvalidateScrollInfo();
}
// Update viewport
if (availableSize != _viewport)
{
_viewport = availableSize;
ScrollOwner?.InvalidateScrollInfo();
}
}
catch (Exception e)
{
}
}
public ScrollViewer ScrollOwner { get; set; }
public bool CanHorizontallyScroll { get; set; } = false;
public bool CanVerticallyScroll { get; set; } = false;
public double HorizontalOffset => _offset.X;
public double VerticalOffset => _offset.Y;
public double ExtentHeight => _extent.Height;
public double ExtentWidth => _extent.Width;
public double ViewportHeight => _viewport.Height;
public double ViewportWidth => _viewport.Width;
public void LineUp()
{
SetVerticalOffset(VerticalOffset - 10);
}
public void LineDown()
{
SetVerticalOffset(VerticalOffset + 10);
}
public void PageUp()
{
SetVerticalOffset(VerticalOffset - _viewport.Height);
}
public void PageDown()
{
SetVerticalOffset(VerticalOffset + _viewport.Height);
}
public void MouseWheelUp()
{
SetVerticalOffset(VerticalOffset - 10);
}
public void MouseWheelDown()
{
SetVerticalOffset(VerticalOffset + 10);
}
public void LineLeft()
{
throw new InvalidOperationException();
}
public void LineRight()
{
throw new InvalidOperationException();
}
public Rect MakeVisible(Visual visual, Rect rectangle)
{
return new Rect();
}
public void MouseWheelLeft()
{
throw new InvalidOperationException();
}
public void MouseWheelRight()
{
throw new InvalidOperationException();
}
public void PageLeft()
{
throw new InvalidOperationException();
}
public void PageRight()
{
throw new InvalidOperationException();
}
public void SetHorizontalOffset(double offset)
{
throw new InvalidOperationException();
}
public void SetVerticalOffset(double offset)
{
try
{
if (offset < 0 || _viewport.Height >= _extent.Height)
{
offset = 0;
}
else
{
if (offset + _viewport.Height >= _extent.Height)
{
offset = _extent.Height - _viewport.Height;
}
}
_offset.Y = offset;
ScrollOwner?.InvalidateScrollInfo();
_trans.Y = -offset /*- 350*/;
// Force us to realize the correct children
InvalidateMeasure();
}
catch (Exception e)
{
}
}
private readonly TranslateTransform _trans = new TranslateTransform();
private Size _extent = new Size(0, 0);
private Size _viewport = new Size(0, 0);
private Point _offset;
#endregion
public static ItemLocation GetItemLocation(DependencyObject obj)
{
return (ItemLocation)obj.GetValue(ItemLocationProperty);
}
public static void SetItemLocation(DependencyObject obj, ItemLocation value)
{
obj.SetValue(ItemLocationProperty, value);
}
public static readonly DependencyProperty ItemLocationProperty = DependencyProperty.RegisterAttached("ItemLocation", typeof(ItemLocation), typeof(VirtualizingTilePanel), new PropertyMetadata(null));
}
public class ItemLocation : System.ComponentModel.INotifyPropertyChanged
{
public ItemLocation(VirtualizingPanel panel, UIElement itemContainer)
{
this._Panel = panel;
this._ItemContainer = itemContainer;
}
private UIElement _ItemContainer;
private VirtualizingPanel _Panel;
public Point? Location
{
get
{
if (_Location == null && _Panel != null && _ItemContainer != null)
{
_Location = _ItemContainer.TranslatePoint(default(Point), _Panel);
}
return _Location;
}
}
private Point? _Location;
public Point? LocationN
{
get
{
if (_LocationN == null && _Location == null && _Panel != null && _ItemContainer != null)
{
Point? np = Location;
if (np != null)
{
_LocationN = new Point(-np.Value.X, -np.Value.Y - 350);
}
}
return _LocationN;
}
}
private Point? _LocationN;
public event PropertyChangedEventHandler PropertyChanged;
internal void OnLocationPropertyChanged()
{
_Location = null;
_LocationN = null;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Location)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(LocationN)));
}
}