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
StackOverflowExceptionexception was a very valuable hint.It looks the issue stems from the nested
ContentPresenterinside theUserControl.It's not apparent what the
ContentPresenteris for.However, when the
ItemsControlloads aMetadataitem:DataTemplatefor theMetadatatype is loaded.Now, the
DataContextof theDataTemplateis theMetadataitem.Metadataitem is inherited asDataContextto theUserControlthat is inside thisDataTemplate.DataContextof theUserControl(still the sameMetadataitem) again is inherited to theDataContextof the internal elements. One of those is theContentPresenter.ContentPresenterbinds itsContentPresenter.Contentproperty to the currentDataContext, theMetadataitem, perBindingdeclaration.Bindigand assigned to theContentPresenter.Contentproperty will force theContentPresenterto try loading aDataTemplateitself for the currentContentvalue.ContentPresenterfinds the implicitDataTemplatefor the content value (theMetadataitem) and applies it.DataTemplatewill also create a newUserControlinstance because thisUserControlis a child of theDataTemplate.StackOverflowExceptionis thrown.Without knowing the purpose of the nested
ContentPresenteryou could define theDataTemplateas an explicit template by assigning it an explicit key. This way the nestedContentPresentercan't load it implicitly:App.xaml
Define the
DataTemplateas explicitMainWindow.xaml
Assign the
DataTemplateexplicitly to theItemsControl.ItemTemplateproperty.Note, because the
ListBoxandListViewboth support UI virtualization by default, you no longer have to override the defaultItemsPanelTemplate: it's already aVirtualizingStackPanel!Now the infinite loop is broken because the
ContentPresenterthat is nested into theUserControlno longer finds aDataTemplateas theDataTemplateis now explicit (registered with an explicitx: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 theContentPresenter.Contentproperty explicitly. The value is implicitly assigned by the framework - if theContentPresenteris placed inside aContentControl(and aUserControlis aContentControl). But most importantly, binding theContentPresenterto theDataTemplate.DataContextis giving you the issues - and doesn't make any sense.Instead of making the
DataTemplateexplicit I highly recommend fixing the layout of theUserControl.