Visual Studio 设计器在使用自定义 ContentPropertyAttribute 时显示为空 Window

Visual Studio Designer shows empty Window when using custom ContentPropertyAttribute

我的应用程序有很多 windows,其中大部分共享一些基本功能。因此,我扩展了 Window class 来为我所有的 windows.

创建一个基础

一切都编译并显示正常,但当我使用我的 window class.

时,设计器只显示一个空的 window

我做了一个简单易用的基本示例,我的真实window要复杂得多,但这说明了问题。 这是代码:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Markup;

namespace WpfApplication1
{
    [ContentProperty("ContentElement")]
    public class MyWindow : Window
    {
        public ToolBar ToolBar { get; private set; }
        public StatusBar StatusBar { get; private set; }
        public Border ContentBorder { get; private set; }

        public UIElement ContentElement
        {
            get { return (UIElement)GetValue(ContentElementProperty); }
            set { SetValue(ContentElementProperty, value); }
        }
        public static readonly DependencyProperty ContentElementProperty = DependencyProperty.Register(
            "ContentElement", typeof(UIElement), typeof(MyWindow),
            new PropertyMetadata(null, (d, e) =>
             {
                 MyWindow w = (MyWindow)d;
                 w.ContentBorder.Child = (UIElement)e.NewValue;
             }));

        public MyWindow() : base()
        {
            ToolBar = new ToolBar();
            ToolBar.Height = 30;
            ToolBar.VerticalAlignment = VerticalAlignment.Top;

            StatusBar = new StatusBar();
            StatusBar.Height = 20;
            StatusBar.VerticalAlignment = VerticalAlignment.Bottom;

            ContentBorder = new Border();
            ContentBorder.SetValue(MarginProperty, new Thickness(0, 30, 0, 20));

            Grid grid = new Grid();
            grid.Children.Add(ToolBar);
            grid.Children.Add(ContentBorder);
            grid.Children.Add(StatusBar);
            Content = grid;
        }
    }
}

XAML 使用 MyWindow 的示例:

<local:MyWindow x:Class="WpfApplication1.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:WpfApplication1"
        mc:Ignorable="d"
        Title="MainWindow" Height="300" Width="300">
    <Grid>
        <Rectangle Fill="Blue" />
    </Grid>
</local:MyWindow>

UserControl 做完全相同的事情就很好,在设计器中也是如此。如果您想尝试,只需将 MyWindow 的每次出现替换为 MyUserControl 并从 UserControl 扩展。

有什么方法可以让我获得像这样的自定义 Window 来与设计师合作,还是我必须制作一个 UserControl 并在每个 window 中使用它? 另外,这是某种错误还是预期行为?

附加信息:我是 运行 Visual Studio 2015 社区,我使用的是 .net 4.6

我也试过另一种方法。我没有使用 ContentPropertyAttribute,而是像这样覆盖了 ContentProperty:

new public object Content {
    get { return GetValue(ContentProperty); }
    set { SetValue(ContentProperty, value); }
}
new public static DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(object), typeof(BaseUserControl), new PropertyMetadata(null, (s, e) =>
{
    MyWindow bw = (MyWindow)s;
    bw.ContentBorder.Child = (UIElement)e.NewValue;
}));

再一次,这对 UserControl 完全有效。使用 Window 我现在至少可以在设计器中看到内容,但是 ToolBarStatusBar 仍然没有出现在设计器中。 运行 一切正常。

首先,我不是 WPF 的超级专家,但做了很多,认为我可以提供并帮助澄清一些组件。首先,您不能从基于 .XAML 的 WPF-Window 声明派生,它只能完全在代码中。我发现有时在 XAML 中构建视觉元素比在代码中构建要容易得多,但两者都可以而且确实有效。

也就是说,我想提供一个可能适合您的解决方案。从 WPF Window Style / Templatea 开始,如果您还不熟悉它们以及其他控件,您可以 运行 通过它们的默认设置。

首先,我从一个 RESOURCE DICTIONARY STYLE 定义开始,它将模仿您在默认表单中可能需要的大部分内容。这成为样式定义的 "ControlTemplate" 中的内容。我在我的机器上创建的根级别 WpfApplication1 中将其创建为文件 "MyWindowStyle.xaml"(只是为了匹配您的示例项目文件命名空间引用)。

在模板中,您几乎可以拥有任何东西...网格、停靠面板、堆栈面板等。在这种情况下,我使用了 DockPanel 并添加了您的示例工具栏、状态栏和两个额外的标签,仅供示例使用.我还预设了尺寸和伪造的颜色,只是为了在您确认它们的影响时让零件可视化。

要查看的关键元素是 .这标识了每个 DERIVED Windows 内容的内容将被放置的位置...将其视为每个表单的占位符以实现个性,而表单的其余部分及其控件都保持一致.当你玩弄它时,你会看到它发挥作用。

它的内容是,注意样式x:Key="MyWindowStyle"。这巧合地与 xaml 相同,但您可以在单个资源字典中拥有 100 种样式。为了演示目的,我保持简单。

<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >

    <Style x:Key="MyWindowStyle" TargetType="Window">
        <Setter Property="SnapsToDevicePixels" Value="true" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type Window}">
                    <Grid>
                        <Grid.Background>
                            <SolidColorBrush Color="{DynamicResource WindowColor}"/>
                        </Grid.Background>
                        <AdornerDecorator>
                            <DockPanel LastChildFill="True" Background="Blue">
                                <!-- List items docked to the top based on top-most first going down -->
                                <ToolBar x:Name="tmpToolBar" Height="45" DockPanel.Dock="Top" />
                                <Label Content="Testing by Style"
                                       Height="30" Width="150" DockPanel.Dock="Top"/>

                                <!-- When docking to the bottom, start with bottom most working up -->
                                <StatusBar x:Name="tmpStatusBar" Height="30" 
                                    Background="Yellow" DockPanel.Dock="Bottom" />
                                <Label Content="Footer area based from style"
                                       Height="30" Width="250" DockPanel.Dock="Bottom" />

                                <!-- This one, since docked last is rest of the space of the window -->
                                <ContentPresenter DockPanel.Dock="Bottom"/>
                            </DockPanel>
                        </AdornerDecorator>
                        <ResizeGrip x:Name="WindowResizeGrip"
                            HorizontalAlignment="Right"
                            VerticalAlignment="Bottom"
                            Visibility="Collapsed"
                            IsTabStop="false" />
                    </Grid>

                    <ControlTemplate.Triggers>
                        <Trigger Property="ResizeMode" Value="CanResizeWithGrip">
                            <Setter TargetName="WindowResizeGrip" 
                                    Property="Visibility" Value="Visible" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>   

接下来,我们需要在应用程序的整个持续时间内使其公开可用,包括在设计器模式下的可用性...在您的项目 "App.xaml" 中,这是应用程序的启动,它将具有默认和空白区域。换成这个。

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary  Source="pack://application:,,,/WpfApplication1;component/MyWindowStyle.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

现在,您的 "MyWindow.cs" class 的代码(不是基于 .xaml window 的定义)。如果你看一下我声明工具栏和状态栏的样式,我分别为它们分配了 "tmpToolBar" 和 "tmpStatusBar" 的名称。注意 [TemplatePart()] 声明。我现在期望模板在 TEMPLATE 某处通过给定名称拥有这些控件。

在构造函数中,我从完全可用的 App.xaml 资源字典中加载样式。然后我跟进 OnApplyTemplate(),我通常会大量记录我的代码,这样任何关注我的人都知道事情是如何/从哪里起源的,并且不言自明。

我整个"MyClass.cs"在下面

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;

namespace WpfApplication1
{
    [TemplatePart(Name = "tmpToolBar", Type = typeof(ToolBar))]
    [TemplatePart(Name = "tmpStatusBar", Type = typeof(StatusBar))]
    public class MyWindow : Window
    {
        protected ToolBar myToolBar;
        protected StatusBar myStatusBar;

        public MyWindow() : base()
        {
            // NOW, look for the resource of "MyWindowStyle" within the dictionary
            var tryStyle = FindResource("MyWindowStyle") as Style;
            // if a valid find and it IS of type Style, set the style of 
            // the form to this pre-defined format and all it's content
            if (tryStyle is Style)
                Style = tryStyle;
        }

        // the actual template is not applied until some time after initialization.
        // at that point, we can then look to grab object references to the controls
        // you have need to "hook up" to.
        public override void OnApplyTemplate()
        {
            // first allow default to happen
            base.OnApplyTemplate();

            // while we get the style loaded, we can now look at the expected template "parts"
            // as declared at the top of this class.  Specifically looking for the TEMPLATE
            // declaration by the name "tmpToolBar" and "tmpStatusBar" respectively.

            // get object pointer to the template as defined in the style template
            // Now, store those object references into YOUR Window object reference of Toolbar
            var myToolBar = Template.FindName("tmpToolBar", this) as ToolBar;
            if (myToolBar != null)
                // if you wanted to add your own hooks to the toolbar control
                // that is declared in the template
                myToolBar.PreviewMouseDoubleClick += myToolBar_PreviewMouseDoubleClick;

            // get object pointer to the template as defined in the style template
            var myStatusBar = Template.FindName("tmpStatusBar", this) as StatusBar;
            if (myStatusBar != null)
                myStatusBar.MouseDoubleClick += myStatusBar_MouseDoubleClick;

            // Now, you can do whatever else you need with these controls downstream to the 
            // rest of your derived window controls
        }

        void myToolBar_PreviewMouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            // in case you wanted to do something based on PreviewMouseDoubleClick of the toolbar
            MessageBox.Show("ToolBar: Current Window Class: " + this.ToString());
        }

        void myStatusBar_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            // in case something for MouseDoubleClick on the StatusBar
            MessageBox.Show("StatusBar: Current Window Class: " + this.ToString());
        }
    }
}

所以现在,让我们把它放到位。让您的应用程序的主 window 派生自 MyWindow class。你唯一需要的是

namespace WpfApplication1
{
    public partial class MainWindow : MyWindow
    {}
}

在表单的设计器中,放入一些控件,例如标签、文本框等。你还没有看到你真正的其他风格,但随它去吧。保存并 运行 示例应用程序。您的主要 window 应该与整个预定义模板以及您专门放置在此表单上的一些额外控件一起显示。

现在,从设计师的角度在您的 "MainWindow" 中获得完整的可视化效果。在

的 .xaml 区域内
<my:MyWindow
    x:Class="WpfApplication1.MainWindow"
    [other declarations]  >

只需在结束符“>”前添加以下内容即可

Style="{StaticResource MyWindowStyle}"

资源可在启动时通过 App.xaml 获得,您现在应该能够在设计时看到整个视觉效果...但是您不能更改最外层的模板,只能更改特定于此的内容关于模板定义的 "ContentPresenter" 部分提到的一页。您要更改的内容在模板分配的占位符部分内。如果要更改 window 控件的主要部分,则需要更新模板!

但这是模板设计器的部分技巧。从这个开始,在视觉上构建你需要的东西,把它放在正确的位置,一旦准备好,把它从这里拿出来放到模板中,现在它适用于所有其他东西 windows。修复字体、大小、颜色等等。

希望这对您有所帮助,如果您有任何后续问题,请告诉我。

Window class 与 UserControl class 相比非常复杂。与 UserControl 中的 80 行相比,Microsoft 在 Window class 中编写了超过 8k 行代码,另外 Window class 包含许多 operation/event/restriction content property,当您将 [ContentProperty("ContentElement")]Window subclass MyWindow 一起使用时,任何一段代码都会阻碍呈现内容。

可能使它成为UserControl是更好的选择,如果不可能你可以临时写一些代码(从ContentElement 属性复制代码)在content property中看看设计视图。

<lib:MyWindow.Content>
    <Button  Content="Click"   Width="200"   />
</lib:MyWindow.Content>

然后在 运行 时间之前删除代码。 (这不是个好主意,但不管怎样。:)我怀疑你已经明白了。