Wednesday, October 16, 2013

Extending XamDataGrid to Support Zooming

Zoomable views have become commonplace in many applications. The ability to arbitrarily zoom in and out is a great way to cater to different screen sizes and viewing scenarios. Unfortunately for me, they have become so common that my boss decided our in-development UI needed the same capabilities. Fortunately for me, we were developing the UI in WPF, so adding zoomable controls isn't a big deal because of the LayoutTransform. The UI we were developing had a lot of spreadsheet type views that were implemented with the Infragistics XamDataGrid, so adding zoomable support to this control was a must.

Adding the ability to zoom within the XamDataGrid has the following requirements.

  1. The XamDataGrid cells and column headers must scale to match the current zoom value.
  2. There must be an editable combo box that contains the current zoom value.
  3. The editable combo box must reside in the horizontal scrollbar on the right side.
  4. Ctrl + mouse wheel must zoom in and out by a set percentage when the mouse is within the boundaries of the XamDataGrid.

MouseWheelZooming Attached Properties

I like using attached properties to tack functionality onto already existing controls. It's quick, easy, and malleable. For this feature addition, I need the control to maintain a zoom value, as well as automatically increase/decrease that value on mouse wheel events. For this, I created a MouseWheelZooming class to house the necessary attached properties.

A IsEnabledProperty will determine if the zooming is in use, attaching event handlers when enabled and changing the zoom values when the wheel events occur. The following code is the implementation for the IsEnabledProperty.
public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(MouseWheelZooming),
                                                                                                          new FrameworkPropertyMetadata(false)
                                                                                                          {
                                                                                                              PropertyChangedCallback = ZoomEnabledChanged,
                                                                                                              BindsTwoWayByDefault = true
                                                                                                          });
public static bool GetIsEnabled(DependencyObject item)
{
    return (bool)item.GetValue(IsEnabledProperty);
}
public static void SetIsEnabled(DependencyObject item, bool value)
{
    item.SetValue(IsEnabledProperty, value);
}

private static void ZoomEnabledChanged(DependencyObject s, DependencyPropertyChangedEventArgs args)
{
    Control c = (Control)s;
    bool wasEnabled = (bool)args.OldValue;
    bool isEnabled = (bool)args.NewValue;

    if (wasEnabled)
        c.RemoveHandler(Control.PreviewMouseWheelEvent, new MouseWheelEventHandler(PreviewMouseWheel));

    if (isEnabled)
        c.AddHandler(Control.PreviewMouseWheelEvent, new MouseWheelEventHandler(PreviewMouseWheel), true);
}

private static void PreviewMouseWheel(Object sender, MouseWheelEventArgs e)
{
    if (Keyboard.Modifiers == ModifierKeys.Control)
    {
        Control zoomableControl = (Control)sender;

        double zoomAmount = (double)zoomableControl.GetValue(ZoomAmountProperty);
        double zoomMinimum = (double)zoomableControl.GetValue(ZoomMinimumProperty);
        double zoomMaximum = (double)zoomableControl.GetValue(ZoomMaximumProperty);

        if (e.Delta > 0)
            zoomAmount *= 1.1;
        else
            zoomAmount *= 0.90909090909090906;

        if (zoomAmount > zoomMaximum)
            zoomAmount = zoomMaximum;
        else if (zoomAmount < zoomMinimum)
            zoomAmount = zoomMinimum;

        zoomAmount = Math.Round(zoomAmount, 2);

        zoomableControl.SetValue(ZoomAmountProperty, zoomAmount);
    }
}
Note that this code references ZoomAmountProperty, ZoomMinimumProperty, and ZoomMaximumProperty. These are additional attached properties in my MouseWheelZooming class, but the declarations are omitted here because they are strictly boiler plate.

That's it. I now have my zoom value, which is also automatically changed by the mouse wheel. Now I just need to hook up the zoom value to the control scaling.

LayoutTransform

As mentioned, I used the LayoutTransform to perform all scaling. All FrameworkElements have two types of transforms.
  1. LayoutTransform - This transform is applied before the layout pass. The layout system will work with the transformed size and location when laying out the UI.
  2. RenderTransform - This transform is applied after the layout pass but before the render. Ideal when changes are independent of the rest of the UI because of performance gains.
I use the LayoutTransform because the size changes need to remain sensible within the UI. For example, scaling a grid to 200% shouldn't overlap other items in the UI. Instead, it should expand as far as it's parent allows, and then expose scrollbars if the size exceeds the allowed space. The LayoutTransform provides this functionality.

The transform used to achieve the scaling is a very simple one. It's just a ScaleTransform that uses a common value for the ScaleX and ScaleY to provide fixed ratio scaling. The only problem is that we're using XamDataGrid, which is a bit more sophisticated than more controls. We can't just scale XamDataGrid and expect everything to come out nicely.

Scaling XamDataGrid

When scaling is applied directly to XamDataGrid you end up with scrollbars that scroll the column headers with the cells. All data grid implementations I'm familiar with scroll only the rows, and the column headers remain static at the top of the view. Having scrolling headers is terrible experience for end users. The scaling has to be done at a deeper level within XamDataGrid.

To get proper grid scaling, the column headers and cells need to be scaled independently so that the layout logic will continue to function as designed (leaving the headers at the top). Luckily, this is possible though the use of styles. The XamDataGrid lets you set the style for various components within the grid through FieldLayoutSettings. This class has two properties on it that we'll use.
  1. DataRecordCellAreaStyle - The style applied to each record's cell area.
  2. HeaderLabelAreaStyle - The style applied to the header's label area.
By applying scaling through these styles, I can change the size of the inner components of the grid and still allow the grid's layout logic to work normally, giving correct functionality when scrolling is necessary (the headers will not scroll). Both properties can use the same style.
<Style x:Key="ZoomTransform" TargetType="{x:Type Control}">
    <Setter Property="LayoutTransform">
        <Setter.Value>
            <ScaleTransform ScaleX="{Binding RelativeSource={RelativeSource AncestorType=igData:XamDataGrid}, Path=(vibrantUiWpf:MouseWheelZooming.ZoomAmount)}" 
                                            ScaleY="{Binding RelativeSource={RelativeSource AncestorType=igData:XamDataGrid}, Path=(vibrantUiWpf:MouseWheelZooming.ZoomAmount)}" />
        </Setter.Value>
    </Setter>
</Style>
This style uses relative binding to reach up to the parent XamDataGrid and obtain the zoom amount. Recall that ZoomAmount is the attached property I created, which is why the syntax for the path is different than typical path syntax. Now when creating a XamDataGrid, the FieldLayoutSettings can be set as such.
<igData:XamDataGrid.FieldLayoutSettings>
    <igData:FieldLayoutSettings DataRecordCellAreaStyle="{StaticResource ZoomTransform}" HeaderLabelAreaStyle="{StaticResource ZoomTransform}" />
</igData:XamDataGrid.FieldLayoutSettings>
The XamDataGrid can now be zoomed with the crtl+mouse wheel, or by setting the value of the ZoomAmount attached property.
XamDataGrid at 50%

XamDataGrid at 100%
XamDataGrid at 200%

ScrollViewer with ComboBox

With the zooming now functional, all that remains is displaying the current zoom value in the UI. Recall from #2 and #3 from the 4 requirements. To achieve this, I need a combo box that will reside adjacent to the horizontal scrollbar. The XamDataGrid is built with a ScrollViewer, so to customize this, I created a ControlTemplate for a ScrollViewer that includes the combo box.
<ControlTemplate x:Key="ZoomableScrollViewer" TargetType="ScrollViewer">
    <ControlTemplate.Resources>
        <Style TargetType="ScrollViewer" />
    </ControlTemplate.Resources>
    <Grid Background="{TemplateBinding Background}" Tag="{Binding}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <ScrollBar
                x:Name="PART_VerticalScrollBar"
                Grid.Column="2"
                Minimum="0.0"
                Maximum="{TemplateBinding ScrollableHeight}"
                ViewportSize="{TemplateBinding ViewportHeight}"
                Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=VerticalOffset, Mode=OneWay}"
                Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"         
                Cursor="Arrow"
                AutomationProperties.AutomationId="VerticalScrollBar"/>
        <ScrollBar
                x:Name="PART_HorizontalScrollBar"
                Orientation="Horizontal"
                Grid.Row="1"
                Grid.Column="1"
                Minimum="0.0"
                Maximum="{TemplateBinding ScrollableWidth}"
                ViewportSize="{TemplateBinding ViewportWidth}"
                Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=HorizontalOffset, Mode=OneWay}"
                Visibility="Visible"
                Cursor="Arrow"
                AutomationProperties.AutomationId="HorizontalScrollBar"/>
        <local:EditableComboBox Width="50" Grid.Column="0" Grid.Row="1" IsEditable="True" FontSize="11" Padding="0"
                                                    Text="{Binding RelativeSource={RelativeSource AncestorType=igData:XamDataGrid}, Path=(local:MouseWheelZooming.ZoomAmount), 
                                                            Mode=TwoWay, UpdateSourceTrigger=Explicit, Converter={StaticResource PercentageConverter}, ConverterParameter=%}">
                    <local:EditableComboBox.ItemsSource>
                        <x:Array Type="{x:Type sys:Double}">
                            <sys:Double&gt20</sys:Double>
                            <sys:Double&gt50</sys:Double>
                            <sys:Double&gt70</sys:Double>
                            <sys:Double&gt100</sys:Double>
                            <sys:Double&gt150</sys:Double>
                            <sys:Double&gt200</sys:Double>
                            <sys:Double&gt400</sys:Double>
                        </x:Array>
                    </local:EditableComboBox.ItemsSource>
                    <local:EditableComboBox.ItemTemplate>
                        <DataTemplate>
                            <TextBlock>
                            <TextBlock.Inlines>
                                 <Run Text="{Binding Mode=OneWay}" />
                                 <Run Text="%" />
                            </TextBlock.Inlines>
                            </TextBlock>
                        </DataTemplate>
                    </local:EditableComboBox.ItemTemplate>
        </local:EditableComboBox>
        <ScrollContentPresenter 
                x:Name="PART_ScrollContentPresenter"
                Grid.ColumnSpan="2"
                Margin="{TemplateBinding Padding}"
                Content="{TemplateBinding Content}"
                ContentTemplate="{TemplateBinding ContentTemplate}"
                CanContentScroll="{TemplateBinding CanContentScroll}"/>
        <Rectangle 
                x:Name="Corner"
                Grid.Column="2"
                Grid.Row="1"
                Fill="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
    </Grid>
</ControlTemplate>
I created this ControlTemplate from the default template for ScrollViewer. The main change is that I added the EditableComboBox to the grid layout (line #37). Note the relative source binding. This will enable the combo box to stay in sync with the current zoom value. I couldn't find a direct way to set this ControlTemplate with XamDataGrid, so I instead used a style to set the template.
<Style TargetType="igData:RecordListControl">
    <Style.Resources>
        <Style TargetType="ScrollViewer">
            <Setter Property="Template" Value="{StaticResource ZoomableScrollViewer}" />
        </Style>
    </Style.Resources>
</Style>
This will set my ControlTemplate on any ScrollViewer within the RecordListControl, which is the control used by XamDataGrid. That's all that is needed. The combo box will now display in the bottom corner.

XamDataGrid with full zooming capabilites

Source code: XamDataGrid.Zoomable

Sunday, June 2, 2013

WPF Manhattan Bar Chart - Tutorial #1

Introduction

3D graphics within a UI can often be gimmicky and fail to provide any real added insight. They look good when putting together a brochure, but users find these controls awkward and confusing. I've made several attempts to integrate 3D ideas into charts, and it rarely pays off. Users prefer the cleaner 2D interface. That said, 3D bar charts can be both functional and visually appealing. A simple 3D bar chart can be constructed with relative ease using the WPF 3D capabilities.

Over the next several tutorials, I will construct a Manhattan bar chart that will aim to be an easy to use WPF control that can be dropping into any project. These tutorials will cover the creation of the control itself, the data formatting, the 3D rendering, the labeling, and various other components that will aid in reusability.

Creating the control

To create this control, I've decided to use the Custom Control (WPF) template. I opted for this over the User Control (WPF) template for a couple reasons.
  1. A user control is generally used as a grouping of already existing controls. That's not what we're doing here.
  2. The custom control sets up the /Themes/Generic.xaml, which allows for a default style containing the ControlTemplate to be defined for the control we're creating.
Custom Control (WPF) can be found in the WPF templates.
To add the control, go to "Add New Item" within a C# project a select "Custom Control". Name the control "ManhattanBarChart.cs" and add it to the project. This will create the following class.
public class ManhattanBarChart : Control
{
    static ManhattanBarChart()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(ManhattanBarChart), new FrameworkPropertyMetadata(typeof(ManhattanBarChart)));
    }
}

You can see that we've got our ManhattanBarChart class, and it inherits directly from Control. In WPF, a control is lookless by nature. To give it a visual, a ControlTemplate is necessary. So how do we define a ControlTemplate for our control? That's where theme styles come into play. A theme style is just like a regular style, except it's set implicitly. So by default, your control will have all properties set to the values indicated by the theme style. This is different than the Style property on the Control, which is known as the explicit style. Setting an explicit style will replace any values you set in the themes style with the settings defined on the explicit style.

So to give our control a visual, we need a themes style. Looking back at our class, notice that a static constructor has been created, and in it the DefaultStyleKeyProperty.OverrideMetadata method is called. Calling the OverrideMetadata method is a way of indicating which style should be used as the themes style for ManhattanBarChart. The default parameters set the themes style as the default style for type ManhattanBarChart. Now lets take a look at Generic.xaml, located in the Themes directory.
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:ManhattanBarChartDemo">


    <Style TargetType="{x:Type local:ManhattanBarChart}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:ManhattanBarChart}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

There is our default style, already created for us and ready for use. As mentioned before, to give the control a visual, Control.Template must be set to a ControlTemplate. This is done on lines 7-16. The default template is a simple border, with template bindings that will apply properties set on the ManhattanBarChart to the controls that make up the template. This ControlTemplate is where we'll do all of our XAML work.

Viewport3D

You may be familiar with drawing in WPF using the Canvas. The Canvas provides a simple way to draw almost anything within the specified boundaries. All you have to do is add the items to the Children collection on the Canvas, and it magically appears. It's great, but limited. The biggest limitation is that all drawing is 2D. Tapping into 3D drawing capabilites is a bit more invovled. But it's not all bad. WPF does provide some high level classes that make working within 3D space less painful that past experiences you may have had (working directly with Direct3D or OpenGL, for example).  The starting point for any 3D drawing within WPF is Viewport3D

Viewport3D inherits FrameworkElement, and therefore can be added anywhere within your UI. It's responsible for transforming your 3D scene into 2D space. Much like Canvas, it also has a Children collection. But this is a collection of Visual3D. These items make up the scene, but we'll take a closer look at that in Tutorial #2. For now, lets focus on the setup of the view.

Camera
Every viewport has a camera. Conceptually, the camera is exactly what it sounds like. If you take a camera, point it at a tree, and take a picture, you'll end up with a photograph of a tree. In that scenario, the viewport is the resulting photograph. It displays what's captured by the camera. There are three types of camera's that can be used.
  1. PerspectiveCamera - This is arguably the most widely used camera. It applies perspective to the scene so that objects further from the camera appear smaller. This gives depth, or perspective.
  2. OrthographicCamera - Much like the PerspectiveCamera, except no perspective is applied. Objects are the same size, regardless of distance from the camera.
  3. MatrixCamera - This is a more advanced camera that allows the developer to specify the individual view and projection matrices.
We'll be using PerspectiveCamera with our viewport. To setup the camera, we need to set a position within the scene, the direction that the camera is looking, and the up axis.

<PerspectiveCamera Position="0,0,1" LookDirection="0,0,-1" UpDirection="0,1,0" />
With these settings the camera is sitting at 0,0,1 in the scene and looking down the Z-axis at the scene origin. The up axis is set as the Y-axis (0,1,0), indicating the Y-axis is our vertical axis. That's all we have to do for now. This camera is ready for use.

Adding Viewport3D to the ControlTemplate

It's now time to add to our ControlTemplate that resided in Generic.xaml. Recall that this template will represent the how our control is viewed, as it's set within the default style. Lets add the Viewport3D we've been working on.
<ControlTemplate TargetType="{x:Type local:ManhattanBarChart}">
    <Border Background="{TemplateBinding Background}"
            BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="{TemplateBinding BorderThickness}">
        <Viewport3D Name="viewport">
            <Viewport3D.Camera>
                <PerspectiveCamera Position="0,0,1" LookDirection="0,0,-1" UpDirection="0,1,0" />
            </Viewport3D.Camera>
        </Viewport3D>
    </Border>
</ControlTemplate>

Not much there yet, but we've got our Viewport3D and set the camera. Our control is setup and ready for 3D rendering!

In the next tutorial, we'll setup the rendering of the bars.

Source code for tutorial #1

Friday, January 11, 2013

Using Contains with an Entity DbSet

When using Entity, the DbSet is generally the gateway to your data. The great thing about that is that you can run LINQ queries on it and they will be converted to SQL, allowing you to selectively acquire data without pulling over the entire set. One thing I've found, however, is that because it must eventually translate to SQL, not everything LINQ can do is supported.

I had a case where I needed a list of only items added to the database since my last query. I wanted to use the Contains method as a way to determine what I already had. Consider the following example Context and Item class
class Context : DbContext
{
    public DbSet<Item> Items { get; set; }
}

class Item
{
    public int Id { get; set; }
}
With this context and a list of previously queried items (previousItems), I thought the following would work
context.Items.Where(x => !previousItems.Contains(x));
However when this query is executed, it throws a NotSupportedException with the following message:

"Unable to create a constant value of type 'Item'. Only primitive types ('such as Int32, String, and Guid') are supported in this context."

So what's going on here? What's this business about creating a constant value? I didn't tell it to do that. Well it turns out that's exactly what I told it to do. It comes back to the point made earlier about Entity converting the LINQ to SQL. Remember that this query is actually run against a database, so my Item object has no meaning. Entity wants to convert it to something SQL will understand, but it cannot. That's the reasoning behind only primitives being supported. The query must use types that SQL knows. With this in mind, I'll work on the Item IDs rather than the item itself.
var itemIds = previousItems.Select(x => x.Id);

var newItems = context.Items.Where(x => !itemIds.Contains(x.Id));
I created a list of the IDs from Items that I already have, and use that to compare against the database IDs to determine if I already have the item. Now that I'm working with type Int32 rather than Item, it works! The query runs and I get the correct results.

So how does the Contains method convert to SQL? The IN operator. My LINQ query ends up looking more like this when run against the database
SELECT * FROM items
WHERE Id NOT IN ( ... )
Pretty cool and very useful!

Saturday, December 8, 2012

Recreating a Dropped Database in Entity 5

I've been working on a windows service that constantly monitors a local database. One of the responsibilities of the service is to create and seed the database if it doesn't already exist. This was necessary because the database may be dropped at anytime within the system, and it needs to be recreated quickly. With Entity Framework 4.3 and no migration support, the following code achieved that.

if (!context.Database.Exists())
{
    context.Database.Create();
    _Seed(context);
    context.SaveChanges();
}

New specifications for this system required support database migrations, so I took the opportunity to upgrade the code to use Entity Framework 5. Without going into the details of how EF 5 migrations work, I'll just say that I enabled migrations and I'm now using the Configuration class to handle seeding by overriding the Seed(context) method.

To handle the migrations, I opted to use the MigrateDatabaseToLatestVersion initializer provided in the framework. This initializer executes any missing migrations to get the database current.

Entity.Database.SetInitializer<DatabaseContext>(
    new MigrateDatabaseToLatestVersion<DatabaseContext, Migrations.Configuration>());

After these changes, I simplified the database check and creation process to the following

if (!context.Database.Exists())
{
    context.Database.Initialize(true);
}

What I found was this works fine when no database exists and the service is started, but it no longer works properly when the database is dropped during the runtime of the service. The Exists check properly returns false, but the Initialize method call does not actually create the database. I'm passing true as the argument, which should force the initializer to run. So what's going on here? From what I can tell, the instance of MigrateDatabaseToLatestVersion will only create a database once. To work around this, I create a new instance of the initializer whenever the database doesn't exist.

if (!context.Database.Exists())
{
    Entity.Database.SetInitializer<Database.DatabaseContext>(
        new MigrateDatabaseToLatestVersion<Database.DatabaseContext, Migrations.Configuration>());
    context.Database.Initialize(true);
}
else
{
    context.Database.Initialize(false);
}

The database is now properly recreated when it no longer exists.

Tuesday, November 6, 2012

Entity Framework 5 requires NuGet 2

After upgrading to Entity Framework 5, the Enable-Migrations command starting failing. I had used it with EF 4.3, but now I kept getting the following error when running Enable-Migrations in the Package Manager Console:

The EntityFramework package manager is not installed on project 'MigrationTest'.

But why? I clearly had EF5 installed, and I wasn't getting any build time errors. What's the problem? Well, after a lot of searching and research, I happened to read that EF 5 is only compatible with NuGet 2. I checked on the version I had installed, and sure enough, I was running NuGet 1.7. After upgrading to the most current version of NuGet, Enable-Migrations started working again.

I have two major issues with this.

  1. Why not a more clear error message? Telling me the package wasn't installed sent me off in the wrong direction.
  2. Why doesn't the Entity Framework Development Center state this information in the 'Get Entity Framework' section? There is no mention of EF 5 being incompatible with older versions of NuGet. Thanks guys.
In any case, if you find yourself upgraded Entity, don't forget to keep NuGet up to date as well.


Friday, October 19, 2012

Intel SDK for OpenCL Fails to Create New Projects

I recently began working with OpenCL and opted to use Intel's OpenCL SDK. I'm working in a Windows environment with Visual Studio 2010, so I was drawn to Intel's SDK because of their Visual Studio plugin support. Unfortunately, I didn't get very far before I hit the first snag.

After installing the SDK, the following project template is available.
OpenCL projects are available after installing the SDK
I gave my project a name and clicked 'OK', and I ended up with an empty project. It had the name I gave it, but no files were associated with it. There was nothing to work with. I tried several more times to no avail. All I got were empty projects. After some time on Intel's forums, I found the culprit. There is a small bug in the current SDK, and it causes project creation on Visual Studio 2010 to fail. Luckly, it's an easy fit.

To get projects to create properly, navigate to the Visual Studio directory. From there, go to VC\vcprojects\OpenCL and locate the  OpenCLProj.vsz file. Open this file in a text editor and replace Wizard=VsWizard.VsWizardEngine.9.0 with Wizard=VsWizard.VsWizardEngine.10.0.

That's it! OpenCL projects are now created with the proper content.

Source: http://software.intel.com/en-us/forums/topic/278415

Monday, October 15, 2012

Extending XamDockManager to Include a New Tab Button

The Infragistics XamDockManager provides an MDI style interface that mirrors Visual Studio 2010, complete with panes that can be floated, docked, and tabbed. One feature not found in XamDockManager but common in MDI tabbed applications is a new tab button. For example, most modern web browsers include this button to the left of the tab headers. I recently found myself needing this feature to meet software requirements that were added to a project I had already begun. With my entire UI already built on XamDockManager, I frantically searched the documentation for the some hidden ability to expose this button, but I came up empty. But all was not lost. The power and flexibility of WPF came through, and I was able to add the button myself.

Any developer that has used WPF knows that the heart of technology lies in styles and templates. The Infragistics implementation is no different, and they rely on it heavily to achieve theming. Why does this matter to me? One reason. All of these templates and styles are provided to the developers and can be found in the install directory. For example, mine are located in "C:\Program Files (x86)\Infragistics\NetAdvantage 2012.1\WPF\DefaultStyles". With these tools in hand, customization can begin.

Below is my basic implementation of the XamDockManager with MDI tabs.

<igDock:XamDockManager Name="dockManager">
    <igDock:DocumentContentHost>
        <igDock:SplitPane>
            <igDock:TabGroupPane Name="tabsPane" TabStripPlacement="Top">
                <igDock:ContentPane Header="Tab 1" />
            </igDock:TabGroupPane>
        </igDock:SplitPane>
    </igDock:DocumentContentHost>
</igDock:XamDockManager>

The TabGroupPane is the item to focus on. It's the control that provides the reserved space for tab headers and the list/close buttons. The ControlTemplate for TabGroupPane can be found in the directory mentioned above, located in \DockManger\DockManagerGeneric.xaml.
<ControlTemplate x:Key="{x:Static igDock:TabGroupPane.DocumentTabGroupTemplateKey}" TargetType="{x:Type igDock:TabGroupPane}">
  <DockPanel ClipToBounds="True" SnapsToDevicePixels="True" KeyboardNavigation.TabNavigation="Local">
   <DockPanel x:Name="PART_HeaderArea" Panel.ZIndex="1" DockPanel.Dock="{TemplateBinding TabStripPlacement}">
    <DockPanel>
     <Button x:Name="closeBtn"
       DockPanel.Dock="Right"
       Command="{x:Static igDock:TabGroupPane.CloseSelectedItemCommand}"
       Style="{DynamicResource {x:Static igDock:TabGroupPane.DocumentCloseButtonStyleKey}}" 
       />
     <Menu x:Name="filesMenu" DockPanel.Dock="Right" Style="{StaticResource RootMenuStyle}">
      <MenuItem x:Name="PART_FilesMenuItem"
                            Style="{DynamicResource {x:Static igDock:TabGroupPane.DocumentFilesMenuItemStyleKey}}" />
     </Menu>
                    <!-- AS 9/10/09 TFS19267 - Added CommandParameter -->
     <Button x:Name="showNavigatorButton"
                            DockPanel.Dock="Right"
                            Visibility="Collapsed"
                            Command="{x:Static igDock:DockManagerCommands.ShowPaneNavigator}" 
                            CommandParameter="{TemplateBinding igDock:XamDockManager.DockManager}"
       Style="{DynamicResource {x:Static igDock:TabGroupPane.DocumentPaneNavigatorButtonStyleKey}}" 
                            />
     <ItemsPresenter x:Name="PART_TabHeaderPanel" Margin="5,2,10,0"
                  KeyboardNavigation.TabIndex="1"/>
    </DockPanel>
   </DockPanel>
   <Border x:Name="ContentPanel"
                KeyboardNavigation.TabIndex="2"
                KeyboardNavigation.TabNavigation="Local"
                KeyboardNavigation.DirectionalNavigation="Contained"
                BorderThickness="{TemplateBinding BorderThickness}"
                CornerRadius="3"
                BorderBrush="{DynamicResource {x:Static igDock:DockManagerBrushKeys.TabbedPaneOuterBorderFillKey}}"
                Background="{TemplateBinding Background}"
                Visibility="Visible"
                SnapsToDevicePixels="True" >
    <Border x:Name="InnerBorder"
                        BorderThickness="1"
                        CornerRadius="1"
                        BorderBrush="{DynamicResource {x:Static igDock:DockManagerBrushKeys.TabbedPaneInnerBorderFillKey}}"
                        SnapsToDevicePixels="True" >
     <Border borderbrush="{DynamicResource {x:Static igDock:DockManagerBrushKeys.TabbedPaneCenterFillKey}}" borderthickness="2" snapstodevicepixels="True" x:name="ThickInnerBorder">
      <Border background="{TemplateBinding Background}" borderbrush="{DynamicResource {x:Static igDock:DockManagerBrushKeys.TabbedPaneOuterBorderFillKey}}" borderthickness="1" snapstodevicepixels="True" x:name="InnerMostBorder">
       <ContentPresenter ContentSource="SelectedContent"
                                 Margin="{TemplateBinding Padding}"
                                 x:Name="PART_SelectedContentHost"
                                 SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                                 ContentTemplateSelector="{TemplateBinding SelectedContentTemplateSelector}"
                                 ContentTemplate="{TemplateBinding SelectedContentTemplate}"
                                 Content="{TemplateBinding SelectedContent}" />
      </Border>
     </Border>
    </Border>
   </Border>
  </DockPanel>
</ControlTemplate>
Note that I've omitted the triggers for simplicity. The area to focus on is lines 3-25, where the tab header layout is defined. A DockPanel is used to add the close and menu buttons that appear in the right corner of the control. This is where the new tab button can be added. To add the button, the following line should be added as the last item in the DockManager.
<Button DockPanel.Dock="Left" Style="{StaticResource NewTabButtonStyle}" />
That's all it takes. The new tab button now lines up to the right of the right-most tab header.
New tab button
Looks good, right? Well, it's getting there, but there is a problem I ran across with this layout. The new button is chosen as the victim to lose units when the measurement pass determines not everything will fit, giving the following results when there is little space or a lot of tab headers.

Squashed new tab button
That looks terrible, but there is an easy fix. The layout needs to be modified so that the new tab button will not be resizable. To achieve this, I wrapped the ItemsPresenter together with the button in a seperate DockPanel nested within the original DockPanel. Docking the DockPanel to the left of the parent DockPanel will prevent the items within from being resizable and getting squashed, while keeping them aligned to the left of the control.
<DockPanel DockPanel.Dock="Left" HorizontalAlignment="Left">
    <Button DockPanel.Dock="Left" Style="{StaticResource NewTabButtonStyle}" />
    <ItemsPresenter x:Name="PART_TabHeaderPanel" Margin="5,2,10,0"
                               KeyboardNavigation.TabIndex="1" DockPanel.Dock="Left" />
</DockPanel>

Now that the layout is working, it's time to address the look and feel of the new tab button. The style referenced, NewTabButtonStyle, is a button style I created for the look and feel I wanted. You can get creative here with the design of the button, but I'd recommend at least using the Infragistics brushes in the style. This keeps the coloring consistent if the theme is changed. For example, I used the following keys for coloring.

  • igDock:DockManagerBrushKeys.TabbedDocumentNotActiveOuterBorderFillKey
  • igDock:DockManagerBrushKeys.TabbedDocumentNotActiveCenterFillKey
  • igDock:DockManagerBrushKeys.TabbedDocumentHottrackCenterFillKey
By using these keys, the new tab button changes colors automatically when the theme of the XamDockManager is changed.
That wraps it up. With these minor modifications and additions, I was able to add a core piece of functionality for my application. A win for both WPF and Infragistics.

Source code: XamDockManager.NewTabButton