英文:
"Wrapper" control is just rendering its child, not its own content
问题
我们的应用程序有很多弹出窗口。我们希望在每个弹出窗口之间共享一些逻辑(例如,当用户点击“关闭”按钮时应该发生什么)。同时,虽然每个弹出窗口的内容不同,但我们希望它们都具有相同的一般布局。
我们的理解是我们应该构建:
PopupControl.cs
,用于存放共享逻辑,每个弹出窗口都将继承该类PopupWrapper.xaml
,其中将插入每个弹出窗口的内容
我们的第一个弹出窗口是用于显示错误消息的。我们能够显示弹出窗口的内容,但无法显示弹出窗口的包装器:
我们漏掉了什么?我怀疑与ControlTemplate
和ContentPresenter
有关。我们已经查阅了大量的文档,但没有找到明确的答案。
以下是相关的文件:
// PopupControl.cs
using System;
using System.Windows;
using System.Windows.Controls;
namespace TestApp
{
public class PopupControl : UserControl
{
public void Close_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine("Closed!");
}
}
}
<!-- PopupWrapper.xaml -->
<local:PopupControl x:Class="TestApp.PopupWrapper"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TestApp"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<ControlTemplate>
<StackPanel>
<TextBlock>(Popup wrapper) Alert!</TextBlock>
<ContentPresenter />
<Button Click="Close_Click">(Popup wrapper) Close</Button>
</StackPanel>
</ControlTemplate>
</local:PopupControl>
// PopupWrapper.xaml.cs
namespace TestApp
{
public partial class PopupWrapper : PopupControl
{
public PopupWrapper()
{
InitializeComponent();
}
}
}
<!-- ErrorPopup.xaml -->
<local:PopupControl x:Class="TestApp.ErrorPopup"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TestApp"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<local:PopupWrapper>
<StackPanel>
<TextBlock>(Popup contents) An error happened!</TextBlock>
<Button Click="Close_Click">Dismiss</Button>
</StackPanel>
</local:PopupWrapper>
</local:PopupControl>
// ErrorPopup.xaml.cs
namespace TestApp
{
public partial class ErrorPopup : PopupControl
{
public ErrorPopup()
{
DataContext = this;
InitializeComponent();
}
}
}
<!-- MainWindow.xaml -->
<Window x:Class="TestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestApp"
mc:Ignorable="d"
Title="Test" Height="450" Width="800">
<StackPanel>
<TextBlock>(Window) This is my app.</TextBlock>
<local:ErrorPopup></local:ErrorPopup>
</StackPanel>
</Window>
英文:
Our app has a lot of popups. We want to share some logic between each popup (e.g. what should happen when the user clicks the 'Close' button). Also while each popup will have different content, we want each one to have the same general layout.
Our understanding is we should build:
PopupControl.cs
to house the shared logic, which each Popup will subclassPopupWrapper.xaml
, into which each Popup's content will be inserted
Our first Popup is to display error messages. We're able to get the Popup's content to display, but not the Popup wrapper:
What are we missing? I suspect it's something with ControlTemplate
and ContentPresenter
. We've looked through oodles of documentation, but haven't found a clear answer.
Here are the relevant files:
// PopupControl.cs
using System;
using System.Windows;
using System.Windows.Controls;
namespace TestApp
{
public class PopupControl : UserControl
{
public void Close_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine("Closed!");
}
}
}
<!-- PopupWrapper.xaml -->
<local:PopupControl x:Class="TestApp.PopupWrapper"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TestApp"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<ControlTemplate>
<StackPanel>
<TextBlock>(Popup wrapper) Alert!</TextBlock>
<ContentPresenter />
<Button Click="Close_Click">(Popup wrapper) Close</Button>
</StackPanel>
</ControlTemplate>
</local:PopupControl>
// PopupWrapper.xaml.cs
namespace TestApp
{
public partial class PopupWrapper : PopupControl
{
public PopupWrapper()
{
InitializeComponent();
}
}
}
<!-- ErrorPopup.xaml -->
<local:PopupControl x:Class="TestApp.ErrorPopup"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TestApp"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<local:PopupWrapper>
<StackPanel>
<TextBlock>(Popup contents) An error happened!</TextBlock>
<Button Click="Close_Click">Dismiss</Button>
</StackPanel>
</local:PopupWrapper>
</local:PopupControl>
// ErrorPopup.xaml.cs
namespace TestApp
{
public partial class ErrorPopup : PopupControl
{
public ErrorPopup()
{
DataContext = this;
InitializeComponent();
}
}
}
<!-- MainWindow.xaml -->
<Window x:Class="TestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestApp"
mc:Ignorable="d"
Title="Test" Height="450" Width="800">
<StackPanel>
<TextBlock>(Window) This is my app.</TextBlock>
<local:ErrorPopup></local:ErrorPopup>
</StackPanel>
</Window>
答案1
得分: 0
虽然我不确定你嵌套的意图,关于PopupWrapper,你将ControlTemplate设置为其Content
属性,但它被ErrorPopup中的StackPanel覆盖了。你需要将ControlTemplate设置为其Template
属性。
<local:PopupControl x:Class="WpfApp1.PopupWrapper"
...>
<local:PopupControl.Template>
<ControlTemplate TargetType="{x:Type local:PopupControl}">
<StackPanel>
<TextBlock>(Popup wrapper) Alert!</TextBlock>
<ContentPresenter />
<Button Click="Close_Click">(Popup wrapper) Close</Button>
</StackPanel>
</ControlTemplate>
</local:PopupControl.Template>
</local:PopupControl>
英文:
Although I am not sure about your intention for nesting, regarding PopupWrapper, you set the ControlTemplate to its Content
property and it is overwritten by the StackPanel in ErrorPopup. You need to set the ControlTemplate to its Template
property instead.
<local:PopupControl x:Class="WpfApp1.PopupWrapper"
...>
<local:PopupControl.Template>
<ControlTemplate TargetType="{x:Type local:PopupControl}">
<StackPanel>
<TextBlock>(Popup wrapper) Alert!</TextBlock>
<ContentPresenter />
<Button Click="Close_Click">(Popup wrapper) Close</Button>
</StackPanel>
</ControlTemplate>
</local:PopupControl.Template>
</local:PopupControl>
答案2
得分: 0
如果您想在提供特定功能和布局的主机中托管动态内容,并且该布局应该由动态内容共享,您只需要定义一个包含ContentPresenter
的主机。ContentPresenter
将根据绑定到内容的数据模型呈现动态内容。布局由隐式的DataTemplate
定义。如果您使用显式的DataTemplate
定义,您将需要提供一个DataTemplateSelector
,根据当前内容模型选择适当的DataTemplate
。
换句话说,在WPF中,动态内容通常由ContentControl
托管并由DataTemplate
呈现。
主机可以简单地分为标题(动态内容上方的固定内容)、正文(动态内容区域)和页脚(动态内容下方的固定内容)。当然,您可以根据需要在固定/共享布局中添加任意多个动态内容占位符。
每个新内容必须由一个专用的内容模型表示(这是有意义的,因为内容需要数据源)。每个内容模型必须有一个专用的DataTemplate
来实际定义内容的布局。
建议让您的主机扩展ContentControl
。
供您参考,Popup
和Window
类也扩展了ContentControl
。根据您实际想要显示动态内容主机的方式(例如作为对话框或作为附加到特定元素的弹出窗口),您可以选择扩展Window
或Popup
。除了基类之外,下面的示例不会改变(因为这些类型都扩展了ContentControl
):
PopupControl.cs
public class PopupControl : ContentControl
{
public string Message
{
get => (string)GetValue(MessageProperty);
set => SetValue(MessageProperty, value);
}
public static readonly DependencyProperty MessageProperty = DependencyProperty.Register(
"Message",
typeof(string),
typeof(PopupControl),
new PropertyMetadata(default));
static PopupControl()
=> DefaultStyleKeyProperty.OverrideMetadata(typeof(PopupControl), new FrameworkPropertyMetadata(typeof(PopupControl)));
public PopupControl()
{
var closeInputGesture = new KeyGesture(Key.Escape);
var closeInputBinding = new InputBinding(ApplicationCommands.Close, closeInputGesture);
_ = this.InputBindings.Add(closeInputBinding);
// Define the command handler for the close button and the key gesture
// defined above
var closeCommandBinding = new CommandBinding(ApplicationCommands.Close, ExecuteCloseCommand, CanExecuteCloseCommand);
_ = this.CommandBindings.Add(closeCommandBinding);
}
private void CanExecuteCloseCommand(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = true;
private void ExecuteCloseCommand(object sender, ExecutedRoutedEventArgs e)
{
// TODO::Implement close control logic
this.Message = "I'm closed!";
}
}
Generic.xaml
Generic.xaml文件必须位于名为"Themes"的目录中,该目录必须位于项目文件结构的根目录中。
<ResourceDictionary>
<Style TargetType="local:PopupControl">
<Setter Property="HorizontalContentAlignment"
Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:PopupControl">
<Border>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Dynamic content header row -->
<RowDefinition /> <!-- Dynamic content row -->
<RowDefinition Height="Auto" /> <!-- Dynamic content footer row -->
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="{TemplateBinding Message}"
FontSize="22"
HorizontalAlignment="Center" />
<ContentPresenter Grid.Row="1"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
<!-- Use a routed command to communicate with the parent.
This button uses a predefined command from the .NET ApplicationCommands class -->
<Button Grid.Row="2"
Content="Close"
Command="{x:Static ApplicationCommands.Close}" />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
在定义动态消息框的框架即可重用部分之后,您必须定义动态内容。
此示例创建了两个简单的内容:一个黄色矩形和一个橙色矩形,上面有一个文本叠加层。
为此,我们必须为每个内容模型定义两个内容模板,并定义一个DataTemplate
来定义每个内容模型的布局:
OrangeRectangleContentModel.cs
public class OrangeRectangleContentModel : INotifyPropertyChanged
{
public OrangeRectangleContentModel(string textValue)
=> this.TextValue = textValue;
public string TextValue { get; }
public event PropertyChangedEventHandler? PropertyChanged;
}
YellowRectangleContentModel.cs
public class YellowRectangleContentModel : INotifyPropertyChanged
{
public YellowRectangleContentModel(string textValue)
=> this.TextValue = textValue;
public string TextValue { get; }
public event PropertyChangedEventHandler? PropertyChanged;
}
MainWindow.xaml
最后,定义各个DataTemplate
布局定义并使用PopupControl
。
<Window>
<Window.Resources>
<DataTemplate DataType="{x:Type local:OrangeRectangleContentModel}">
<Grid>
<Rectangle Fill="Orange"
Width="200"
Height="200" />
<TextBlock Text="{Binding TextValue}"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type local:YellowRectangleContentModel}">
<Grid>
<Rectangle Fill="Yellow"
Width="200"
Height="200" />
<TextBlock Text="{Binding TextValue}"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Grid>
</DataTemplate>
</Window.Resources>
<local:PopupControl Message="Dynamic content host"
Content="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=PopupContent}" />
<Window>
MainWindow.xaml.cs
partial class MainWindow : Window
{
public object PopupContent
{
get => (object)GetValue(PopupContentProperty);
set => SetValue(PopupContentProperty, value);
}
public static readonly DependencyProperty PopupContentProperty = DependencyProperty.Register(
"PopupContent",
typeof(object),
typeof(MainWindow),
new PropertyMetadata(default));
public MainWindow()
{
InitializeComponent();
this.Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
// Create the dynamic content and assign it to the PopupControl
// via data binding. In case of a dialog, you would call Window.ShowDialog from here
// after assigning the content model to the Window.Content property.
var dynamicPopupContent = new OrangeRectangleContentModel("I'm an orange rectangle");
this.PopupContent = dynamicPopupContent;
}
英文:
If you you want to host dynamic content inside a host that provides specific functionality and layout that is supposed to be shared by the dynamic content, you only need to define a single host that contains a ContentPresenter
. The ContentPresenter
will render the dynamic content based on a data model that is bound to the content. The layout is defined by an implicit DataTemplate
. In case you use explicit DataTemplate
definitions you would have to provide a DataTemplateSelector
that selects the proper DataTemplate
based on the current content model.
In other words, in WPF dynamic content is usually hosted by a ContentControl
and rendered by a DataTemplate
.
The host can be simply structured into a header (fix content above the dynamic content) a body (the dynamic content area) and a footer (fixed content below the dynamic content). Of course you can add as many dynamic content placeholders into the fixed/shared layout as required.
Each new content must be represented by a dedicated content model (which makes sense as the content requires a data source). Each content model must have a dedicated DataTemplate
to actually define the layout of the content.
It's recommended to let your host extend ContentControl
.
For your info, the classes Popup
and Window
also extend ContentControl
. Depending on how you actually want to show the dynamic content host (for example as dialog or as a popup that is attached to a particular element) you may choose to extend Window
or Popup
instead. Other that the base class, the below example does not change (because those types are all extending ContentControl
):
PopupControl.cs
public class PopupControl : ContentControl
{
public string Message
{
get => (string)GetValue(MessageProperty);
set => SetValue(MessageProperty, value);
}
public static readonly DependencyProperty MessageProperty = DependencyProperty.Register(
"Message",
typeof(string),
typeof(PopupControl),
new PropertyMetadata(default));
static PopupControl()
=> DefaultStyleKeyProperty.OverrideMetadata(typeof(PopupControl), new FrameworkPropertyMetadata(typeof(PopupControl)));
public PopupControl()
{
var closeInputGesture = new KeyGesture(Key.Escape);
var closeInputBinding = new InputBinding(ApplicationCommands.Close, closeInputGesture);
_ = this.InputBindings.Add(closeInputBinding);
// Define the command handler for the close button and the key gesture
// defin ed above
var closeCommandBinding = new CommandBinding(ApplicationCommands.Close, ExecuteCloseCommand, CanExecuteCloseCommand);
_ = this.CommandBindings.Add(closeCommandBinding);
}
private void CanExecuteCloseCommand(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = true;
private void ExecuteCloseCommand(object sender, ExecutedRoutedEventArgs e)
{
// TODO::Implement close control logic
this.Message = "I'm closed!";
}
}
Generic.xaml
The Generic.xaml file must be located in a directory named "Themes" that must be located at the root of the project's file structure.
<ResourceDictionary>
<Style TargetType="local:PopupControl">
<Setter Property="HorizontalContentAlignment"
Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:PopupControl">
<Border>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Dynamic content header row -->
<RowDefinition /> <!-- Dynamic content row -->
<RowDefinition Height="Auto" /> <!-- Dynamic content footer row -->
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="{TemplateBinding Message}"
FontSize="22"
HorizontalAlignment="Center" />
<ContentPresenter Grid.Row="1"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
<!-- Use a routed command to communicate with the parent.
This button uses a predefined command from the .NET ApplicationCommands class -->
<Button Grid.Row="2"
Content="Close"
Command="{x:Static ApplicationCommands.Close}" />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
After defining the framework for your dynamic message box i.e. the reusable part, you have to define the dynamic content.
This example creates to simple contents: a yellow and an orange rectangle with a text overlay.
To accomplish this we have to define two content models and two layouts by defining a DataTemplate
for each content model:
OrangeRectangleContentModel.cs
public class OrangeRectangleContentModel : INotifyPropertyChanged
{
public OrangeRectangleContentModel(string textValue)
=> this.TextValue = textValue;
public string TextValue { get; }
public event PropertyChangedEventHandler? PropertyChanged;
}
YellowRectangleContentModel.cs
public class YellowRectangleContentModel : INotifyPropertyChanged
{
public YellowRectangleContentModel(string textValue)
=> this.TextValue = textValue;
public string TextValue { get; }
public event PropertyChangedEventHandler? PropertyChanged;
}
MainWindow.xaml
Finally define the individual DataTemplate layout definitions and use the PopupControl.
<Window>
<Window.Resources>
<DataTemplate DataType="{x:Type local:OrangeRectangleContentModel}">
<Grid>
<Rectangle Fill="Orange"
Width="200"
Height="200" />
<TextBlock Text="{Binding TextValue}"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type local:YellowRectangleContentModel}">
<Grid>
<Rectangle Fill="Yellow"
Width="200"
Height="200" />
<TextBlock Text="{Binding TextValue}"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Grid>
</DataTemplate>
</Window.Resources>
<local:PopupControl Message="Dynamic content host"
Content="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=PopupContent}" />
<Window>
MainWindow.xaml.cs
partial class MainWindow : Window
{
public object PopupContent
{
get => (object)GetValue(PopupContentProperty);
set => SetValue(PopupContentProperty, value);
}
public static readonly DependencyProperty PopupContentProperty = DependencyProperty.Register(
"PopupContent",
typeof(object),
typeof(MainWindow),
new PropertyMetadata(default));
public MainWindow()
{
InitializeComponent();
this.Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
// Create the dynamic content and assign it to the PopupControl
// via data binding. In case of a dialog, you would call Window.ShowDialog from here
// after assigning the content model to the Window.Content property.
var dynamicPopupContent = new OrangeRectangleContentModel("I'm an orange rectangle");
this.PopupContent = dynamicPopupContent;
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论