ListView 中的 DataTemplate(带有 VirtualizingStackPanel)导致 StackOverflow

问题描述 投票:0回答:1

我有一个数据类型,

Metadata
,其中包含视频或图像的文件路径。我正在尝试使用
DataTemplate
来显示该数据类型。将会有数千个这样的对象,所以我还使用
ListView
+
VirtualizingStackPanel
(实际上,是
VirtualizingWrapPanel
,但我切换到 StackPanel 以确保这不是一个错误) WrapPanel 代码)尝试虚拟化这些元素。

这是 ItemsControl 的 XAML 代码:

<ListView x:Name="ListBlock"
            Margin="5"
            Background="Transparent">
    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel/>
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>
</ListView>

DataTemplate 的代码(在

App.xaml
内:

<DataTemplate DataType="{x:Type media:Metadata}">
    <controls:MediaContainer/>
</DataTemplate>

MediaContainer 的代码:

<UserControl>
    <Border x:Name="Outline">
        <Grid>
            <ContentPresenter Content="{Binding}"/>
            <Image x:Name="DisplayImage" />
            <MediaElement x:Name="DisplayVideo" />
        </Grid>
    </Border>
</UserControl>

背后的代码:

// 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
    }
}

然后我将

Metadata
列表指定为另一个窗口的“已加载”
ItemsSource

Loaded += (_, _) => {
    ListBlock.ItemsSource = storage.CurrentAlbum.Media;
    // Logging ListBlock.Items.Count shows the correct number of items              
};

我的印象是这就是我要做的全部事情,但是当使用数据模板执行代码时,我收到一个 StackOverflow 异常(执行代码没有数据模板不会导致异常)。

我已确保为模板创建的 UserControl 中没有递归代码,并且我从不手动创建控件(即构造它的唯一方法是 WPF)。不过,我已经在下面包含了整个脚本,以防万一它是相关的。

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);
    }
}
wpf datatemplate itemscontrol virtualizingstackpanel
1个回答
0
投票

StackOverflowException
例外是一个非常有价值的提示。

看来问题源于

ContentPresenter
内嵌套的
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>

不清楚

ContentPresenter
的用途。

但是,当

ItemsControl
加载
Metadata
项目时:

  1. 已加载
    DataTemplate
    类型的
    Metadata

    现在,
    DataContext
    DataTemplate
    Metadata
    项。
  2. Metadata
    项目作为
    DataContext
    继承到此
    UserControl
    内的
    DataTemplate
  3. DataContext
    (仍然是相同的
    UserControl
    项)的
    Metadata
    再次继承到内部元素的
    DataContext
    。其中之一是
    ContentPresenter
  4. 根据
    ContentPresenter
    声明,
    ContentPresenter.Content
    将其
    DataContext
    属性绑定到当前
    Metadata
    ,即
    Binding
    项。
  5. Bindig
    返回并分配给
    ContentPresenter.Content
    属性的值将强制
    ContentPresenter
    尝试加载
    DataTemplate
    本身作为当前
    Content
    值。
  6. ContentPresenter
    找到内容值(
    DataTemplate
    项)的隐式
    Metadata
    并应用它。
  7. 加载
    DataTemplate
    还将创建一个新的
    UserControl
    实例,因为此
    UserControl
    DataTemplate
    的子级。
  8. 流程再次从1)开始==>无限循环==>耗尽堆栈内存==>抛出
    StackOverflowException

在不知道嵌套

ContentPresenter
的用途的情况下,您可以通过为其分配显式键来将
DataTemplate
定义为显式模板。这样嵌套的
ContentPresenter
就无法隐式加载它:

应用程序.xaml

DataTemplate
定义为显式

<DataTemplate x:Key="ListBoxItemDataTemplate" 
              DataType="{x:Type media:Metadata}">
    <controls:MediaContainer/>
</DataTemplate>

MainWindow.xaml

DataTemplate
显式分配给
ItemsControl.ItemTemplate
属性。

注意,因为

ListBox
ListView
都默认支持 UI 虚拟化,所以您不再需要覆盖默认的
ItemsPanelTemplate
:它已经是
VirtualizingStackPanel

<ListView x:Name="ListBlock"
          ItemTemplate="{StaticResource ListBoxItemDataTemplate"}>
</ListView>

现在无限循环被打破,因为嵌套在

ContentPresenter
中的
UserControl
不再找到
DataTemplate
,因为
DataTemplate
现在是显式的(使用显式
x:Key
注册)。

现在你应该能够更好地理解这个问题了。 最终如何解决取决于嵌套

ContentPresenter
的目的。通常,您不会显式设置
ContentPresenter.Content
属性。该值由框架隐式分配 - 如果
ContentPresenter
放置在
ContentControl
内(并且
UserControl
ContentControl
)。但最重要的是,将
ContentPresenter
绑定到
DataTemplate.DataContext
会给您带来问题 - 并且没有任何任何意义。

我强烈建议修复

DataTemplate
的布局,而不是明确显示
UserControl

© www.soinside.com 2019 - 2024. All rights reserved.