Click here to Skip to main content
Click here to Skip to main content

A Recursively Splittable WPF Content Control Style for User Customizable Interfaces

, 5 Mar 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
A recursively splittable user control for designing a custom interface, which can be serialized for sharing.

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 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 the a style was opted as the proper solution.

As noted earlier, an unsplit panel need 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 row 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 enumaration 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 switcheable 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)
     {
         // Do something to gracefully declare error.
         return;
     }

     RecursiveSplitViewModel splittableViewModel = splittableView.DataContext as RecursiveSplitViewModel;

     if (splittableViewModel != null)
     {
         splittableViewModel.SplitState = RecursiveSplitViewState.HorizontalSplit;

     }
}

	// .. For vertical split ...
     if (splittableViewModel != null)
     {
         splittableViewModel.SplitState = RecursiveSplitViewState.HorizontalSplit;
     }

	// .. For unsplitting the parent view ...
     if (splittableViewModel != null)
     {
         splittableViewModel.Parent.SplitState = RecursiveSplitViewState.Unsplit;
     }

Such were the code for menu items' handlers, which were 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)
{
   // No selection is allowed on disabled viewmodel objects.
   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 propeties 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;
         // A split viewmodel must have two children.
      myChild1 = new RecursiveSplitViewModel(this);
      myChild2 = new RecursiveSplitViewModel(this);
         // Notify any bound object.
      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 RecusriveSplitViewModel 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 tree view, 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 an 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. 

Developer 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  

  • January 3, 2013: Original publication.
  • January 26, 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.
  • February 26, 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. 

License

The source code and files are licensed under The Code Project Open License (CPOL). 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Dr. Hurol Aslan

United States United States
A Physics PhD with an engineering background who also earned an MBA. I am a long-time programmer who was confined to coding for academic purposes. Since 2004, I have been extending my coding skills into wider areas, like desktop interface development with data binding, automation of office applications, and development of physics simulations targeting social phenomena.

Comments and Discussions

 
QuestionNew version! +5 PinmemberMember 45586629-Jan-13 9:54 
AnswerRe: New version! +5 PinmemberDr. Hurol Aslan29-Jan-13 11:22 
QuestionExcellent! +5 PinmemberMember 4558663-Jan-13 8:07 
AnswerRe: Excellent! +5 PinmemberDr. Hurol Aslan3-Jan-13 21:24 
GeneralRe: Excellent! +5 PinmemberMember 4558663-Mar-13 4:43 
GeneralRe: Excellent! +5 PinmemberDr. Hurol Aslan3-Mar-13 6:24 
GeneralRe: Excellent! +5 PinmemberDr. Hurol Aslan3-Mar-13 6:43 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.141223.1 | Last Updated 5 Mar 2013
Article Copyright 2013 by Dr. Hurol Aslan
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid