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:

Nov 19, 2007

Anonymous Delegates, how do I love thee? Let me count the ways...

This isn't anything new but I came across a situation recently where I was able to re-factor a seemingly trivial piece code to use an anonymous delegate and it reminded me how cool they are.

One great thing about generics is you can create a collection of strongly typed custom objects and iterate that collection to perform any number of operations. Often times you want to locate an item in the collection given a certain criteria.

I put together a simple little example to show the situation pre-generics,post generics without anonymous delegates, and post generics with anonymous delegates.

To set the scene let's assume we have a person class defined as follows

public class Person   

{
private int id;

private string firstName = string.Empty;
private string lastName = string.Empty;


public int ID
{
get { return id; }
set { id = value; }
}

public string FirstName
{
get { return firstName; }
set { firstName = value; }
}

public string LastName
{
get { return lastName; }
set { lastName = value; }
}
}


Prior to generics you might load an ArrayList of person objects like so:



static ArrayList LoadPersonList()
{
ArrayList list = new ArrayList();

Person Bob = new Person();
Bob.ID = 1;
Bob.FirstName = "Bob";
Bob.LastName = "Smith";

Person Sally = new Person();
Sally.ID = 2;
Sally.FirstName = "Sally";
Sally.LastName = "Jones";

list.Add(Bob);
list.Add(Sally);

return list;
}


So working from this example if we wanted to find the Person record with the first name Bob we could do something like this.



First write a find method that searches the ArrayList for the given first name. Something like this :



static Person FindPersonByFirstName(string firstName, ArrayList personList)
{
foreach (Person p in personList)
{
if (p.FirstName == firstName)
{
return p;
}
}
return null;
}


Then we call the find method and pass the FirstName "Bob":



static void Main(string[] args)
{
ArrayList personList = LoadPersonList();
Person bob = FindPersonByFirstName("Bob", personList);
}


Not so bad. With generics we get a little better.



First our load method now looks like this:



static List<Person> LoadPersonList()
{
List<Person> list = new List<Person>();

Person Bob = new Person();
Bob.ID = 1;
Bob.FirstName = "Bob";
Bob.LastName = "Smith";

Person Sally = new Person();
Sally.ID = 2;
Sally.FirstName = "Sally";
Sally.LastName = "Jones";

list.Add(Bob);
list.Add(Sally);

return list;
}


And our find method has been simplified to look like this



 



private static bool FindPersonByFirstName(Person p)
{
if (p.FirstName == "Bob")
return true;
else
return false;
}


And now we use the List<T> Find method like this to locate the item in the list that we want:



static void Main(string[] args)
{
List<Person> personList = LoadPersonList();
Person found = personList.Find(FindPersonByFirstName);
}


You notice that we are now hard-coding "Bob" in the implementation of the Find method. That obviously isn't ideal. We need a better solution for passing arguments to the find method while still leveraging the generic List's built in find features (namely iterating the collection for you)



This is where anonymous delegates come in to play. Using the same person list we loaded above, we can eliminate our find method all together and do this instead:



static void Main(string[] args)
{
List<Person> personList = LoadPersonList();
Person found = personList.Find( delegate (Person p) { return p.FirstName == "Bob";});
}


Neat huh?



To take it a little further if you want to be able to call a generic method passing the List and the value to search for (instead of writing the above for each time you need to find an item). You could do something like this :



static Person FindPersonByFirstName(string firstName, List<Person> list)
{
return list.Find(delegate(Person p) { return p.FirstName == firstName; });
}


With this implementation of the Find method the Main method from above would like this:



static void Main(string[] args)
{
List<Person> personList = LoadPersonList();
Person found = FindPersonByFirstName("Bob", personList);
}

This works for all of the methods attached List<T> and will simplify the mundane task implementing the guts of the various methods (FindAll,FindLast,Exists,etc..)