我有一个数据类型,
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);
}
}
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
项目时:
DataTemplate
类型的 Metadata
。DataContext
的 DataTemplate
是 Metadata
项。Metadata
项目作为 DataContext
继承到此 UserControl
内的 DataTemplate
。DataContext
(仍然是相同的UserControl
项)的Metadata
再次继承到内部元素的DataContext
。其中之一是ContentPresenter
。ContentPresenter
声明,ContentPresenter.Content
将其 DataContext
属性绑定到当前 Metadata
,即 Binding
项。Bindig
返回并分配给 ContentPresenter.Content
属性的值将强制 ContentPresenter
尝试加载 DataTemplate
本身作为当前 Content
值。ContentPresenter
找到内容值(DataTemplate
项)的隐式 Metadata
并应用它。DataTemplate
还将创建一个新的 UserControl
实例,因为此 UserControl
是 DataTemplate
的子级。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
。