Search code examples
wpfdatatemplateitemscontrolvirtualizingstackpanel

DataTemplate inside ListView (with VirtualizingStackPanel) causing StackOverflow


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);
    }
}

Solution

  • 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:

    1. The DataTemplate for the Metadata type is loaded.
      Now, the DataContext of the DataTemplate is the Metadata item.
    2. This Metadata item is inherited as DataContext to the UserControl that is inside this DataTemplate.
    3. The 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.
    4. The ContentPresenter binds its ContentPresenter.Content property to the current DataContext, the Metadata item, per Binding declaration.
    5. The value returned from the Bindig and assigned to the ContentPresenter.Content property will force the ContentPresenter to try loading a DataTemplate itself for the current Content value.
    6. The ContentPresenter finds the implicit DataTemplate for the content value (the Metadata item) and applies it.
    7. Loading the DataTemplate will also create a new UserControl instance because this UserControl is a child of the DataTemplate.
    8. The process starts at 1) again ==> infinite loop ==> exhausts stack memory ==> 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.