Introduction
Every once in a while, when reading a programming book (in this case "WPF 4 unleashed"), I see an example that I would like to expand on. Doing so usually ensures
I understand the concepts been discussed and gives me a fun project to work on. The
Visual Studio like layout example in Chapter 5 of Adam Nathan's book is the example of choice
I have decided to expand on. In the book, Adam puts up an example of collapsible, dock-able, and resizable
Visual Studio like panes. This was done to show how WPF
layout features are used to create complex user interfaces. In his example, Adam dealt with
two hard coded panes and mention that one would most likely abstract the code into
a custom control for more general use. That is exactly what I have done, and will try to describe in this article.
The code discussed below will build on the concept used to create the two panes, and create a custom control that allows for a generic manner of adding panes to either columns
(left or right) or rows. These panes will be collapsible, dock-able, and resizable.

The above image shows you a complete example of a dock-able window (the code is attached with this article).
Background
While I will do my best to explain the basic concepts used in creating this layout custom control, I would suggest reading the chapter and the original
code example (I would rather not plagiarize, or repeat information that the author explains in better terms). Also note, that although I have written
functional code that does exactly what I describe in this article, there are many more improvements that can be done to make my example a truly usable/ customize-able
custom control. At the end of article I will mention some of the things that would probably need to be changed/ refactored.
To start off this article, I will describe the task I am trying to solve and then how I intend to solve it.
Task
I want a Visual Studio like frame where the main content fills the window, I want to have the ability to have dock-able content to either the left side,
right side, or at the bottom of the main content. I want to be able to resize the dock-able content, when pinned relative to the main content and when unpinned
regardless of the main content. I want to be able to choose which content to pin and when a content is pinned
to get rid of the button that is activate
in the dock-able content. When a dock-able content is hovering freely, I want to be able to collapse it by moving over the main window.
Lastly I want to able to lay out my dock-able content declaratively in XAML.
Solution
First and foremost choosing a container that allows the user the ability to interactively re-size on the go means using a Grid (with GridSplitters).
However since the dock-able content also needs to the ability to be re-sized
regardless of the main content (allowing for overlaps), I will need more than one Grid.
The solution used here, will be for every dock-able content, a Grid will be used in its construction. The Grids will then be layered on top of one
another with the main content being layer 0. To allow the windows to be synchronized when docked, we will use the
Grid's SharedSizeGroup property.
When a layer is docked, we will programmatically add a new shared column or row definition to the layers below it and add to the layer itself
any shared column or row definition needed for already docked layers that are above it in hierarchy. Since we are using the SharedSizeGroup feature
of the Grid we are able to keep the proportions in terms of width (when it comes to columns) and height (when it comes to rows) in sync with all layers.
To mimic the collapsing of the dock-able content, all we have to do is set the Visibility of the Grid to Collapsed.
We will use StackPanels as the button bars for the columns and rows. When all the dock-able content
is docked, the StackPanels collapse.

The image above shows an example of one of the dock-able content layers overlapping the main content below it.
Using the Code
Prior to getting into the details of how we achieve the functionality as shown in the image above, I believe this is an appropriate time to walk
you through the process of creating the custom control project.
- Create a new solution (I have named my solution DockableVSExample).
- Click on the solution in Solution Explorer and add a new folder named Common.
- Add a new WPF Custom Control Library named Controls in the Common folder.
- In the Themes folder add a new resource named LayeredGrid.xaml.
- Rename the controls.cs file auto generated when the Controls project was created to
LayeredGrid.cs.
- Then add a new Layer.cs class file to the Controls project.
- In the Generic .xaml (note the build action on this should be set to Page), add the below entry inside the
ResourceDictionary.MergedDictionaries tag.
<ResourceDictionary Source="/Controls;component/Themes/LayeredGrid.xaml"/>
Once the above steps are done, we can now work on the LayeredGrid.xaml file (as it needs to be setup prior to working on the LayeredGrid class).
Open up the LayeredGrid.xaml for editing and in the ResourceDictionary tag, add the
Controls namespace within the tag
xmlns:local="clr-namespace:Controls;assembly=Controls". Then add the below
XAML to the LayeredGrid.xaml file.
<Style TargetType="{x:Type local:LayeredGrid}">
<Setter
Property="Template">
<Setter.Value>
<ControlTemplate>
<DockPanel
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
LastChildFill="True"
Name="PART_ParentPanel">
<DockPanel.BitmapEffect>
<BevelBitmapEffect
BevelWidth="15"
EdgeProfile="BulgedUp"/>
</DockPanel.BitmapEffect>
<DockPanel.Resources>
<Color
x:Key="PrimaryColor">
CadetBlue
</Color>
<Color
x:Key="SecondaryColor">
#CC0D1000
</Color>
<SolidColorBrush
x:Key="PrimaryBrush"
Color="{StaticResource PrimaryColor}" />
<SolidColorBrush
x:Key="TextBrush"
Color="Black" />
<SolidColorBrush
x:Key="DisabledColor"
Color="#8CFFFFFF" />
<SolidColorBrush
x:Key="BackgroundBrush"
Color="#FFFFFFFF" />
<Style
x:Key="buttonStyle"
TargetType="{x:Type Button}">
..
..
..
</Style>
<RadialGradientBrush
x:Key="myColorfulLabelBrush"
RadiusX="0.5"
RadiusY="1"
>
<GradientStop
Color="#CC0D1000"
Offset="0.1"/>
<GradientStop
Color="CadetBlue"
Offset="0.9"/>
</RadialGradientBrush>
<RadialGradientBrush
x:Key="myColorfulBorderBrush"
RadiusX="0.4"
RadiusY="0.6"
>
<GradientStop Color="#CC3D2614" Offset="0.3"/>
<GradientStop Color="Gold" Offset="0.8"/>
</RadialGradientBrush>
</DockPanel.Resources>
<StackPanel
Name="PART_BottomCntl"
Background="{StaticResource myColorfulLabelBrush}"
Orientation="Horizontal"
Panel.ZIndex="1"
DockPanel.Dock="Bottom">
</StackPanel>
<StackPanel
Name="PART_LeftCntl"
Background="{StaticResource myColorfulLabelBrush}"
Orientation="Horizontal"
DockPanel.Dock="Left">
<StackPanel.LayoutTransform>
<RotateTransform Angle="90"/>
</StackPanel.LayoutTransform>
</StackPanel>
<StackPanel
Name="PART_RightCntl"
Background="{StaticResource myColorfulLabelBrush}"
Orientation="Horizontal"
DockPanel.Dock="Right">
<StackPanel.LayoutTransform>
<RotateTransform Angle="90"/>
</StackPanel.LayoutTransform>
</StackPanel>
<Grid
Name="PART_MasterGrid"
Grid.IsSharedSizeScope="True">
</Grid>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Within the XAML content, there are a few things worth mentioning. First is the
DockPanel (named PART_ParentPanel), which serves
the purpose of allowing me to lay out three StackPanels (left, right, and bottom Button bar) and
a Grid (serving as a content placeholder). The three StackPanels (named
PART_LeftCntl, PART_RightCntl,
and PART_ButtomCntl) allow for the placement of the buttons used to activate dock-able content layers.
The Grid (named PART_MasterGrid)
represent the panel container where all layers will be contained. PART_MasterGrid has the attached property IsSharedSizeScope set to true, this allows any child Grid to share size information.
The first class that will be discussed is, the Layer class. The purpose of the Layer class is to define
the properties that each layer of dock-able content needs to describe its location (Left or Right),
its orientation (Row or Column), its level (a number which tells me in which order to place the layers,
the name placed on the dock-able panel, and the content it hosts). This is the class that I would put any property that I want a
Layer
as a whole to inherit. The class implementation is shown below and is easy to follow, as such will not be explained further.
public class Layer : UIElement
{
public enum LayerOrientation
{
Row,
Column
}
public enum LayerColumnLocation
{
Left,
Right
}
public static readonly DependencyProperty LevelProperty;
public static readonly DependencyProperty ContentProperty;
public static readonly DependencyProperty OrientationProperty;
public static readonly DependencyProperty NameProperty;
public static readonly DependencyProperty ColumnLocationProperty;
public int Level
{
get { return (int)GetValue(LevelProperty); }
set { SetValue(LevelProperty, value); }
}
public UIElement Content
{
get { return (UIElement)GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
public LayerOrientation Orientation
{
get { return (LayerOrientation)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
public LayerColumnLocation ColumnLocation
{
get { return (LayerColumnLocation)GetValue(ColumnLocationProperty); }
set { SetValue(ColumnLocationProperty, value); }
}
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
static Layer()
{
LevelProperty = DependencyProperty.Register(
"Level",
typeof(int),
typeof(Layer)
);
ContentProperty = DependencyProperty.Register(
"Content",
typeof(UIElement),
typeof(Layer)
);
OrientationProperty = DependencyProperty.Register(
"Orientation",
typeof(LayerOrientation),
typeof(Layer));
NameProperty = DependencyProperty.Register(
"Name",
typeof(string),
typeof(Layer));
ColumnLocationProperty = DependencyProperty.Register(
"ColumnLocation",
typeof(LayerColumnLocation),
typeof(Layer),
new PropertyMetadata
(
LayerColumnLocation.Left
)
);
}
}
The LayeredGrid class (which inherits from the ContentControl class) serves as the class backing the custom control.
The LayeredGrid class has only one DependencyProperty which is named LayersProperty. This dependency property is of type
ObservableCollection< Layer>, and its purpose is to allow us to add one or more
Layer objects to the LayeredGrid. As this is the class that does all the heavy lifting, I will try and explain its implementation in detail.
The below code is just the fields that are used within the next set of methods I will be discussing.
#region fields
private Grid PART_MasterGrid;
private StackPanel PART_RightCntl;
private StackPanel PART_LeftCntl;
private StackPanel PART_BottomCntl;
private DockPanel PART_ParentPanel;
private readonly ObservableCollection<Layer> _aValues = new ObservableCollection<Layer>();
private readonly List<GridnFloatingBtnCombo> _columnLayers = new List<GridnFloatingBtnCombo>();
private readonly List<GridnFloatingBtnCombo> _rowLayers = new List<GridnFloatingBtnCombo>();
private const string ColumnStr = "column";
private const string RowStr = "row";
private const string LayerStr = "Layer";
private const string PinStr = "btn";
#endregion
The readonly _columnLayers and _rowLayers lists declared above are of type GridnFloatingBtnCombo.
GridnFloatBtnCombo is the class used to hold each individual Layer's Grid and the Button that is associated
with its activation (the button placed in the button bar StackPanel). The GridnFloatingBtnCombo defines a list of Grid.ColumnDefinitions
which holds a ColumnDefinition for all column oriented Layers that have a Level greater than that of the Layer
that ties to the class. The same explanation goes for the list of Grid.RowDefinitions that is defined in the class.
The defined List of ColumnLocations in the class, is synchronized with the list of ColumnDefintions and serves the purpose
of associating each ColumnDefinition with a location on the Grid. That is if the ColumnLocation is defined
as Right we just add the ColumnDefintion to the Grid.Children collection, else if it is Left
we insert the ColumnDefintion into the Grid at index 0. Because of the possibility of inserting ColumnDefinitions at index 0,
we need the ability to maintain the Grid column index of the main content, hence the definition of the MainContentLocation property.
The GridnFloatingBtnCombo then has two methods to increment the MainContentLocation (MainContentPositionIncrement()) and decrement the
MainContentLocation (MainContentPositionDecrement()). The implementation of the
GridnFloatingBtnCombo is shown below.
#region layer grid, button btn, columns and rows definition holder class
private class GridnFloatingBtnCombo
{
public readonly Grid Grid;
public readonly Button Btn;
public readonly List<ColumnDefinition> ColumnDefinitions;
public readonly List<Layer.LayerColumnLocation> ColumnLocations;
public readonly List<RowDefinition> RowDefinitions;
public int MainContentLocation { get; private set; }
public GridnFloatingBtnCombo(Grid grid, Button btn)
{
Grid = grid;
Btn = btn;
ColumnDefinitions = new List<ColumnDefinition>();
RowDefinitions = new List<RowDefinition>();
ColumnLocations = new List<Layer.LayerColumnLocation>();
MainContentLocation = 1;
}
public void MainContentPositionIncrement()
{
MainContentLocation++;
}
public void MainContentPositionDecrement()
{
if (MainContentLocation > 1)
MainContentLocation--;
}
}
#endregion
As mentioned above the LayeredGrid class has only one Dependency Property named LayersProperty. As this is a collection that needs to be initialized
internally in code (an exception occurs otherwise), I register the dependency property as a
readonly property and then in the non-static constructor initialize the collection.
#region properties and DPs
private static readonly DependencyPropertyKey LayersPropertyKey =
DependencyProperty.RegisterReadOnly("Layers",
typeof (
ObservableCollection
<Layer>),
typeof (LayeredGrid),
new PropertyMetadata(null));
public static readonly DependencyProperty LayersProperty = LayersPropertyKey.DependencyProperty;
public ObservableCollection<Layer> Layers
{
get { return (ObservableCollection<Layer>)GetValue(LayersProperty); }
set { SetValue(LayersProperty, value); }
}
#endregion
#region constructor
static LayeredGrid()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(LayeredGrid),
new FrameworkPropertyMetadata(typeof(LayeredGrid)));
}
public LayeredGrid()
{
SetValue(LayersPropertyKey, _aValues);
}
#endregion
The overridden OnApplyTemplate method is where the layers are built up and put together. This method gets all
PART_* controls mentioned above,
sets up a parent Grid, sets up the first layer (Layer 0) that it adds to the parent
Grid, after which all column layers are then setup (these also get added
to the parent grid), followed by all row layers being then setup, and finally the parent
Grid is then added to the PART_MasterGrid.
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
PART_MasterGrid = GetTemplateChild("PART_MasterGrid") as Grid;
PART_RightCntl = GetTemplateChild("PART_RightCntl") as StackPanel;
PART_LeftCntl = GetTemplateChild("PART_LeftCntl") as StackPanel;
PART_BottomCntl = GetTemplateChild("PART_BottomCntl") as StackPanel;
PART_ParentPanel = GetTemplateChild("PART_ParentPanel") as DockPanel;
if (PART_MasterGrid == null)
return;
var parentGrid = new Grid();
SetUpParentGrid(parentGrid);
var layer0 = Layers.FirstOrDefault(x => x.Level == 0);
if (layer0 == null)
return;
var columnLayers =
Layers.Select(x => x)
.Where(x =>
x.Level > 0 &&
x.Orientation == Layer.LayerOrientation.Column)
.OrderBy(x => x.Level);
var rowLayers =
Layers.Select(x => x)
.Where(x=>
x.Level>0 &&
x.Orientation==Layer.LayerOrientation.Row)
.OrderBy(x=> x.Level);
var item = SetupLayer0(layer0,
columnLayers,
rowLayers.Count());
parentGrid.Children.Add(item);
Grid.SetRow(item, 0);
if (columnLayers.Any())
{
foreach (var layer in columnLayers)
{
SetupColumnLayers(parentGrid, layer, columnLayers.Count());
}
}
if(rowLayers.Any())
{
foreach (var layer in rowLayers)
{
SetupRowLayers(parentGrid, layer, rowLayers.Count());
}
}
PART_MasterGrid.Children.Add(parentGrid);
Grid.SetRow(parentGrid,0);
}
The SetupParentGrid method adds two RowDefinitions to the parent
Grid. The first RowDefinition of Height
star
is meant to house the main content.
private static void SetUpParentGrid(Grid parent)
{
var row1 = new RowDefinition
{
Height = new GridLength(1, GridUnitType.Star)
};
var row2 = new RowDefinition { Height = GridLength.Auto };
parent.RowDefinitions.Add(row1);
parent.RowDefinitions.Add(row2);
}
The SetupLayer0 method adds three Grid ColumnsDefinitions. The first column represents the placement location
of all left dock-able content, the second column represents the location of the main content, and finally the third column represents the placement location
of all right dock-able content. An event handler is attached to the Grid.MouseEnter event to collapse any Layer's Grid
if its activate Layer Button is visible (meaning the Layer is not docked). Finally for every row and column Layer
a RowDefintion and ColumnDefinition is added to an instance of GridnFloatingBtnCombo for Layer 0.
Point to note, is how I set the SharedSizeGroup property of the Column/Row
definitions.
private Grid SetupLayer0(Layer layer0, IEnumerable<Layer> columnLayers, int numberofRows)
{
var grid = new Grid { Name = ColumnStr + LayerStr + layer0.Level };
grid.ColumnDefinitions.Add(new ColumnDefinition
{
Width = GridLength.Auto
});
grid.ColumnDefinitions.Add(new ColumnDefinition
{
Width = new GridLength(1, GridUnitType.Star)
});
grid.ColumnDefinitions.Add(new ColumnDefinition
{
Width = GridLength.Auto
});
grid.RowDefinitions.Add(new RowDefinition());
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
if (layer0.Content != null)
grid.Children.Add(layer0.Content);
if (layer0.Content != null)
Grid.SetColumn(layer0.Content,1);
grid.MouseEnter += (o, e) =>
{
for (var i = 1; i < _columnLayers.Count; i++)
{
if (_columnLayers[i].Btn.Visibility == Visibility.Visible)
{
_columnLayers[i].Grid.Visibility = Visibility.Collapsed;
}
}
for (var i = 1; i < _rowLayers.Count; i++)
{
if (_rowLayers[i].Btn.Visibility == Visibility.Visible)
{
_rowLayers[i].Grid.Visibility = Visibility.Collapsed;
}
}
};
var gnb = new GridnFloatingBtnCombo(grid, null);
if (columnLayers.Any())
{
var list = columnLayers.ToList();
for (int i = 0; i < columnLayers.Count(); i++)
{
gnb.ColumnDefinitions.Add(new ColumnDefinition
{
SharedSizeGroup = ColumnStr +
(i + 1) +
list[i].ColumnLocation,
Width = GridLength.Auto
});
gnb.ColumnLocations.Add(list[i].ColumnLocation);
}
}
if (numberofRows > 0)
{
for (int i = 0; i < numberofRows; i++)
{
gnb.RowDefinitions.Add(new RowDefinition
{
SharedSizeGroup = RowStr + (i + 1),
Height = GridLength.Auto
});
}
}
_columnLayers.Add(gnb);
_rowLayers.Add(gnb);
return grid;
}
The SetupColumnLayers method deals with creating all column layers irrespective of the ColumnLocation.
To build up a column Layer, we create a Grid with
three ColumnDefinitions just as with Layer 0,
however in this case the dock-able content will be place in column 0 for Layers that have their ColumnLocation as
Left
and column 2 for Layers that have their ColumnLocation as
Right. This is evident in how the ColumnDefinition's
SharedSizedGroup property is setup. An internal Grid is created with
two RowDefinitions, the first row
houses a DockPanel that in turn contains a docking pin button (docked to the right), and a TextBlock
that holds the Layer's Name (docked to the left). A GridSplitter is added to the Layer's Grid
along side the internal Grid to allow for resizing the content. The docking pin Button has a Click event handle that deals
with the docking and undocking of the Layer. Finally for any Layer with a higher
Level than the current Layer
we create a ColumnDefinition (with its SharedGroupSize property set accordingly).
private void SetupColumnLayers(Grid parentGrid, Layer layer, int columnLayerCnt)
{
var grid = new Grid
{
Name = ColumnStr + LayerStr + layer.Level,
Visibility = Visibility.Collapsed
};
grid.ColumnDefinitions.Add(new ColumnDefinition
{
SharedSizeGroup =
layer.ColumnLocation == Layer.LayerColumnLocation.Left
? ColumnStr + layer.Level+layer.ColumnLocation
: null,
Width = GridLength.Auto
});
grid.ColumnDefinitions.Add(new ColumnDefinition
{
Width = new GridLength(1, GridUnitType.Star)
});
grid.ColumnDefinitions.Add(new ColumnDefinition
{
SharedSizeGroup =
layer.ColumnLocation == Layer.LayerColumnLocation.Right
? ColumnStr + layer.Level + layer.ColumnLocation
: null,
Width = GridLength.Auto
});
var internalGrid = new Grid();
internalGrid.RowDefinitions.Add(new RowDefinition
{
Height = GridLength.Auto
});
internalGrid.RowDefinitions.Add(new RowDefinition ());
internalGrid.Background = (RadialGradientBrush) PART_MasterGrid.FindResource( "myColorfulLabelBrush" );
grid.Children.Add(internalGrid);
Grid.SetColumn(internalGrid,
layer.ColumnLocation == Layer.LayerColumnLocation.Left ? 0 : 2
);
var dockpanel = new DockPanel();
internalGrid.Children.Add(dockpanel);
Grid.SetRow(dockpanel, 0);
var btn = new Button
{
Name = ColumnStr + PinStr + layer.Level,
Width = 28.0,
Background = Brushes.Transparent,
BorderBrush = Brushes.Transparent,
Style = (Style) PART_MasterGrid.FindResource( "buttonStyle" ),
Content = new Path
{
Stroke = Brushes.Black,
Fill=Brushes.Gold,
StrokeThickness = 1,
Stretch = Stretch.Fill,
Width = 9.0,
Height = 15,
Data = PinPathgeometry()
}
};
dockpanel.Children.Add(btn);
DockPanel.SetDock(btn, Dock.Right);
btn.Click += (o, e) =>
{
int level = layer.Level;
var item = _columnLayers[level].Btn;
if (item.Visibility == Visibility.Collapsed)
ColumnUndockPane(level, o as Button);
else
ColumnDockPane(level, o as Button);
};
var textblock = new TextBlock
{
Padding = new Thickness(8),
TextTrimming = TextTrimming.CharacterEllipsis,
Foreground = Brushes.Gold,
Text = layer.Name
};
dockpanel.Children.Add(textblock);
DockPanel.SetDock(textblock,Dock.Left);
if (layer.Content != null)
{
internalGrid.Children.Add(layer.Content);
Grid.SetRow(layer.Content, 1);
}
var gridSplitter = new GridSplitter
{
Width = 2,
Background = Brushes.CadetBlue,
HorizontalAlignment =
layer.ColumnLocation == Layer.LayerColumnLocation.Right
? HorizontalAlignment.Left
: HorizontalAlignment.Right
};
grid.Children.Add(gridSplitter);
Grid.SetColumn(gridSplitter,
layer.ColumnLocation == Layer.LayerColumnLocation.Right ? 2 : 0
);
grid.MouseEnter += (o, e) =>
{
var level = layer.Level;
for (var i = (level + 1); i < _columnLayers.Count; i++)
{
if (_columnLayers[i].Btn.Visibility == Visibility.Visible)
_columnLayers[i].Grid.Visibility = Visibility.Collapsed;
}
};
parentGrid.Children.Add(grid);
Grid.SetRow(grid, 0);
var gnb = new GridnFloatingBtnCombo( grid,
AddToColumnStackPanel(layer)
);
if (columnLayerCnt > 0)
{
for (int i = layer.Level; i < columnLayerCnt; i++)
{
gnb.ColumnDefinitions
.Add(new ColumnDefinition
{
SharedSizeGroup = ColumnStr + (i + 1) + layer.ColumnLocation,
Width = GridLength.Auto
});
gnb.ColumnLocations.Add(layer.ColumnLocation);
}
}
_columnLayers.Add(gnb);
}
The SetupRowLayers method, just like its column Layer setup counterpart, creates a Layer Grid.
In this case however, two RowDefinitions are created, with the second row serving the purpose of housing the dock-able content. Also unlike the column Layer
setup, the row Layer setup uses a DockPanel to host both the docking pin Button and the Layer's Content.
Finally rather than adding the Layer's Grid to the parent Grid, we add this directly to the PART_MasterGrid
(this allows the dockable row span across the dock-able columns when docked).
private void SetupRowLayers(Grid parentGrid, Layer layer, int numberofRows)
{
var grid = new Grid
{
Name = RowStr + LayerStr + layer.Level,
Visibility = Visibility.Collapsed
};
grid.RowDefinitions.Add(new RowDefinition
{
Height = new GridLength(1,GridUnitType.Star)
});
grid.RowDefinitions.Add(new RowDefinition
{
SharedSizeGroup = RowStr+layer.Level,Height = GridLength.Auto
});
var dockpanel = new DockPanel
{
Margin = new Thickness(0, 4, 0, 0),
Background =
(RadialGradientBrush) PART_MasterGrid.FindResource("myColorfulLabelBrush"),
LastChildFill = true
};
grid.Children.Add(dockpanel);
Grid.SetRow(dockpanel, 1);
var gridsplitter = new GridSplitter
{
Height = 4,
Background = Brushes.CadetBlue,
ResizeDirection = GridResizeDirection.Rows,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Top
};
grid.Children.Add(gridsplitter);
Grid.SetRow(gridsplitter, 1);
var stackpanel = new StackPanel
{
Height = 25.0,
HorizontalAlignment = HorizontalAlignment.Stretch
};
dockpanel.Children.Add(stackpanel);
DockPanel.SetDock(stackpanel,Dock.Top);
var btn = new Button
{
Name = RowStr + PinStr + layer.Level,
Width = 26.0,
HorizontalAlignment = HorizontalAlignment.Right,
Background = Brushes.Transparent,
BorderBrush = Brushes.Transparent,
Style = (Style) PART_MasterGrid.FindResource("buttonStyle"),
BorderThickness = new Thickness(0)
};
stackpanel.Children.Add(btn);
var path = new Path
{
Stroke = Brushes.Black,
Fill = Brushes.Gold,
StrokeThickness = 1,
Stretch = Stretch.Fill,
Width = 9.0,
Height = 15
};
var pathgeometry = PinPathgeometry();
path.Data = pathgeometry;
btn.Content = path;
btn.Click += (o, e) =>
{
int level = layer.Level;
var pgrid = parentGrid;
var item = _rowLayers[level].Btn;
if (item.Visibility == Visibility.Collapsed)
RowUndockPane(level, o as Button, pgrid);
else
RowDockPane(level, o as Button, pgrid);
};
if (layer.Content != null)
{
dockpanel.Children.Add(layer.Content);
DockPanel.SetDock(layer.Content,Dock.Top);
}
grid.MouseEnter += (o, e) =>
{
var level = layer.Level;
for (var i = 1; i < _rowLayers.Count; i++)
{
if (i == level)
continue;
if (_rowLayers[i].Btn.Visibility == Visibility.Visible)
{
_rowLayers[i].Grid.Visibility = Visibility.Collapsed;
}
}
};
PART_MasterGrid.Children.Add(grid);
Grid.SetRow(grid,0);
var gnb= new GridnFloatingBtnCombo(grid, AddToRowStackPanel(layer));
if (numberofRows > 0)
{
for (int i = layer.Level; i < numberofRows; i++)
{
gnb.RowDefinitions.Add(new RowDefinition
{
SharedSizeGroup = RowStr + (i + 1),
Height = GridLength.Auto
});
}
}
_rowLayers.Add(gnb);
}
The PinPathGeometry method just deals with the drawing of the pin that is housed within the pin Button for all dock-able content.
private static PathGeometry PinPathgeometry()
{
return new PathGeometry
{
Figures = new PathFigureCollection
{
new PathFigure
{
StartPoint = new Point(10,0),
IsFilled = true,
Segments = new PathSegmentCollection
{
new LineSegment{Point = new Point(10,0)},
new LineSegment{Point = new Point(30,0)},
new LineSegment{Point = new Point(30,5)},
new LineSegment{Point = new Point(10,5)},
new LineSegment{Point = new Point(10,0)}
}
},
new PathFigure
{
StartPoint = new Point(4.5,5),
Segments = new PathSegmentCollection
{
new LineSegment{Point = new Point(40.5,5)}
}
},
new PathFigure
{
StartPoint = new Point(22,5),
Segments = new PathSegmentCollection
{
new LineSegment{Point = new Point(22,10)}
}
}
}
};
}
The AddToColumnStackPanel serves the sole purpose of creating a button that is then added to either the PART_RightCntl
or the PART_LeftCntl based on the Layer's ColumnLocation. The Button has a Click event
handler attached that sets the visibility of the current Layer's Grid and collapses any other column Layer that is not docked.
private Button AddToColumnStackPanel(Layer layer)
{
var btn = new Button
{
Background = Brushes.Transparent,
BorderBrush = Brushes.Transparent,
BorderThickness = new Thickness(0),
Height = 22,
MinWidth = 65.0,
Padding = new Thickness(10,0,15,0),
FontWeight = FontWeights.Bold,
Style = (Style)PART_MasterGrid.FindResource("buttonStyle"),
Content = layer.Name
};
btn.Click += (o, e) =>
{
var level = layer.Level;
var item = _columnLayers[level];
item.Grid.Visibility = Visibility.Visible;
Grid.SetZIndex(item.Grid, 1);
for (int i = 1; i < _columnLayers.Count; i++)
{
if (i == level)
continue;
var loc = _columnLayers[i];
Grid.SetZIndex(loc.Grid, 0);
if (loc.Btn.Visibility == Visibility.Visible)
loc.Grid.Visibility = Visibility.Collapsed;
}
};
if (layer.ColumnLocation==Layer.LayerColumnLocation.Right)
PART_RightCntl.Children.Add(btn);
else
PART_LeftCntl.Children.Add(btn);
return btn;
}
The AddToRowStackPanel serves the sole purpose of creating a button that is then added to the PART_BottomCntl StackPanel.
The Button has a Click event handler attached that sets the visibility of the current Layer's Grid and collapses any other row
Layer that is not docked.
private Button AddToRowStackPanel(Layer layer)
{
var btn = new Button
{
Background = Brushes.Transparent,
BorderBrush = Brushes.Transparent,
BorderThickness = new Thickness(0),
Height = 24,
Padding = new Thickness(10, 0, 15, 0),
FontWeight = FontWeights.Bold,
Style = (Style)PART_MasterGrid.FindResource("buttonStyle"),
Content = layer.Name
};
btn.Click += (o, e) =>
{
var level = layer.Level;
var item = _rowLayers[level];
item.Grid.Visibility = Visibility.Visible;
Grid.SetZIndex(item.Grid,1);
for(int i=1; i<_rowLayers.Count; i++)
{
if (i==level)
continue;
var loc = _rowLayers[i];
Grid.SetZIndex(loc.Grid,0);
if (loc.Btn.Visibility == Visibility.Visible)
loc.Grid.Visibility = Visibility.Collapsed;
}
};
PART_BottomCntl.Children.Add(btn);
return btn;
}
The ColumnDockPane method, deals with docking layered content to either the left side or
right side of the main content. It starts the process off by checking
what column location the Layer should reside. If it is on the right the Layer 0
Grid adds a ColumnDefinition whose SharedGroupSize
is equivalent to that of the current Layer being docked, else if it is on the left side, the ColumnDefinition is inserted at index 0 and
the MainContentLocation is incremented by 1. We then call the Grid.SetColumn to set Layer 0's main content
to its new column location. The next step is to perform the exact same logic done for Layer 0 to the current Layer we are trying to dock
(i.e., we add a ColumnDefinition for any docked Layer whose level is above that of the current Layer being docked).
Lastly we perform the exact same operation of adding a ColumnDefintion (the SharedGroupSize being equivalent to that of the current
Layer) to every docked Layer whose level is less than the current Layer we are trying to dock.
private void ColumnDockPane(int level, Button btn)
{
var item = _columnLayers[level];
item.Btn.Visibility = Visibility.Collapsed;
var rtTrans = new RotateTransform(90);
btn.LayoutTransform = rtTrans;
if (_columnLayers[0].ColumnLocations[level - 1] == Layer.LayerColumnLocation.Right)
_columnLayers[0].
Grid.
ColumnDefinitions.
Add(_columnLayers[0].ColumnDefinitions[level - 1]);
else
{
_columnLayers[0].MainContentPositionIncrement();
_columnLayers[0].
Grid.
ColumnDefinitions.
Insert(0, _columnLayers[0].ColumnDefinitions[level - 1]);
Grid.SetColumn(_columnLayers[0]. Grid.Children[0],
_columnLayers[0].MainContentLocation);
}
for (var i = level + 1; i < _columnLayers.Count; i++)
{
if (_columnLayers[i].Btn.Visibility != Visibility.Collapsed)
continue;
if (item.ColumnLocations[i-level - 1] == Layer.LayerColumnLocation.Right)
item.Grid.ColumnDefinitions.Add(item.ColumnDefinitions[i - level - 1]);
else
{
item.MainContentPositionIncrement();
item.Grid.ColumnDefinitions.Insert(0, item.ColumnDefinitions[i-level - 1]);
foreach (UIElement child in item.Grid.Children)
{
Grid.SetColumn(child, item.MainContentLocation - 1);
}
}
}
for (var i = 1; i < level; i++)
{
var loc = _columnLayers[i];
if (loc.Btn.Visibility != Visibility.Collapsed)
continue;
if (loc.ColumnLocations[level - 1 - i] == Layer.LayerColumnLocation.Right)
loc.Grid.ColumnDefinitions.Add(loc.ColumnDefinitions[level - 1 - i]);
else
{
loc.MainContentPositionIncrement();
loc.Grid.ColumnDefinitions.Insert(0, loc.ColumnDefinitions[level - 1 - i]);
foreach (UIElement child in loc.Grid.Children)
{
Grid.SetColumn(child, loc.MainContentLocation - 1);
}
}
}
}
The ColumnUndockPane method is the exact opposite of the ColumnDockPane method and is self explanatory once the reader understands the latter method.
private void ColumnUndockPane(int level, Button btn)
{
var item = _columnLayers[level];
item.Btn.Visibility = Visibility.Visible;
btn.LayoutTransform = null;
item.Grid.Visibility = Visibility.Visible;
for (var i = 0; i < level; i++)
{
if (_columnLayers[i].ColumnLocations[level - 1-i] == Layer.LayerColumnLocation.Left)
{
_columnLayers[i].MainContentPositionDecrement();
if(i==0)
Grid.SetColumn(_columnLayers[i].Grid.Children[0],
_columnLayers[i].MainContentLocation);
else
{
foreach (UIElement child in _columnLayers[i].Grid.Children)
{
Grid.SetColumn(child, _columnLayers[i].MainContentLocation - 1);
}
}
}
_columnLayers[i].
Grid.
ColumnDefinitions.
Remove(_columnLayers[i].ColumnDefinitions[level - 1 - i]);
}
int v = 0;
foreach (var t in item.ColumnDefinitions)
{
if (item.ColumnLocations[v++] == Layer.LayerColumnLocation.Left)
{
item.MainContentPositionDecrement();
foreach (UIElement child in item.Grid.Children)
{
Grid.SetColumn(child, item.MainContentLocation - 1);
}
}
item.Grid.ColumnDefinitions.Remove(t);
}
}
The RowDockPane method is simpler than its column equivalent, as we do not have to worry about an up or down location.
We just add to the parent Grid the RowDefinition that ties to the current row Layer we are trying
to dock. The next step is to perform the exact same logic done for Layer 0 to the current Layer we are trying
to dock (i.e., we add a RowDefinition for any docked Layer whose level is above that of the current Layer being docked).
Lastly we perform the exact same operation of adding a RowDefinition (the SharedGroupSize being equivalent to that of the current
Layer) to every docked Layer whose level is less than the current Layer we are trying to dock.
private void RowDockPane(int level, Button btn, Grid parentGrid)
{
var item = _rowLayers[level];
item.Btn.Visibility = Visibility.Collapsed;
var rtTrans = new RotateTransform(90);
btn.LayoutTransform = rtTrans;
parentGrid.RowDefinitions.Add(_rowLayers[0].RowDefinitions[level - 1]);
for(var i=level+1; i<_rowLayers.Count; i++)
{
if (_rowLayers[i].Btn.Visibility == Visibility.Collapsed)
item.Grid.RowDefinitions.Add(item.RowDefinitions[i-level-1]);
}
for(var i =1; i<level;i++)
{
var loc = _rowLayers[i];
if(loc.Btn.Visibility==Visibility.Collapsed)
loc.Grid.RowDefinitions.Add(loc.RowDefinitions[level-1-i]);
}
}
RowUndockPane is the exact opposite of RowDockPane, so no explanation is needed here.
private void RowUndockPane(int level, Button btn, Grid parentGrid)
{
var item = _rowLayers[level];
item.Btn.Visibility = Visibility.Visible;
btn.LayoutTransform = null;
item.Grid.Visibility = Visibility.Visible;
parentGrid.RowDefinitions.Remove(_rowLayers[0].RowDefinitions[level - 1]);
for(int i=1; i<level; i++)
{
_rowLayers[i].Grid.RowDefinitions.Remove(_rowLayers[i].RowDefinitions[level - 1-i]);
}
foreach (RowDefinition t in item.RowDefinitions)
{
item.Grid.RowDefinitions.Remove(t);
}
}
Finally to show a working example as the images you saw above, in MainWindow.xaml of the solution you created, you add the below
XAML, and voila! you have
a working docking application (comment out the toolbars as they include images that are enclosed within the project). Although the
XAML is placed here,
I would suggest collapsing it when reading and download the project itself.
<Window x:Class="DockableVsExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Controls;assembly=Controls"
Title="MainWindow" Height="800" Width="1024">
<Window.Resources>
<RadialGradientBrush
x:Key="myColorfulLabelBrush"
RadiusX="0.5"
RadiusY="1"
>
<GradientStop Color="#CC0D1000" Offset="0.1"/>
<GradientStop Color="CadetBlue" Offset="0.9"/>
</RadialGradientBrush>
<RadialGradientBrush
x:Key="myColorfulBorderBrush"
RadiusX="0.4"
RadiusY="0.6"
>
<GradientStop Color="#CC3D2614" Offset="0.3"/>
<GradientStop Color="Gold" Offset="0.8"/>
</RadialGradientBrush>
</Window.Resources>
<DockPanel >
<DockPanel.BitmapEffect>
<BevelBitmapEffect BevelWidth="15" EdgeProfile="BulgedUp"/>
</DockPanel.BitmapEffect>
<Menu DockPanel.Dock="Top">
<MenuItem Header="File">
File
</MenuItem>
<MenuItem Header="Edit">
Edit
</MenuItem>
<MenuItem Header="View">
View
</MenuItem>
<MenuItem Header="Project">
Project
</MenuItem>
<MenuItem Header="Build">
Build
</MenuItem>
<MenuItem Header="Data">
Data
</MenuItem>
<MenuItem Header="Tools">
Tools
</MenuItem>
<MenuItem Header="Window">
Window
</MenuItem>
<MenuItem Header="Community">
Community
</MenuItem>
<MenuItem Header="Help">
Help
</MenuItem>
</Menu>
<Border
DockPanel.Dock="Top"
BorderBrush="{StaticResource myColorfulBorderBrush}"
BorderThickness="0">
<Label
Background="{StaticResource myColorfulLabelBrush}"
Foreground="Wheat"
FontWeight="ExtraBlack"
FontSize="16"
HorizontalContentAlignment="Center">
Docking Yeahhh!!!!
</Label>
<Border.BitmapEffect>
<EmbossBitmapEffect />
</Border.BitmapEffect>
</Border>
<StatusBar
DockPanel.Dock="Bottom"
Background="{StaticResource myColorfulLabelBrush}"
Height="15"/>
<controls:LayeredGrid
Grid.Row="1"
Grid.Column="2"
Grid.RowSpan="3">
<controls:LayeredGrid.Layers>
<controls:Layer Level="2" Orientation="Row" Name="Text Manager 2">
<controls:Layer.Content>
<controls:LayeredGrid>
<controls:LayeredGrid.Layers>
<controls:Layer Level="1" Orientation="Row" Name="Logger">
<controls:Layer.Content>
<ListBox
MinHeight="60"
>
<ListBoxItem Content="{Binding Title}"></ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="1" Orientation="Column"
Name="Solution Explorer" ColumnLocation="Right">
<controls:Layer.Content>
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<ToolBar Grid.Row="0">
<ToggleButton >
<Image Source="Images\Home-icon.png"/>
</ToggleButton>
<ToggleButton>
<Image Source="Images\Next-icon.png"/>
</ToggleButton>
<ToggleButton>
<Image Source="Images\Next-icon.png">
<Image.LayoutTransform>
<RotateTransform Angle="180"/>
</Image.LayoutTransform>
</Image>
</ToggleButton>
</ToolBar>
<TreeView
Grid.Row="1"
>
<TreeViewItem Header="Solution Explorer">
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
</TreeViewItem>
</TreeView>
</Grid>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="2" Orientation="Column" Name="Toolbox">
<controls:Layer.Content>
<ListBox >
<ListBoxItem>Button</ListBoxItem>
<ListBoxItem>Label</ListBoxItem>
<ListBoxItem>CheckBox</ListBoxItem>
<ListBoxItem>ListBox</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="3" Orientation="Column" Name="Toolbox Manager">
<controls:Layer.Content>
<ListBox >
<ListBoxItem>Button</ListBoxItem>
<ListBoxItem>Label</ListBoxItem>
<ListBoxItem>CheckBox</ListBoxItem>
<ListBoxItem>ListBox</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="4" Orientation="Column" Name="Numbers">
<controls:Layer.Content>
<ListBox >
<ListBoxItem>1</ListBoxItem>
<ListBoxItem>2</ListBoxItem>
<ListBoxItem>3</ListBoxItem>
<ListBoxItem>4</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="5" Orientation="Column" Name="Names">
<controls:Layer.Content>
<ListBox >
<ListBoxItem>Ty</ListBoxItem>
<ListBoxItem>Tayo</ListBoxItem>
<ListBoxItem>Temitayo</ListBoxItem>
<ListBoxItem>Lauren</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="0" >
<controls:Layer.Content>
<ListBox
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="Wheat">
<ListBoxItem>Article #1</ListBoxItem>
<ListBoxItem>Article #2</ListBoxItem>
<ListBoxItem>Article #3</ListBoxItem>
<ListBoxItem>Article #4</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
</controls:LayeredGrid.Layers>
</controls:LayeredGrid>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="1" Orientation="Column"
Name="Solution Explorer" ColumnLocation="Right">
<controls:Layer.Content>
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<ToolBar Grid.Row="0">
<ToggleButton >
<Image Source="Images\Home-icon.png"/>
</ToggleButton>
<ToggleButton>
<Image Source="Images\Next-icon.png"/>
</ToggleButton>
<ToggleButton>
<Image Source="Images\Next-icon.png">
<Image.LayoutTransform>
<RotateTransform Angle="180"/>
</Image.LayoutTransform>
</Image>
</ToggleButton>
</ToolBar>
<TreeView
Grid.Row="1"
>
<TreeViewItem Header="Solution Explorer">
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
</TreeViewItem>
</TreeView>
</Grid>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="2" Orientation="Column"
Name="Explorer" ColumnLocation="Right">
<controls:Layer.Content>
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<TreeView
Grid.Row="1"
>
<TreeViewItem Header="Explorer">
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #4"></TreeViewItem>
<TreeViewItem Header="Project #5"></TreeViewItem>
<TreeViewItem Header="Project #6"></TreeViewItem>
<TreeViewItem Header="Project #7"></TreeViewItem>
<TreeViewItem Header="Project #8"></TreeViewItem>
<TreeViewItem Header="Project #9"></TreeViewItem>
<TreeViewItem Header="Project #10"></TreeViewItem>
<TreeViewItem Header="Project #11"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
</TreeViewItem>
</TreeView>
</Grid>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="3" Orientation="Column" Name="Toolbox">
<controls:Layer.Content>
<ListBox >
<ListBoxItem>Button</ListBoxItem>
<ListBoxItem>Label</ListBoxItem>
<ListBoxItem>CheckBox</ListBoxItem>
<ListBoxItem>ListBox</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="4" Orientation="Column" Name="Toolbox Manager">
<controls:Layer.Content>
<ListBox >
<ListBoxItem>Button</ListBoxItem>
<ListBoxItem>Label</ListBoxItem>
<ListBoxItem>CheckBox</ListBoxItem>
<ListBoxItem>ListBox</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="5" Orientation="Column" Name="Numbers">
<controls:Layer.Content>
<ListBox >
<ListBoxItem>1</ListBoxItem>
<ListBoxItem>2</ListBoxItem>
<ListBoxItem>3</ListBoxItem>
<ListBoxItem>4</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="6" Orientation="Column" Name="Names">
<controls:Layer.Content>
<ListBox >
<ListBoxItem>Ty</ListBoxItem>
<ListBoxItem>Tayo</ListBoxItem>
<ListBoxItem>Temitayo</ListBoxItem>
<ListBoxItem>Lauren</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="1" Orientation="Row" Name="Text Manager">
<controls:Layer.Content>
<ListBox
MinHeight="60"
>
<ListBoxItem Content="{Binding Title}"></ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="0" >
<controls:Layer.Content>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="4"
Background="White"></Grid>
<GroupBox
Grid.Row="1"
Grid.Column="0"
Background="White"
VerticalAlignment="Stretch"
VerticalContentAlignment="Stretch"
Header="Recent Projects">…
</GroupBox>
<GroupBox
Grid.Row="2"
Grid.Column="0"
Background="White"
Header ="Getting Started"
>
…
</GroupBox>
<GroupBox
Grid.Row="3"
Grid.Column="0"
Background="White"
Header="Headlines">…
</GroupBox>
<GridSplitter
Width="2"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="4"
Background="Transparent"
HorizontalAlignment="Left" />
<ListBox
Grid.Column="2"
Grid.Row="0"
Grid.RowSpan="4"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="CadetBlue">
<ListBoxItem>Article #1</ListBoxItem>
<ListBoxItem>Article #2</ListBoxItem>
<ListBoxItem>Article #3</ListBoxItem>
<ListBoxItem>Article #4</ListBoxItem>
</ListBox>
</Grid>
</controls:Layer.Content>
</controls:Layer>
</controls:LayeredGrid.Layers>
</controls:LayeredGrid>
</DockPanel>
</Window>
Points of Interest
There are a few things that I did not implement in the custom control, but would make sense to probably implement if this code is to be used other than as an example.
I will list out a few of the changes that should be made in case someone finds the code useful.
- The
Level as implemented expects the user to take care of placing the correct numbers to
Level (i.e., there can't be gaps, the numbering is expected to be sequential).
This would need to be changed.
- I have not tested how the custom control will act when
Layers are added and removed on the go via code,
however it is easy to see that a collection change event handler in the custom control will be needed to handle such cases.
- The style used in the custom control is defined within the custom control, this defeats the aim of having a custom control as users are unable to template the style of the control.
The above three points are probably the most necessary changes I believe would have to
be done to make the control adequate for general use.