Nov 21, 2007

WPF “Master Page” like functionality

UPDATE: I found and issue with placing the default content elements in the Resources for the Page. When you place the elements directly in the resources, without wrapping them in a template, there is only one instance of the elements created. This means that after the MasterPage control is created the first time the elements will never get re-initialized. This causes problems when you have buttons or other interactions that bind to properties specific to current instance of the master page.

For example : If you had a ButtonContentArea defined and you placed a save button in that area with a Command "SaveCommand" that was bound to a command on the consumer of the MasterPage control, that command would be initialized the first time the MasterPage control is created. Then any subsequent consumers of the master page would use the Binding from the first instance of the master page. This means your save command would be routed to the wrong handler. If you make the Dependency Properties of Type DataTemplate and you wrap the elements in a DataTemplate then bind to the ContentTemplate property of the controls you can resolve this issue. The code below has been updated to reflect this change.

Now on with your regularly scheduled programming.

Coming from an ASP.NET background one of the things I really like are Master Pages. If you aren't familiar with master pages read this first.

In WPF there are several different ways to share styles and templates across pages. Recently I had a need to share a common layout across multiple pages while still allowing for different content and behaviors on each page. The first thing I thought was Master Page. However this feature doesn't come out of the box with WPF.

However it is easy enough to implement your own version. A quick search provided me with Karin Huber's code project article about this very subject. This approach uses a user control as the master page. After playing around with this example one problem I found is that you are unable to name elements that are inside of the master page user control (others have discovered this as well if you view the comments on the codeplex project or you view Rob Relyea's blog post) This isn't a deal breaker by any means but I wanted to give a go at implementing it myself. It was surprisingly easy when I got down to it.

First I created a simple custom control named Master Page and a few dependency properties for the different content zones I am going to expose. Here is what that looks like

public class MasterPage : Control
{
public static DependencyProperty MainContentAreaProperty =
DependencyProperty.Register("MainContentArea", typeof (DataTemplate), typeof (MasterPage));

public static DependencyProperty HeaderContentAreaProperty =
DependencyProperty.Register("HeaderContentArea", typeof(DataTemplate), typeof(MasterPage));

public static DependencyProperty RightContentAreaProperty =
DependencyProperty.Register("RightContentArea", typeof(DataTemplate), typeof(MasterPage));

public static DependencyProperty FooterContentAreaProperty =
DependencyProperty.Register("FooterContentArea", typeof(DataTemplate), typeof(MasterPage));

static MasterPage()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MasterPage), new FrameworkPropertyMetadata(typeof(MasterPage)));
}

public DataTemplate MainContentArea
{
get { return (DataTemplate)GetValue(MainContentAreaProperty); }
set { SetValue(MainContentAreaProperty, value); }
}

public DataTemplate HeaderContentArea
{
get { return (DataTemplate)GetValue(HeaderContentAreaProperty); }
set { SetValue(HeaderContentAreaProperty, value); }
}

public DataTemplate RightContentArea
{
get { return (DataTemplate)GetValue(RightContentAreaProperty); }
set { SetValue(RightContentAreaProperty, value); }
}

public DataTemplate FooterContentArea
{
get { return (DataTemplate)GetValue(FooterContentAreaProperty); }
set { SetValue(FooterContentAreaProperty, value); }
}

}

The generic.xaml is straightforward. I placed ContentControls where I wanted "content zones." I named them accordingly and I bound the ContentTemplate property to the appropriate dependency property

<Style TargetType="{x:Type local:MasterPage}">
<Setter Property="HeaderContentArea" Value="{StaticResource HeaderContent}"/>
<Setter Property="MainContentArea" Value="{StaticResource MainContent}"/>
<Setter Property="RightContentArea" Value="{StaticResource RightContent}"/>
<Setter Property="FooterContentArea" Value="{StaticResource FooterContent}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MasterPage}">
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="225"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="50"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="25"></RowDefinition>
<RowDefinition Height="25"></RowDefinition>
</Grid.RowDefinitions>
<ContentControl Grid.Column="0" Grid.Row="0" x:Name="HeaderContentArea" ContentTemplate="{TemplateBinding HeaderContentArea}"/>
<ContentControl Grid.Column="0" Grid.Row="1" x:Name="MainContentArea" ContentTemplate="{TemplateBinding MainContentArea}"/>
<ContentControl Grid.Column="0" Grid.Row="2" x:Name="FooterContentArea" ContentTemplate="{TemplateBinding FooterContentArea}"/>
<ContentControl Grid.Column="1" Grid.RowSpan="3" x:Name="RightContentArea" ContentTemplate="{TemplateBinding RightContentArea}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

This allows a consumer of this control to bind any piece of content they want to the dependency property of their choice and that content will be displayed in the "content zone" defined in the generic.xaml.

For example your Window1.xaml would look something like this 

<Window.Resources>
<DataTemplate x:Key="HeaderContent">
<StackPanel>
<TextBlock Text="Header Text"/>
<Rectangle Fill="Black" Width="25" Height="25"/>
</StackPanel>
</DataTemplate>

<DataTemplate x:Key="RightContent">
<StackPanel Grid.RowSpan="3">
<Rectangle Width="100" Height="50" Fill="DarkBlue"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
<local:MasterPage HeaderContentArea="{StaticResource HeaderContent}" RightContentArea="{StaticResource RightContent}" />


Pretty simple right?

Well what if we don't bind to all the available content zones like in the example above? With the current setup the layout elements (in this case grid rows / columns) will still be rendered but they will be empty. This may or may not be the desired behavior for your situation.

Furthermore you don't want to build this nice layout template and still force your consumers to bind content that will be static across multiple consumers. So we want to provide default content for the content zones from within the generic.xaml.

Easy enough we just need to bind the content property of our zone to a static resource defined in generic.xaml right?

Something like this :

<DataTemplate x:Key="FooterContent">
<StackPanel>

<TextBlock Text="Copyright 2007. All rights reserved"/>
</StackPanel>
</DataTemplate>

<ContentControl Grid.Column="0" Grid.Row="2" x:Name="FooterContentArea" Content="{StaticResource FooterContent}"/>

This will work and will give you a distinct behavior. By doing this you are no longer binding to the dependency property defined in the control and therefore you are forcing the content zone to use the content defined in the generic.xaml. Your consumer will not be able override the content for this zone. This may or may not be the desired behavior.

For my situation I wanted to provide default content for the different zones but I also wanted to let the consumer override the content if they wanted to. I also wanted to provide specific defaults for zones that give the user a visual cue they have not bound to a "required" content zone.

First we defined default content for each of our zones in the generic.xaml file like this.

<DataTemplate x:Key="HeaderContent">
<StackPanel>
<TextBlock Text="Header" FontSize="15" FontWeight="Bold"/>
</StackPanel>
</DataTemplate>

<DataTemplate x:Key="MainContent">
<StackPanel>
<TextBlock Opacity="0.25" FontSize="18" Text="CONTENT ERROR! Bind content to the MainContentArea dependency property" TextWrapping="Wrap"/>
</StackPanel>
</DataTemplate>

<DataTemplate x:Key="RightContent">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Right Text"/>
<Rectangle Fill="AliceBlue" Width="25" Height="25">
<Rectangle.BitmapEffect>
<DropShadowBitmapEffect/>
</Rectangle.BitmapEffect>
</Rectangle>
<Ellipse Fill="YellowGreen" Width="25" Height="25"/>
</StackPanel>
</DataTemplate>

<DataTemplate x:Key="FooterContent">
<StackPanel>
<TextBlock Text="Copyright 2007. All rights reserved"/>
</StackPanel>
</DataTemplate>
Then we modified our control template to use Setters to set the dependency properties to the Static Resources in our generic.xaml.
<Style TargetType="{x:Type local:MasterPage}">
<Setter Property="HeaderContentArea" Value="{StaticResource HeaderContent}"/>
<Setter Property="MainContentArea" Value="{StaticResource MainContent}"/>
<Setter Property="RightContentArea" Value="{StaticResource RightContent}"/>
<Setter Property="FooterContentArea" Value="{StaticResource FooterContent}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MasterPage}">
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="225"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="50"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="25"></RowDefinition>
<RowDefinition Height="25"></RowDefinition>
</Grid.RowDefinitions>
<ContentControl Grid.Column="0" Grid.Row="0" x:Name="HeaderContentArea" ContentTemplate="{TemplateBinding HeaderContentArea}"/>
<ContentControl Grid.Column="0" Grid.Row="1" x:Name="MainContentArea" ContentTemplate="{TemplateBinding MainContentArea}"/>
<ContentControl Grid.Column="0" Grid.Row="2" x:Name="FooterContentArea" ContentTemplate="{TemplateBinding FooterContentArea}"/>
<ContentControl Grid.Column="1" Grid.RowSpan="3" x:Name="RightContentArea" ContentTemplate="{TemplateBinding RightContentArea}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Notice that we can now keep the content properties for each of our zones bound to the dependency properties we defined in our class. We have also set these properties with default values so if the consumer doesn't set them they will get the content we defined. This is nice because it allows us to move static content in to the custom control's generic.xaml and leaves less for the consumer to worry about.

Another nice feature is we are able to give the user a visual error if they do not bind to a zone that we feel they should bind to. In this case I have defined a main content area and defaulted the content to a textblock that shows an error:

<DataTemplate x:Key="MainContent">
<StackPanel>
<TextBlock Opacity="0.25" FontSize="18" Text="CONTENT ERROR! Bind content to the MainContentArea dependency property" TextWrapping="Wrap"/>
</StackPanel>
</DataTemplate>

Right away the consumer will see the message that they forgot to bind to the content area and can quickly fix the error.

Overall this is a pretty simple solution that gives you a nice way to share a layout across multiple pages while still allowing the individual pages to control the content. This also makes it easy to change the layout of your pages in one place. You just modify the layout of the "content zones" in the master page and all your consumer using the control will see the change.

You can find the sample code here

Labels:

1 Comments:

Blogger Patrick said...

Hi Brad,

I know this is an old article but is the sample code still somehow available?

April 29, 2014 at 7:07 AM  

Post a Comment

Subscribe to Post Comments [Atom]

<< Home