Introduction
This article describes a style that transforms a content control into a recursively splittable panel which can be used in WPF applications for enabling the end users to design custom layouts for the visual interface.
Background
Visual interfaces with fixed menus and layouts may seem limiting to most users. However, an interface with user-splittable panels can provide the freedom of designing custom layouts for the interface. The development process described below will show how this can be achieved with beginner-level knowledge of styles with triggers. It is also possible to enable users to save their customized layouts and load them later.
The initial idea was to develop a user control which could be split into two instances of its own type, separated by a GridSplitter
This was done by developing a WPF user control, because WPF would allow saving the control layout in an XAML file, but switching from unsplit state to split state was done entirely in code, and this just was not in line with the proper techniques of WPF. In addition, saving the visual layout produced a file cluttered with irrelevant details, like the resources associated with each instance of the user control.
The user control developed in the initial attempt did not have any appearance or behavior of its own, because it did not need any. All it was supposed to do was to be able to change its appearance to a split panel, when a splitting action was performed by the user. Of course, in its unsplit state, the control had be able to display some content, maybe via a ContentPresenter
. The control, itself, was nothing different from a ContentControl
, with the ability to display different contents, depending on the split state determined by the user.
The solution was to use a ContentControl
with separate content templates for different splitting state. The switching of templates was done via style triggers, which just checked the current value of an enumeration property called SplitState
, whose values were named Unsplit
, HorizontalSplit
, and VerticalSplit
. Saving the visual layout required nothing more than saving the current value of this enumeration.
The Splittable View Style
A ContentControl Style with Switchable Content Templates
In this updated article, the development process is explained in a tutorial style, taking from the point where a style was opted as the proper solution.
As noted earlier, an unsplit panel needs nothing more than a ContentPresenter
, which can display any type of content, and maybe a border around it:
<DataTemplate x:Key="UnsplitTemplate">
<Border>
<ContentPresenter/>
</Border>
</DataTemplate>
This template definition can be placed in the resources section of an application window's XAML file, or in a separate XAML file of a resource dictionary. The latter method is preferable, if a developer wants to reuse the template in other applications. This is what has been done in the code example of this article; the resource dictionary is in a separate class library project, which can be referenced in WPF application projects.
For a horizontally split panel, a grid with three rows will be needed:
<DataTemplate x:Key="HorizontalSplitTemplate">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="2" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentControl Grid.Row="0" Style="{DynamicResource RecursiveSplitViewStyle}" />
<GridSplitter Grid.Row="1" Height="Auto" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" ResizeDirection="Rows" />
<ContentControl Grid.Row="2" Style="{DynamicResource RecursiveSplitViewStyle}" />
</Grid>
</DataTemplate>
The template for a vertically split panel will have ColumnDefinitions
and its GridSplitter
's ResizeDirection
will say "Columns". Making a recursively splittable panel requires that a split panel's children should also be splittable panels.
The templates for the unsplit and split panels can be placed in a ContentControl
style with triggers to switch templates according to the current splitting state:
<Style x:Key="SplittablePanelStyle" TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=SplitState}" Value="Unsplit">
<Setter Property="ContentTemplate" Value="{StaticResource UnsplitTemplate}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=SplitState}" Value="HorizontalSplit">
<Setter Property="ContentTemplate" Value="{StaticResource HorizontalSplitTemplate}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=SplitState}" Value="VerticalSplit">
<Setter Property="ContentTemplate" Value="{StaticResource VerticalSplitTemplate}"/>
</DataTrigger>
</Style.Triggers>
</Style>
In summary, when applied to a ContentControl
object, this style enables that object change its contents according to an enumeration property named SplitState
. This style is already applied to split panel's children in the content template defined for the horizontally or vertically split panels. Of course, since the style had to refer to the split panel templates, the split panel templates referred to the style as a DynamicResource
. This circular referencing makes a ContentControl object
employing this style recursively splittable, because its children are also splittable.
One may now ask where this SplitState
property should be defined. If one derived a new user control class from the ContentControl
class, it could contain that property, but then saving the visual control was not considered the right way. A view model object defined as the data context of the ContentControl
is more in line with the MVVM methodology of WPF. Saving the visual layout means only saving the view model, which contain the properties that define the visual appearance, such as SplitState
property.
A View Model Class for the Splittable Panel
We can encapsulate the SplitState
property, or any other property that defines the visual appearance of a recursively splittable ContentControl
object in a view model class, which we will name RecursiveSplitViewModel
:
SplitState
, which specifies the current split state SplitPosition
, which specifies the width or the height of the first child panel InnerContent
, which specifies the object describing the visual content of the panel in its unsplit state, and Child1
and Child2
, which are the instances of the same class associated with the child views
These are the most basic properties that one can think of. An instance of this view model class will represent a binary tree of view model objects associated with a parallel binary tree of a recursively split panel. Saving the current values of the properties for the topmost member of this binary tree will have saved the final layout of the recursively split panels. Any application which needs a recursively splittable panel will simply place a ContentControl
object with this style, in the desired place, and create a RecursiveSplitViewModel
object as its DataContext
.
Splittable View Templates with Additional Properties
The InnerContent
property of the view model should be the binding source for the Content
property of the ContentPresenter
in the unsplit panel template:
<DataTemplate x:Key="UnsplitTemplate">
<Border>
<ContentPresenter Content="{Binding Path=InnerContent}"
ContentTemplateSelector=
"{x:Static local:RecursiveSplitViewModel.InnerContentTemplateSelector}"/>
</Border>
</DataTemplate>
The ContentPresenter
element is responsible for displaying the InnerContent
object, which can be visual controls, but then saving the view models' binary tree would again mean saving those visual controls, with all their cluttered structures. Going back to the idea of switchable templates, it will be easier to use simple objects as InnerContent
, and then let the client application decide what actual visual content should be displayed at runtime. This can be achieved by adding a property of the DataTemplateSelector
type to the view model class. However, it is hardly likely that each instance of the view model will need a separate template selector of its own. A static
property shared by all view model objects should answer the need. A client application making use of this style and the view model will specify a template selector object for that static
property, and take control of what is displayed for which type of content.
There is also the issue of the size of child panels in a split panel. The user can move the splitter to change the relative sizes of the child panels, and loading a saved layout should preserve the child panels' final sizes. The view model's SplitPosition
property takes care of that. We just need to bind the first row's or column's size to that property, but in a two-way mode. The author's preference was to specify the size in pixels, in a double type property, so a converter class was used to handle switching back and forth between double
and GridLength
types:
<RowDefinition Height="{Binding Path=SplitPosition, Mode=TwoWay,
Converter={StaticResource splitPositionConverter}}"/>
In short, SplitPosition
is the distance (in pixels) from the leftmost or the topmost edge of a split panel, depending on the splitting direction. It would be far more preferable to specify the splitter position as a fraction of the slit panel's size, but that proved far too difficult, because it required using the current parent size as a parameter or as another bound property in a multi-value converter.
Performing the Splitting Action Through Commands
As noted above, the earlier attempts involved splitting a user control in code, which were placed in event handlers for mouse clicks or menu item clicks. Before a style was used, the code performed a splitting operation by literally creating a grid with three rows and columns, with a grid splitter between two instances of the user control. Using a style with triggers made it possible to do the splitting by simply setting the view model object's SplitState
property:
private void OnHorizontalSplit(object sender, RoutedEventArgs e)
{
MenuItem clickedItem = sender as MenuItem;
ContentControl splittableView =
(clickedItem.Parent as ContextMenu).PlacementTarget as ContentControl;
if (splittableView == null)
{
return;
}
RecursiveSplitViewModel splittableViewModel =
splittableView.DataContext as RecursiveSplitViewModel;
if (splittableViewModel != null)
{
splittableViewModel.SplitState = RecursiveSplitViewState.HorizontalSplit;
}
}
if (splittableViewModel != null)
{
splittableViewModel.SplitState = RecursiveSplitViewState.HorizontalSplit;
}
if (splittableViewModel != null)
{
splittableViewModel.Parent.SplitState = RecursiveSplitViewState.Unsplit;
}
Such was the code for menu items' handlers, which was placed in a code file associated with the resource dictionary file containing the style. In a sense, the ContentControl
style was transformed into a class, and that was not much different from having a user control. What is more, the content control, which was a view object in this project, was telling its view model to change the splitting state, so that view model could cause the view to split.
A view object telling things to its view model was another point where this project was still not in line with proper techniques of WPF. In the code sample accompanying this updated article, the resource dictionary containing the style makes use of command bindings instead of event handlers:
<ContextMenu x:Key="splitViewContextMenu" x:Shared="True" x:Name="splitViewContextMenu">
<MenuItem Header="Horizontal Split" Command="{Binding Path=SplitCommand}"
CommandParameter="{x:Static hwpf:RecursiveSplitState.HorizontalSplit}"/>
<MenuItem Header="Vertical Split" Command="{Binding Path=SplitCommand}"
CommandParameter="{x:Static hwpf:RecursiveSplitState.VerticalSplit}"/>
</ContextMenu>
This is how a user can use the context menu to split a panel:
This context menu definition is in the same resource dictionary as the splitable view style, and they are bound to the same command definition in the view model class:
public ICommand SplitCommand
{
get
{
if (mySplitCommand == null)
{
mySplitCommand = new HurWpfCommand(ExecuteSplitCommand, IsEnabled);
}
return mySplitCommand;
}
}
which pointed to the following action method:
private void ExecuteSplitCommand(object newSplitState)
{
if (newSplitState is RecursiveSplitState)
{ SplitState = (RecursiveSplitState)newSplitState; }
}
As it is clear from the code snippets above, the view model object will do its own splitting by simply checking the new enumeration value sent as a parameter by the menu item to the SplitCommand
property's executable method. The command property is of a custom command class created by adopting the simplest examples found all over the net.
The Selection Command
The command implementation for performing the splitting achieves the ultimate purpose and makes a ContentControl
making use of the style a recursively splittable panel. However, it is possible that a developer -e.g., the author- will want to enable users to change panel contents, say, by drag drop operations. Without knowing what expectation a client application would have, a few simple commands like the one above would just not be sufficient to answer such advanced needs. Instead, the splittable view style has been designed to simply inform its view model that it has been selected, in case of a mouse button down or a drop event. Of course, it simply previews those events in its unsplit template, without actually handling them:
<DataTemplate x:Key="UnsplitTemplate">
<Border x:Name="viewBorder" Style="{StaticResource DefaultUnsplitBorderStyle}">
<ContentPresenter x:Name="innerContentPresenter" Content="{Binding Path=InnerContent}"
ContentTemplateSelector=
"{x:Static hwpf:RecursiveSplitViewModel.InnerContentTemplateSelector}">
</ContentPresenter>
<winInter:Interaction.Triggers>
<winInter:EventTrigger EventName="PreviewMouseDown">
<winInter:InvokeCommandAction Command="{Binding Path=SelectCommand, Mode=OneWay}"
CommandParameter="{StaticResource trueValue}"/>
</winInter:EventTrigger>
<winInter:EventTrigger EventName="PreviewDrop">
<winInter:InvokeCommandAction Command="{Binding Path=SelectCommand, Mode=OneWay}"
CommandParameter="{StaticResource trueValue}"/>
</winInter:EventTrigger>
</winInter:Interaction.Triggers>
</Border>
</DataTemplate>
In the above XAML snippet, wininter
is a reference to System.Windows.Interactivity
namespace. Since previews of user interaction events were not easy -well, not for the author, anyway- to be handled by ICommand
implementations, event triggers were used as an alternative.
The action method for the SelectCommand
property in the view model class simply sets the selection status to the specified parameter, or just toggles the selection status:
private void ExecuteSelectCommand(object selectMe)
{
if (!IsEnabled) { return; }
if (selectMe is bool)
{
IsSelected = (bool)selectMe;
}
else { IsSelected = !IsSelected; }
}
This is not the whole story, however. In order to inform a client application, the currently selected view model object is stored at a static
property of the RecursiveViewModel
class.
In summary, the view model for the recursively splitable panel leaves anything other than its splitting to the client applications which make us of the style. If a drag operation is started, it is up to the application to determine in which view model object the operation has been initiated. The application developer need only look up the currently selected view model object. Similarly, checking the currently selected view model after a drop operation will help determine which view model is the intended target of the dropped item.
The Code for the View Model Class
In addition to the command implementations described above, the view model class contains very little code; most of the work is done by bindings. Originally, the properties intended as binding sources were implemented as dependency properties. In the code sample of this updated article, the view model class implements the INotifyPropertyChanged
interface, instead of dependency properties. This preference has simplified the additional code that needed to be executed in case certain properties changed.
For example, when the splitting command's executable action method changes the SplitState
property, the ContentControl
making use of the style will switch to a split template containing two of its own instances with a grid splitter in between. However, the two instances in the split template will also need two view model instances as their data contexts. Therefore, the view model class must create two of its own instances as Child1
and Child2
after a splitting command is executed:
public RecursiveSplitState SplitState
{
get { return mySplitState; }
set
{
mySplitState = value;
myChild1 = new RecursiveSplitViewModel(this);
myChild2 = new RecursiveSplitViewModel(this);
OnPropertyChanged("SplitState");
}
}
Using the Code
In the code sample of this updated article, the resource dictionary containing the splittable view style and the associated view model class are put in a separate WPF class library project, named HurWpfLib
. This was done only to make them easier to redistribute and reuse. The class library also contains a few other classes like a double
-GridLength
converter class.
If the class library project is to be used as it is, a developer will only need to merge the resource dictionary containing the style:
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/HurWpfLib;
component/RecursiveSplitView.xaml"/>
</ResourceDictionary.MergedDictionaries>
Afterwards, a content control with the recursive split view style can be placed anywhere it is needed. With a data context of the RecursiveSplitViewModel
type, it will become a recursively splittable panel:
<ContentControl x:Name="MainSplitView" Style="{StaticResource RecursiveSplitViewStyle}">
<ContentControl.DataContext>
<hwpf:RecursiveSplitViewModel/>
</ContentControl.DataContext>
</ContentControl>
Once a ContentControl
with the RecursiveSplitViewStyle
is placed, the rest can be done by modifying the properties of the view model object that is defined as its data context. For example, the following XAML snippet will create a vertically-split split panel, whose right panel has been split horizontally, in a way to produce three panes in total:
<ContentControl x:Name="MainSplitView" Style="{StaticResource RecursiveSplitViewStyle}">
<ContentControl.DataContext>
<hwpf:RecursiveSplitViewModel SplitState="Vertical" SplitPosition="250">
<hwpf:RecursiveSplitViewModel.Child2>
<hwpf:RecursiveSplitViewModel SplitState="Horizontal" SplitPosition="150"/>
</hwpf:RecursiveSplitViewModel.Child2>
</hwpf:RecursiveSplitViewModel>
</ContentControl.DataContext>
</ContentControl>
Any specific UI elements can be declared within view model definitions as their inner contents. If one needed a treeview
, a textbox
and a listbox
in the three panes of the above snippet, one would detail the declaration as follows:
<ContentControl x:Name="MainSplitView" Style="{StaticResource RecursiveSplitViewStyle}">
<ContentControl.DataContext>
<hwpf:RecursiveSplitViewModel SplitState="Vertical" SplitPosition="250">
<hwpf:RecursiveSplitViewModel.Child1>
<hwpf:RecursiveSplitViewModel>
<hwpf:RecursiveSplitViewModel.InnerContent>
<TreeView/>
</hwpf:RecursiveSplitViewModel.InnerContent>
</hwpf:RecursiveSplitViewModel>
</hwpf:RecursiveSplitViewModel.Child1>
<hwpf:RecursiveSplitViewModel.Child2>
<hwpf:RecursiveSplitViewModel SplitState="Horizontal" SplitPosition="150">
<hwpf:RecursiveSplitViewModel.Child1>
<hwpf:RecursiveSplitViewModel>
<hwpf:RecursiveSplitViewModel.InnerContent>
<TextBox/>
</hwpf:RecursiveSplitViewModel.InnerContent>
</hwpf:RecursiveSplitViewModel>
</hwpf:RecursiveSplitViewModel.Child1>
<hwpf:RecursiveSplitViewModel.Child2>
<hwpf:RecursiveSplitViewModel>
<hwpf:RecursiveSplitViewModel.InnerContent>
<ListBox/>
</hwpf:RecursiveSplitViewModel.InnerContent>
</hwpf:RecursiveSplitViewModel>
</hwpf:RecursiveSplitViewModel.Child2>
</hwpf:RecursiveSplitViewModel>
</hwpf:RecursiveSplitViewModel.Child2>
</hwpf:RecursiveSplitViewModel>
</ContentControl.DataContext>
</ContentControl>
It will be a good idea to set the IsEnabled
property to False
for view model definitions with pre-defined inner contents. If that is done, they will not accept new inner contents and they will not allow splitting by the user. One can also define view model objects of individual panes as resources and shorten an XAML declaration like the one above, but then it may be hard to establish bindings between inner content elements.
A Sample Application With a Template Selector
The code sample accompanying this updated article contains a sample database viewing application, which allows the user to display any data table or column in any of the panes created by splitting panels that appear as the pages of a tab control. A user will simply drag an item from the treeview
representing the database schema onto a pane of a split panel and a datatable
will be displayed there.
<ContentControl x:Name="MainSplitView" Style="{StaticResource RecursiveSplitViewStyle}">
<ContentControl.DataContext>
<hwpf:RecursiveSplitViewModel x:Name="MainSplitViewModel"
SplitState="VerticalSplit" SplitPosition="250">
<hwpf:RecursiveSplitViewModel.Child1>
<hwpf:RecursiveSplitViewModel x:Name="DbTreeViewPanel" IsEnabled="False">
<hwpf:RecursiveSplitViewModel.InnerContent>
<localdbs:DbSource DatabaseType="AccessMdb"
DatabaseName="D:\Projects\Nwind.mdb"/>
</hwpf:RecursiveSplitViewModel.InnerContent>
</hwpf:RecursiveSplitViewModel>
</hwpf:RecursiveSplitViewModel.Child1>
<hwpf:RecursiveSplitViewModel.Child2>
<hwpf:RecursiveSplitViewModel x:Name="DbSplitViewsPanel" IsEnabled="False">
<hwpf:RecursiveSplitViewModel.InnerContent>
<TabControl x:Name="SplitViewsTab"
ItemTemplate="{StaticResource SplitViewsTab_PageHeaderTemplate}"
ContentTemplate="{StaticResource SplitViewsTab_PageContentTemplate}">
<TabControl.ItemsSource>
<hwpf:RecursiveSplitViewModelCollection x:Name="SplitViewModelCollection">
<hwpf:RecursiveSplitViewModel/>
</hwpf:RecursiveSplitViewModelCollection>
</TabControl.ItemsSource>
</TabControl>
</hwpf:RecursiveSplitViewModel.InnerContent>
</hwpf:RecursiveSplitViewModel>
</hwpf:RecursiveSplitViewModel.Child2>
</hwpf:RecursiveSplitViewModel>
</ContentControl.DataContext>
</ContentControl>
The above code snippet creates a vertically split panel and places a DbSource
object as the inner content of the left panel. DbSource
class is a custom class which can scan the schema of an Access database (or a SQL Server database, if the database type is defined so) and create DbTableSource
and DbColumnSource
objects corresponding to the data tables and columns in the database. The explanations about these custom classes are beyond the scope of this article and may appear in a separate article about a flexible database display application.
Developers who want to test the sample application will need to define the server name (if a SQL server database is to be used), the database name, and the user name and the password (if needed) in the DbSource
definition.
The important thing here, is that, the application specifies a template selector for the RecursiveSplitViewModel
class and that template selector selects a template containing a TreeView
object for the DbSource
object defined as the inner content of the left panel. As a result, the user will see a tree view display of the database schema on the left panel.
The right panel, on the other hand, contains a pre-defined UI element, which is a tab control whose item source is a collection of RecursiveViewModel
objects. The collection initially contains a single view model object. Because the tab control defines a content control with the splittable view style as its content template, the user sees a splittable panel as the only tab page. That tab page panel can be recursively split.
The tab control's header template contains a button which helps the user to add, insert or delete tab pages by pressing certain keys while clicking the button. This is a very crude solution that demonstrates a way to help users to customize the layout of a database display application. The important thing here, is that, the application lets the user save the collection in an XAML file and then load it later to recover the final layouts of all the splittable tab pages.
Updates
- 3rd January, 2013: Original publication
- 26th January, 2013: First update
- The user control which represented a nested collection of splittable views, and the user control library containing it have been eliminated.
- Splittable view style's content templates have been simplified.
- View model class properties related to splitter have been removed for simplicity.
IsSplitterEnabled
property is the only one that remains. - The editor bar with buttons for splitting the views and setting splitter properties has been abandoned in favor of a much simpler context menu.
- The object names and terms have been modified in the article and the sample code for consistency.
- 26th February, 2013: Second update
- The code-behind file of the resource dictionary containing the splittable view style has been eliminated.
- The event handlers appearing in the code behind file have been replaced with command bindings.
- A more complex, but meaningful sample application has been provided, though with almost no explanations on what it does.
- Proper tributes have been paid by adding references wherever other developers' solutions were adopted. Hopefully, sharing of this work will make good of all remaining unpaid debts.
Feedback
The author will appreciate all positive or negative comments, and desires to hear about how it is used or modified, and what problems have been encountered in their use.