简体   繁体   中英

Visual Studio Designer shows empty Window when using custom ContentPropertyAttribute

My application has a lot of windows and most of them share some basic features. Because of that I extended the Window class to create a base for all my windows.

Everything compiles and displays fine but the designer just shows an empty window when I use my window class.

I made a basic example that can be easily used, my real window is much more complex but this shows the problem. Here is the code:

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 example for using 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>

Doing the exact same thing with a UserControl works just fine, also in the designer. Just replace every occurance of MyWindow with MyUserControl and extend from UserControl if you want to try that.

Is there any way I can get a custom Window like that to work with the designer, or do i have to make a UserControl and use that in every window? Also, is this some kind of bug or intended behavior?

Addional info: I'm running Visual Studio 2015 Community and I'm using .net 4.6

I Also tried another approach. Instead of using the ContentPropertyAttribute i have overwritten the ContentProperty like this:

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

Again this works completely fine with a UserControl . With a Window I can at least see the Content in the designer now, but the ToolBar and StatusBar are still not showing up in the designer. When running it everything works correctly.

First, I am no super expert on WPF, but have done a bunch and think I can offer and help clarify some components. First, you can NOT derive from a .XAML based declaration of a WPF-Window, it can only be if entirely within code. I have come to find that sometimes the visual element building is much easier to do in XAML than it is within code, but both can and do work.

So, that said, I would like to offer a solution that might work for you. Starting with WPF Window Style / Templatea , if you are not already familiar with them, along with other controls you can run through their defaults.

First, I am starting with a RESOURCE DICTIONARY STYLE definition that will mimic much of what you may want in your default form. This becomes the stuff within the "ControlTemplate" of the style definition. I have created this as a file "MyWindowStyle.xaml" at the root level WpfApplication1 I created on my machine (just to match your sample project file namespace reference).

Inside the template, you could have almost anything... grids, dock panel, stack panels, etc. In this case, I have used a DockPanel and added your sample ToolBar, StatusBar and two extra labels just for sample. I also preset size and bogus color just to give visualization of the parts when you confirm their impact.

The CRITICAL element to look at is the . This identifies where the content for each of your DERIVED Windows content will be placed... Think of it as a place-holder for each one of your forms for individuality while the rest of the form, its controls all remain consistent. You will see it come into play as you play around with it.

The content of it is and notice the style x:Key="MyWindowStyle". This coincidentally is the same as the xaml, but you could have 100's of styles within a single resource dictionary. I am keeping simple to just the one for demo purposes.

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

Next, we need to make this publicly available for the entire duration of the application, including availability within the designer mode... Within your projects "App.xaml" which is the startup for the application, it will have a default and empty area. Replace it with this.

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

Now, to a CODE-ONLY (not a .xaml window based definition) of your "MyWindow.cs" class. If you look at the style where I declared the toolbar and statusbar, I assigned them the names of "tmpToolBar" and "tmpStatusBar" respectively. Notice the [TemplatePart()] declarations. I am now expecting the template to HAVE these controls by the given name within the TEMPLATE somewhere.

Within the constructor, I am loading the Style from the App.xaml resource dictionary being fully available. Then I follow-up with the OnApplyTemplate() which I typically heavily document my code so anyone following me has some idea how / where things originated from and self explanatory.

My entire "MyClass.cs" is below

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

So now, lets put it into place. Have your application's main window derive from the MyWindow class. The only thing you need there is

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

In the DESIGNER of your form, put in a few controls, such as label, textbox, whatever. You do not see your actual other style yet, but just go with it. Save and run the sample app. Your main window should be displayed with the entire pre-defined template there ALONG WITH the few extra control you had placed specifically on this form.

Now, to get the full visualization in your "MainWindow" from the designer perspective. Within the .xaml area of

<my:MyWindow
    x:Class="WpfApplication1.MainWindow"
    [other declarations]  >

just add the following before the close ">"

Style="{StaticResource MyWindowStyle}"

The resource is available via the App.xaml at startup and you should now be able to see the entire visual while you are designing... but you cant change the outermost template, just the content specific to this one page as mentioned about the "ContentPresenter" part of the template definition. What you are changing is within the place-holder portion allocated by the Template. If you want to change the main part of the window controls, you need to update the TEMPLATE!

But here is part of the trick of the template designer. Start with this, and build in what you need visually, get it placed right and once ready, take it out of here and put into the template and now it is applicable to the rest of all windows. Fix fonts, sizes, colors, whatever.

Hope this helps, and if you have any questions for follow-up, let me know.

Window class is very complex in compare to UserControl class. Microsoft has written more than 8k lines of code in Window class compare to 80 lines in UserControl , additionally Window class contain many operation/event/restriction on content property , and any one piece of code is hindering in rendering the content when you use [ContentProperty("ContentElement")] with the Window subclass MyWindow .

Probably making it a UserControl is better option, If not possible you can write some code temporarily(copy code from ContentElement property) in content property to see the design view.

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

and then just remove the code before run time. (Not a good idea but, whatever works.:) and I suspect that you have already figured that out.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM