Sunday, June 10, 2007

Don't subclass a Panel, unless you're making a Panel

I have a tongue-in-cheek thing I say a lot about WPF compared to Windows Forms:

In Windows Forms, there are two ways to do everything: a good way and a bad way.

In WPF, there are ten ways to do everything: two that are amazing, 3 that are good, 1 that is bad, and 4 that suck.

My goal in this post is to help move people upstream towards amazing, specifically how and when to use Panel (and its subclasses).

Markup subclassing

Everytime you go into VS and create a new WPF project, you see markup subclassing. This is the Window x:Class="WPFApplication1.Window1" stuff. It allows you to associate a piece of XAML to a subclass you define in Code. There are four approved places you'll see this in WPF: Application, Window, Page, and UserControl.

More than once, though, I've seen clever people (both outside and inside Microsoft) take this approach one step further. They want to reuse a piece of UI several times. Maybe they have a Grid defined with a set of columns, rows, and controls defined. It's pretty easy to take that Xaml-defined Grid (with all of its XML child elements), put it in it's own Xaml file, slap on an x:Class statement and start rolling. Things work great.

Rock-n-roll, right?

Well, not really.

Inheritance for polymorphism

I read a great internal paper at MS once--written by one of those brilliant old-guard devs with decades of perspective. The point the author pushed: use inheritance only for polymorphism. Translated: when making MyGrid, only subclass Grid if you mean for people to treat your new MyGrid just like another Grid. Code (and a user, for the most part) that deals with Grid should be able to "deal" with MyGrid without any issues.

Where things break down

So let's say you define your BadGrid Xaml like this (with an associated BadGrid.xaml.cs file with properties, events, etc):

<Grid x:Class="PanelFun.BadGrid"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<
Grid.RowDefinitions>
<
RowDefinition/>
<
RowDefinition/>
<
RowDefinition/>
</
Grid.RowDefinitions>
<
Grid.ColumnDefinitions>
<
ColumnDefinition/>
<
ColumnDefinition Width="100"/>
</
Grid.ColumnDefinitions>

<
Label Grid.Row="0" Grid.Column="0" Content="_First Name" Target="{Binding ElementName=FirstName}" />
<
TextBox Grid.Row="0" Grid.Column="1" Name="FirstName" />

<
Label Grid.Row="1" Grid.Column="0" Content="_Middle Name" Target="{Binding ElementName=MiddleName}" />
<
TextBox Grid.Row="1" Grid.Column="1" Name="MiddleName" />

<
Label Grid.Row="2" Grid.Column="0" Content="_Last Name" Target="{Binding ElementName=LastName}" />
<
TextBox Grid.Row="2" Grid.Column="1" Name="LastName" />

</
Grid>

Looks okay, right?


Let's use it.

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PanelFun">
    <local:BadGrid/>        
</Window>

Run the app. No problem!


Now let's say a naive user (maybe someone in your company or a customer) takes your control and sees that it's a Grid. They write this Xaml:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PanelFun">

<local:BadGrid>
<
Grid.RowDefinitions>
<
RowDefinition/>
</
Grid.RowDefinitions>

<
Label Grid.Row="3" Grid.Column="0" Content="_Favorite Color"/>
<
TextBox Grid.Row="3" Grid.Column="1" />
</
local:BadGrid>
</Window>

Things compile fine. They are using BadGrid just like a Grid. No problem, right?


The problem, of course, is that you didn't mean people to use BadGrid like a Grid. You wanted them to use it like a simple Control. Weird things happen, dogs and cats living together, mass hysteria.


In this case, you get the user-created row inserted into your grid. If the user tries to put a name on the TextBlock, the compiler blows-up because of naming conflicts between the definition scope (in BadGrid.Xaml) and the usage scope (in the Window). The user has no clue why things blew up. (I'm not all that sure myself, actually.)


If you want to use markup subclassing, stay inside the fences


Application, Window, Panel, UserControl. It's pretty simple to paste your desired Xaml into a UserControl, with no unplanned weirdness.


If you want to extend markup subclassing further, we should talk to Rob. He's the Xaml guy. :-)


If you want to do any subclassing, pick the right base class


In general (even if you're not using Markup subclassing), make sure you "mean it" when you use inheritance in WPF (or any object orientated framework, for that matter). If you subclass a ContentControl, do you mean people to treat it like a ContentControl all the time? Same thing for ItemsControl, Decorator, and Panel.


When would I subclass a Panel?


Well, if you want to make your own panel, start with Panel. AniTilePanel from my bag-o-tricks is a good example.


You might want to subclass a bit deeper, but only in special cases.You could make OrderedStackPanel, where you introduce your own AttachedProperty: OrderedStackPanel.Order. This can be applied to items and your panel will order the items in the stack based on the property. The cool thing: without the property set, your panel should act just like StackPanel. This holds to the theory about inheritance and polymorphism.


Make Sense? Sound good? Hack on!

5 comments:

Ian said...

But UserControl also breaks the "Inheritance for Polymorphism." rule.

It derives from Control, but unlike WPF's other built-in controls, setting its Template does nothing useful. (On the contrary, doing so will most likely break the user control.)

Worse, UserControl derives only indirectly from Control. It does so via ContentControl. This is the base class of controls to which you can add content. And yet trying to give UserControl content will cause it to break.

So UserControl doesn't look any better than Grid. It takes features that form part of the public feature set in the base class, and repurposes them as implementation details, leading to poor encapsulation.

That's certainly why I've used various panels as base classes in the past. I was aware of the problems you describe here, but UserControl seemed to have all the same problems. Since you can't avoid the problems, why not at least pick the most convenient base class?

About the only thing in UserControl's favour is that it is at least a highly recognizable violation of the principles you describe here. So arguably it's less likely to be misunderstood than violating exactly the same principles with other base classes.

Kevin Moore said...

Ian: you're absolutely right. If I had been a bit more savvy, I may have fought harder to keep the original FE-based solution.

I agree w/ your analysis, though. Better to have a highly visible violation. Also, people tend to have a very solid idea of the model for Grid or StackPanel, which they may try to impose on a subclass. This is less the case with just Control.

Malmer said...

An other option to keep UserControl from getting messed up is by hiding the parent's ContentProperty with your own. Using the "new" keyword:

public static readonly new DependencyProperty ContentProperty = DependencyProperty.Register( "Content", typeof( Object ), typeof( MyUserControl ) );

and the clr property:

public new Object Content
{
get { return (Object)this.GetValue( ContentProperty ); }
set { this.SetValue( ContentProperty, value ); }
}

This also adds the benefit of baing able to add a contentpresenter without using templating in your UserControl XAML:

[ContentPresenter Content="{Binding ElementName=userControl,Path=Content}" /]

(using [ and ] instead of tags since it isn't allowed in post)

Might not be the world's most elegant sollution, but it sure is more conventient than working with templates.

jaap.taal said...

I've experimented with UserControls and there was one thing that wasn't working very well.
I've created some sort of splitpanel with buttons that can maximize/minimize a top area and/or a bottom area.
I use DependencyProperties of type Object to pass other WPF (or some custom control) elements.

The namescope problem Kevin describes occurs here too.
The problem I've found is described more detailed on this WPF Forum post:
http://forums.microsoft.com/forums/ShowPost.aspx?PostID=2230427&SiteID=1

My conclusion is not to pass WPF objects to user controls, only pass data to user controls.

Anonymous said...

I've been surfing this topic for a couple days now and I hear what you're saying. However, what WOULD be nice is the ability to create, declaritively, a base xaml Systems.Windows.COntrols.Page object (drag and drop from the toolbox) and then have other pages inherit this guy. I understand that I can create my base page all in code, and I have, but like you say, somebody comes along and doesn't use it as a control and whammo you're in trouble.

Here's what would be nice. You inherit the xaml page and in design time your inherting page displays the base xaml - design time. THis would prevent developers from hacking up the existing code.

Anyway, this sucks that I can't inherit base xaml for the page object.