I have a data type, Metadata
, that contains a file path to either a video or an image. I'm attempting to use a DataTemplate
to display that data type. There's going to be thousands of these objects, so I'm also using an ListView
+ VirtualizingStackPanel
(well, actually, a VirtualizingWrapPanel
, but I switch to the StackPanel to make sure it wasn't a bug with the WrapPanel code) to attempt to virtualize those elements.
Here's the XAML code for the ItemsControl:
<ListView x:Name="ListBlock"
Margin="5"
Background="Transparent">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
The code for the DataTemplate (inside App.xaml
:
<DataTemplate DataType="{x:Type media:Metadata}">
<controls:MediaContainer/>
</DataTemplate>
The code for the MediaContainer:
<UserControl>
<Border x:Name="Outline">
<Grid>
<ContentPresenter Content="{Binding}"/>
<Image x:Name="DisplayImage" />
<MediaElement x:Name="DisplayVideo" />
</Grid>
</Border>
</UserControl>
And the code behind:
// Called On Loaded
Metadata data = (Metadata)DataContext;
Uri uri = new(data.FilePath);
if (data.FileType == FileType.Video)
{
DisplayVideo = new()
{
Source = uri,
Height = data.Height,
Width = data.Width
};
}
else
{
BitmapImage source = new(uri);
DisplayImage = new()
{
Source = data.FileType == FileType.Gif ? null : source,
Height = data.Height,
Width = data.Width
};
if (data.FileType == FileType.Gif)
{
// more code
}
}
I then assign a list of Metadata
as the ItemsSource
on Loaded for another window
Loaded += (_, _) => {
ListBlock.ItemsSource = storage.CurrentAlbum.Media;
// Logging ListBlock.Items.Count shows the correct number of items
};
I was under the impression that this is all I'd have to do, but when executing the code with the Data Template, I get a StackOverflow exception (executing the code without the Data Template does not cause the exception).
I've insured that there's no recursive code within the UserControl that I've created for the template, and I never create the control manually (ie. the only thing to constructs it is WPF). I've included the entire script below, just in case it's pertinent, however.
public Border OutlineElement { get => Outline; }
public Image ImageElement { get => DisplayImage; }
public MediaElement VideoElement { get => DisplayVideo; }
public Metadata Metadata { get; private set; }
public event EventHandler OnSelect;
private bool isSelected = false;
public MediaContainer() {
InitializeComponent();
Loaded += (_, _) => Load();
}
public void Select() {
if (isSelected) {
Outline.BorderBrush = Brushes.Transparent;
isSelected = false;
} else {
Outline.BorderBrush = Configs.OUTLINE_COLOR;
isSelected = true;
OnSelect?.Invoke(this, EventArgs.Empty);
}
}
private void Load() {
Metadata data = (Metadata)DataContext;
Uri uri = new(data.FilePath);
Metadata = data;
(double height, double width) = Scale(data);
Outline.Width = width + (Configs.OUTLINE_WIDTH * 2);
Outline.Height = height + (Configs.OUTLINE_WIDTH * 2);
if (data.FileType == FileType.Video) {
DisplayVideo = new() {
Source = uri,
Height = height,
Width = width,
LoadedBehavior = MediaState.Manual,
Volume = 0
};
DisplayVideo.MouseEnter += (o, e) => PeekVideo(o, e, true);
DisplayVideo.MouseLeave += (o, e) => PeekVideo(o, e, false);
DisplayVideo.Pause();
} else {
BitmapImage source = new(uri);
DisplayImage = new() {
Source = data.FileType == FileType.Gif ? null : source,
Height = height,
Width = width
};
if (data.FileType == FileType.Gif) {
ImageBehavior.SetAnimatedSource(DisplayImage, source);
ImageBehavior.SetRepeatBehavior(DisplayImage, System.Windows.Media.Animation.RepeatBehavior.Forever);
}
}
async void PeekVideo(object o, MouseEventArgs e, bool isEntering) {
if (e.LeftButton == MouseButtonState.Pressed) { return; }
DisplayVideo.LoadedBehavior = MediaState.Manual;
if (!isEntering) {
DisplayVideo.Pause();
DisplayVideo.Position = new(0);
return;
}
await Task.Delay(250);
if (!DisplayVideo.IsMouseOver) { return; }
DisplayVideo.Volume = 0;
DisplayVideo.Play();
}
static (double, double) Scale(Metadata meta) {
double _height = meta.Height;
double _width = meta.Width;
if (meta.Height != Configs.HEIGHT && meta.Height > 0) {
double scale = Configs.HEIGHT / meta.Height;
_height = meta.Height * scale;
_width = meta.Width * scale;
}
return new(_height, _width);
}
}
The StackOverflowException
exception was a very valuable hint.
It looks the issue stems from the nested ContentPresenter
inside the UserControl
.
<UserControl>
<Border x:Name="Outline">
<Grid>
<!-- This is the problematic element, and doesn't make sense -->
<ContentPresenter Content="{Binding}"/>
<Image x:Name="DisplayImage" />
<MediaElement x:Name="DisplayVideo" />
</Grid>
</Border>
</UserControl>
It's not apparent what the ContentPresenter
is for.
However, when the ItemsControl
loads a Metadata
item:
DataTemplate
for the Metadata
type is loaded.DataContext
of the DataTemplate
is the Metadata
item.Metadata
item is inherited as DataContext
to the UserControl
that is inside this DataTemplate
.DataContext
of the UserControl
(still the same Metadata
item) again is inherited to the DataContext
of the internal elements. One of those is the ContentPresenter
.ContentPresenter
binds its ContentPresenter.Content
property to the current DataContext
, the Metadata
item, per Binding
declaration.Bindig
and assigned to the ContentPresenter.Content
property will force the ContentPresenter
to try loading a DataTemplate
itself for the current Content
value.ContentPresenter
finds the implicit DataTemplate
for the content value (the Metadata
item) and applies it.DataTemplate
will also create a new UserControl
instance because this UserControl
is a child of the DataTemplate
.StackOverflowException
is thrown.Without knowing the purpose of the nested ContentPresenter
you could define the DataTemplate
as an explicit template by assigning it an explicit key. This way the nested ContentPresenter
can't load it implicitly:
App.xaml
Define the DataTemplate
as explicit
<DataTemplate x:Key="ListBoxItemDataTemplate"
DataType="{x:Type media:Metadata}">
<controls:MediaContainer/>
</DataTemplate>
MainWindow.xaml
Assign the DataTemplate
explicitly to the ItemsControl.ItemTemplate
property.
Note, because the ListBox
and ListView
both support UI virtualization by default, you no longer have to override the default ItemsPanelTemplate
: it's already a VirtualizingStackPanel
!
<ListView x:Name="ListBlock"
ItemTemplate="{StaticResource ListBoxItemDataTemplate"}>
</ListView>
Now the infinite loop is broken because the ContentPresenter
that is nested into the UserControl
no longer finds a DataTemplate
as the DataTemplate
is now explicit (registered with an explicit x:Key
).
Now you should be able to understand the issue better.
How to finally solve it depends on the purpose of the nested ContentPresenter
. Usually, you don't set the ContentPresenter.Content
property explicitly. The value is implicitly assigned by the framework - if the ContentPresenter
is placed inside a ContentControl
(and a UserControl
is a ContentControl
). But most importantly, binding the ContentPresenter
to the DataTemplate.DataContext
is giving you the issues - and doesn't make any sense.
Instead of making the DataTemplate
explicit I highly recommend fixing the layout of the UserControl
.