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 GridSplitter
s).
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 Grid
s 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 StackPanel
s as the button bars for the columns and rows. When all the dock-able content
is docked, the StackPanel
s 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 StackPanel
s (left, right, and bottom Button bar) and
a Grid
(serving as a content placeholder). The three StackPanel
s (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.ColumnDefinition
s
which holds a ColumnDefinition
for all column oriented Layer
s that have a Level
greater than that of the Layer
that ties to the class. The same explanation goes for the list of Grid.RowDefinition
s that is defined in the class.
The defined List
of ColumnLocation
s in the class, is synchronized with the list of ColumnDefintion
s 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 ColumnDefinition
s 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 RowDefinition
s 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
ColumnsDefinition
s. 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 ColumnDefinition
s just as with Layer
0,
however in this case the dock-able content will be place in column 0 for Layer
s that have their ColumnLocation
as
Left
and column 2 for Layer
s 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 RowDefinition
s, 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 RowDefinition
s 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
Layer
s 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.