Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WPF

WPF Graphic-based TabControl and Mode Toggling

Rate me:
Please Sign up or sign in to vote.
5.00/5 (11 votes)
28 Mar 2016CPOL5 min read 37K   2.1K   40   6
Using binding over Graphic-elements to template Tabcontrol to a folder's Cardboard-splitters look

Image 1

* This option has some virtues:

1. More 'easy-to-use'.

2. Visual Studio design-time display bug(see 'special notes) - SOLVED. 

3. Tested under vs2015

 

 

 

Image 2

* Special Notes

  • This code was developed under VS2012 NET 4.5
  • This code uses the following libraries:
    • Microsoft.Expression.Interactions
    • System.Windows.Interactivity
  • Under VS2012, some parts of the XAML produce Design-Time Error (while VS2010 accepts it as valid XAML)
    The Run-Time Compiler accepts this XAML and app runs as expected.
    To get Design-Time Designer-View: comment the problematic parts marked in the code:
    XML
    ...
    <!--designtime bug - COMMENT for design-time view-->
    <PolyBezierSegment >
    	<PolyBezierSegment.Points>
    		<MultiBinding Converter="
    		{StaticResource HeaderContentHeightNumOfItemsItemIndex2HeaderLeftCurves}">
    			<Binding ElementName="gridHeaderContent"
    			Path="ActualHeight"/>
    			<Binding RelativeSource="
    			{RelativeSource AncestorType=TabControl}"
    			Path="Items.Count"/>
    			<Binding RelativeSource="
    			{RelativeSource AncestorType=TabItem}"
    			Path="(TabControl.AlternationIndex)"/>
    		</MultiBinding>
    	</PolyBezierSegment.Points>
    </PolyBezierSegment>
    <!--designtime bug - COMMENT for design-time view-->
    
    ...

Introduction

Templating TabControl raises some issues regarding the behavior and possible improvements of the default window's TabControl. In this article, I'll share these and show my attempts to improve this control both in look and behavior. I will use 'Graphic-Elements Binding Technique' to achieve 'Cardboard-folder-splitters' look and Two-Mode Toggle UI solution. For convenience, I've placed the default equivalent aside to my templated custom solution.

Background

When the UI of my currently developed app is called for the use of a TabControl, several drawbacks emerged:

  1. General look -simple and Winforms like
  2. TabItem's headers occupy only portion of the available area, and even worse - when overflow - creates a new row/column which is confusing.
  3. Whenever TabItem's content is 'heavy', the TabControl's UI freezes.
  4. The previous point has some correlation to the 'unusual manner' of which TabControl displays its selected TabItem's content.
    It RE-LOAD the selected TabItem's content for every selection!
    At first glance, this might be considered as a very problematic behavior, actually there are several articles and code samples that aimed to prevent/workaround this exact behavior.
    While experimenting on templating the TabControl, I've actually faced the dilemma of this issue, and surprisingly (after weighing the mentioned alternatives), I've decided to keep it.
    The logic for this relays on real time UI scenarios - TabControl might contain many controls spread within its TabItems, as the TabControl might be only one of many controls in the entire app. Creating and maintaining all the controls inside the TabControl may have substantial burden upon the application while chances that the user will use them might be slim.
    In such cases - 'Load on Demand' might be a better approach.
    So, rather than altering TabControl's 'Load on Demand' behavior, I've put my efforts on improving the 'accompanied symptoms' such as UI responsiveness.

Using the Code

The TabControl Templates

The most noticeable feature of this TabControl is its look, TabItems are defined by a shaped border which stretches to consume the entire available area, this is done inside the TabControl's Style in two places:

  • TabControl's Template
  • TabControl's ItemContainerStyle Template

Each of this template consists of a matching Two columned Grid (the first is for the Items-Headers, the second for content).

The TabControl's Template deals with the right column which holds:

  • Content-Presenter - for the selected TabItem content:
    XML
    <ContentPresenter   x:Name="content"  Opacity="0" ContentSource="SelectedContent" 
    ContentTemplate="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, 
    Path=SelectedItem.ContentTemplate}">
    	<i:Interaction.Triggers >
    		<i:EventTrigger EventName="Loaded">
    			<app:actSelectedContentDisplayHandler
    			ContentTemplate ="{Binding RelativeSource=
    		{RelativeSource AncestorType=TabControl},Path=SelectedItem.ContentTemplate}"
    			LoadingTextBlock ="{Binding ElementName=tbLoading}"
    			/>
    		</i:EventTrigger>
    	</i:Interaction.Triggers>
    </ContentPresenter>
  • Shared border by all TabItems:
    XML
    <Path Grid.Column="1" Stroke="{TemplateBinding BorderBrush}"
    StrokeThickness="{x:Static Member=app:TabListBoxConstants.StrokeThickness}"
    Stretch="None"    Fill="White">
        <Path.Resources>
            <app:HeaderContentWidth2ContentTopLinePoint
            x:Key="HeaderContentWidth2ContentTopLinePoint"/>
            <app:HeaderContentWidth2ContentRTPoint1
            x:Key="HeaderContentWidth2ContentRTPoint1"/>
            <app:HeaderContentWidth2ContentRTPoint2
            x:Key="HeaderContentWidth2ContentRTPoint2"/>
            <app:HeaderContentWidthHeight2ContentRightLinePoint
            x:Key="HeaderContentWidthHeight2ContentRightLinePoint"/>
            <app:HeaderContentWidthHeight2ContentRBPoint1
            x:Key="HeaderContentWidthHeight2ContentRBPoint1"/>
            <app:HeaderContentWidthHeight2ContentRBPoint2
            x:Key="HeaderContentWidthHeight2ContentRBPoint2"/>
            <app:HeaderContentHeight2ContentBottomLinePointForContent
            x:Key="HeaderContentHeight2ContentBottomLinePointForContent"/>
        </Path.Resources>
        <Path.Data>
            <PathGeometry >
                <PathFigure IsClosed="False" >
    
                    <PathFigure.StartPoint>
                        <Point X="{x:Static Member=app:TabListBoxConstants.CornerRadios}"
                        Y="{x:Static Member=app:TabListBoxConstants.HalfStrokeThickness}"/>
                    </PathFigure.StartPoint>
                    <LineSegment Point="{Binding ElementName=gridHeaderContent,
                    Path=ActualWidth,Converter=
    		{StaticResource HeaderContentWidth2ContentTopLinePoint}}"/>
                    <QuadraticBezierSegment
                                Point1="{Binding ElementName=gridHeaderContent,
                                Path=ActualWidth,Converter={StaticResource
                                HeaderContentWidth2ContentRTPoint1}}"
                                Point2="{Binding ElementName=gridHeaderContent,
                                Path=ActualWidth,Converter={StaticResource
                                HeaderContentWidth2ContentRTPoint2}}"
                                />
                    <LineSegment>
                        <LineSegment.Point >
                            <MultiBinding Converter="
                            {StaticResource HeaderContentWidthHeight2ContentRightLinePoint}">
                                <Binding ElementName="
                                gridHeaderContent" Path="ActualWidth"/>
                                <Binding ElementName="
                                gridHeaderContent" Path="ActualHeight"/>
                            </MultiBinding>
                        </LineSegment.Point>
                    </LineSegment>
    
                    <QuadraticBezierSegment >
                        <QuadraticBezierSegment.Point1>
                            <MultiBinding Converter="
                            {StaticResource HeaderContentWidthHeight2ContentRBPoint1}">
                                <Binding ElementName="
                                gridHeaderContent" Path="ActualWidth"/>
                                <Binding ElementName="
                                gridHeaderContent" Path="ActualHeight"/>
                            </MultiBinding>
                        </QuadraticBezierSegment.Point1>
                        <QuadraticBezierSegment.Point2>
                            <MultiBinding Converter="
                            {StaticResource HeaderContentWidthHeight2ContentRBPoint2}">
                                <Binding ElementName="
                                gridHeaderContent" Path="ActualWidth"/>
                                <Binding ElementName="
                                gridHeaderContent" Path="ActualHeight"/>
                            </MultiBinding>
                        </QuadraticBezierSegment.Point2>
                    </QuadraticBezierSegment>
                    <LineSegment Point="{Binding ElementName=gridHeaderContent,
                    Path=ActualHeight,Converter={StaticResource
                    HeaderContentHeight2ContentBottomLinePointForContent}}"></LineSegment>
                </PathFigure>
            </PathGeometry>
        </Path.Data>
    </Path>
  • 'Loading...' message
XML
<TextBlock x:Name="tbLoading"  Text="Loading..."
    FontSize ="18" HorizontalAlignment ="Center"
    VerticalAlignment="Center" Opacity="0"></TextBlock>

The TabControl's ItemContainerStyle Template deals with mostly the left column (TabItem header) but with columns span set to 2 (for reasons I will not get into) which holds :

  • Content-Presenter - for the selected TabItem Header content:
    XML
    <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"   
    Content="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Header}"/>
  • Special border-shape for the TabItem:
    XML
    <Path Grid.ColumnSpan="2"  x:Name="path"  
    Stroke="{TemplateBinding BorderBrush}"   StrokeThickness="
    {x:Static Member=app:TabListBoxConstants.StrokeThickness}" Stretch="None"
    Fill="White"  >
        <Path.Data >
        <PathGeometry >
        <PathFigure  IsClosed="False"
        StartPoint="{Binding ElementName=gridHeaderContent,Path=ActualHeight,
        Converter={StaticResource HeaderContentHeight2ContentBottomLinePoint}}" >
        <QuadraticBezierSegment
        Point1="{Binding ElementName=gridHeaderContent,Path=ActualHeight,
        Converter={StaticResource HeaderContentHeight2ContentLBPoint1}}"
        >
        <QuadraticBezierSegment.Point2>
        <MultiBinding Converter="{StaticResource 
        HeaderContentHeightNumOfItemsItemIndex2HeaderContentLBPoint2   }">
        <Binding ElementName="gridHeaderContent" 
        Path="ActualHeight"/>
        <Binding RelativeSource="{RelativeSource AncestorType=TabControl}" 
        Path="Items.Count"/>
        <Binding RelativeSource="{RelativeSource AncestorType=TabItem}" 
        Path="(TabControl.AlternationIndex)"/>
    </MultiBinding>
    
    </QuadraticBezierSegment.Point2>
    </QuadraticBezierSegment>
    
    ...
  • Special background-border-shape for the TabItem:
    XML
    <Path x:Name="pathdark" Grid.ColumnSpan="2" 
    Visibility="{Binding RelativeSource={RelativeSource TemplatedParent},
    Path=IsSelected,Converter={StaticResource IsSelected2NotVisibility}}"  
    Stroke="Gray"   StrokeThickness="
    {x:Static Member=app:TabListBoxConstants.StrokeThickness}" 
    Stretch="None"  Margin="0" SnapsToDevicePixels="True"
    Fill="WhiteSmoke"  >
        <Path.Data >
        <PathGeometry >
    
        <PathFigure  IsClosed="False"
        StartPoint="{Binding ElementName=gridHeaderContent,
        Path=ActualHeight,Converter={StaticResource HeaderContentHeight2ContentBottomLinePoint}}" >
    
        <QuadraticBezierSegment
        Point1="{Binding ElementName=gridHeaderContent,
        Path=ActualHeight,Converter={StaticResource HeaderContentHeight2ContentLBPoint1}}"
        >
        <QuadraticBezierSegment.Point2>
        <MultiBinding Converter="
        {StaticResource HeaderContentHeightNumOfItemsItemIndex2HeaderContentLBPoint2   }">
        <Binding ElementName="gridHeaderContent" 
        Path="ActualHeight"/>
        <Binding RelativeSource="{RelativeSource AncestorType=TabControl}" 
        Path="Items.Count"/>
    <Binding RelativeSource="{RelativeSource AncestorType=TabItem}" 
    Path="(TabControl.AlternationIndex)"/>
    </MultiBinding>
    
    </QuadraticBezierSegment.Point2>
    </QuadraticBezierSegment>
    
    ...

The TabItems are visually defined by a silhouette border line.
The line is made by a Path element which consists of a number of geometry segments of various types.
Each segment's parameters are binded to various variables such as width/height of available area, header width constant, number of items, index of the item, and so on.
Extrapolation of the actual segment's parameter value is done using a set of case-based ValueConverters.
For example:

XML
...
<LineSegment >
      <LineSegment.Point>
          <MultiBinding Converter="
          {StaticResource HeaderContentHeightNumOfItemsItemIndex2ContentLeftBottomLinePoint}">
              <Binding ElementName="
              gridHeaderContent" Path="ActualHeight"/>
              <Binding RelativeSource="
              {RelativeSource AncestorType=TabControl}"
              Path="Items.Count"/>
              <Binding RelativeSource="
              {RelativeSource AncestorType=TabItem}"
              Path="(TabControl.AlternationIndex)"/>
          </MultiBinding>
      </LineSegment.Point>
  </LineSegment>
  <QuadraticBezierSegment>
      <QuadraticBezierSegment.Point1 >
          <MultiBinding Converter="
          {StaticResource HeaderContentHeightNumOfItemsItemIndex2HeaderRBPoint1}">
              <Binding ElementName="gridHeaderContent"
              Path="ActualHeight"/>
              <Binding RelativeSource="
              {RelativeSource AncestorType=TabControl}"
              Path="Items.Count"/>
              <Binding RelativeSource="
              {RelativeSource AncestorType=TabItem}"
              Path="(TabControl.AlternationIndex)"/>
          </MultiBinding>
      </QuadraticBezierSegment.Point1>
      <QuadraticBezierSegment.Point2 >
          <MultiBinding Converter="
          {StaticResource HeaderContentHeightNumOfItemsItemIndex2HeaderRBPoint2}">
              <Binding ElementName="gridHeaderContent"
              Path="ActualHeight"/>
              <Binding RelativeSource="
              {RelativeSource AncestorType=TabControl}"
              Path="Items.Count"/>
              <Binding RelativeSource="
              {RelativeSource AncestorType=TabItem}"
              Path="(TabControl.AlternationIndex)"/>
          </MultiBinding>
      </QuadraticBezierSegment.Point2>
  </QuadraticBezierSegment>
...

* Note

This Template method actually alters the XAML's tree in a way that the Content of the TabItem is placed in a different branch of the XAML's tree.
Subsequently - RoutedEvents coming from TabItem's content elements could not be 'caught' at the TabItem (that has this content) level !

UI Responsiveness while Loading 'heavy' TabItemContent

As mentioned earlier, I have found the TabControl's 'RE-Load on Demand' behavior quite acceptable, the thing that still troubled me was the UI responsiveness issue especially when loading 'heavy' content.
In such cases, UI becomes frozen even BEFORE TabItems redraws as selected, and returns to be active only after all content has been loaded.

I have solved this using a System.Windows.Interactivity TriggerAction<t> </t>called actSelectedContentDisplayHandler.

The main idea behind this class is to do the following on each new content which is selected (and needs to be loaded):

  1. Hide previous selected content
  2. Show 'Loading...' message
  3. Perform NOT UI-Thread related loading Async while keeping app's UI active, thus allowing the selected TabItem to be redrawn as selected.
  4. Do UI-Thread related loading while occupying the UI-thread for the minimum necessary time
  5. When visual elements are loaded - show content & hide the 'Loading...' message
XML
<ContentPresenter   x:Name="content"  Opacity="0" ContentSource="SelectedContent" 
ContentTemplate="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, 
Path=SelectedItem.ContentTemplate}">
    <i:Interaction.Triggers >
        <i:EventTrigger EventName="Loaded">
            <app:actSelectedContentDisplayHandler
                ContentTemplate ="{Binding RelativeSource={RelativeSource AncestorType=TabControl},
		Path=SelectedItem.ContentTemplate}"
                LoadingTextBlock ="{Binding ElementName=tbLoading}"
                />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ContentPresenter>
C#
private async static void ContentTemplateChanged
    (DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
    ContentPresenter ContPres =
    ((actSelectedContentDisplayHandler)obj).AssociatedObject;// obj.GetValue
    		//(contentPresenterProperty) as ContentPresenter;
    if (ContPres == null)
    {
        return;
    }

    TextBlock tbLoading = obj.GetValue(LoadingTextBlockProperty) as TextBlock;

    ContPres.Opacity = 0;
    tbLoading.Opacity = 1;

    CancellationTokenSource cts = new CancellationTokenSource();

    Task t = Task.Factory.StartNew(() =>
    {
        AutoResetEvent are = new AutoResetEvent(false);
        Application.Current.Dispatcher.BeginInvoke
        (DispatcherPriority.ApplicationIdle, new Action(() =>
        {
            DataTemplate ContTemp =
            obj.GetValue(ContentTemplateProperty) as DataTemplate;
            ContPres.ContentTemplate = ContTemp;

            ContPres.SetValue(areProperty, are);
        }));

        are.WaitOne(1000);
    }, cts.Token);
    ContPres.SetValue(ActiveTaskProperty, t);
    await t;
    ContPres.SetValue(ActiveTaskProperty, null);
    ContPres.Opacity = 1;
    tbLoading.Opacity = 0;
}
C#
void AssociatedObject_LayoutUpdated(object sender, EventArgs e)
{
    if (this.AssociatedObject.GetValue
    (actSelectedContentDisplayHandler.ActiveTaskProperty) != null) // cts!=null)
    			// behaviorTabListBox.t !=null)
    {
        if (VisualTreeHelper.GetChildrenCount(this.AssociatedObject) > 0)
        {
            (this.AssociatedObject.GetValue
            (actSelectedContentDisplayHandler.areProperty) as AutoResetEvent).Set();

        }
    }
}

Two Modes Toggling

One of my personal UI-related eye sores is the use of ToggleButton/Button to switch between two display modes.
In many cases, the relation between the control and the area being changed is not clear, and to top that, the text on the button is confusing - in some cases, it displays the mode currently not displayed, and in others it displays the currently displayed mode name.

Here, I give an alternative UI solution for this issue, while keeping the same UI Design 'language' which makes this feature much more comprehensive to the user.

The principles for creating this feature are much the same as for the TabControl, with addition of animated trasaction between modes.

XML
<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="ShowStates">
        <VisualState x:Name="ShowLeftTab">
            <Storyboard >
                <DoubleAnimationUsingKeyFrames Storyboard.
                TargetName="LeftTab" Storyboard.
                TargetProperty="Opacity">
                    <EasingDoubleKeyFrame KeyTime="0:0:0.2"
                    Value="1"></EasingDoubleKeyFrame>
                </DoubleAnimationUsingKeyFrames>
                <Int32AnimationUsingKeyFrames Storyboard.TargetName="
                LeftTab" Storyboard.TargetProperty="(Panel.ZIndex)">
                    <DiscreteInt32KeyFrame KeyTime="0:0:0"
                    Value="4"></DiscreteInt32KeyFrame>
                </Int32AnimationUsingKeyFrames>
                <DoubleAnimation Storyboard.TargetName="LeftContent"
                Storyboard.TargetProperty="Opacity" To="1">
                </DoubleAnimation>

                <Int32AnimationUsingKeyFrames Storyboard.TargetName="
                RightContent" Storyboard.TargetProperty="(Panel.ZIndex)">
                    <DiscreteInt32KeyFrame KeyTime="0:0:0.2"
                    Value="1"></DiscreteInt32KeyFrame>
                </Int32AnimationUsingKeyFrames>
                <DoubleAnimation Storyboard.TargetName="RightContent"
                Storyboard.TargetProperty="Opacity" To="0">
                </DoubleAnimation>
            </Storyboard>
        </VisualState>
        <VisualState x:Name="ShowRightTab">
            <Storyboard >
                <DoubleAnimationUsingKeyFrames Storyboard.TargetName="
                LeftTab" Storyboard.TargetProperty="Opacity">
                    <EasingDoubleKeyFrame KeyTime="0:0:0.2"
                    Value="0"></EasingDoubleKeyFrame>
                </DoubleAnimationUsingKeyFrames>
                <Int32AnimationUsingKeyFrames Storyboard.TargetName="
                LeftTab" Storyboard.TargetProperty="(Panel.ZIndex)">
                    <DiscreteInt32KeyFrame KeyTime="0:0:0.2"
                    Value="2"></DiscreteInt32KeyFrame>
                </Int32AnimationUsingKeyFrames>
                <DoubleAnimation Storyboard.TargetName="LeftContent"
                Storyboard.TargetProperty="Opacity" To="0">
                </DoubleAnimation>

                <Int32AnimationUsingKeyFrames Storyboard.TargetName="
                RightContent" Storyboard.TargetProperty="(Panel.ZIndex)">
                    <DiscreteInt32KeyFrame KeyTime="0:0:0.2"
                    Value="3"></DiscreteInt32KeyFrame>
                </Int32AnimationUsingKeyFrames>
                <DoubleAnimation Storyboard.TargetName="RightContent"
                Storyboard.TargetProperty="Opacity" To="1">
                </DoubleAnimation>
                <DoubleAnimation Storyboard.TargetName="RightTabFront"
                Storyboard.TargetProperty="Opacity" To="0"
                Duration="0:0:0.1"></DoubleAnimation>
            </Storyboard>
        </VisualState>
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

Points of Interest

The idea of flexible (Binded) geometry has much more potential beyond the layout-related adjustable borders shown in this article.

Hopefully, in my next article, dealing with this technique, I'll describe methods to incorporate animations into it, in order to achieve elaborate UI features such as controls that 'shape-shift' into dialogs and so.

I hope that you found this article helpful.

License

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


Written By
Software Developer (Senior) self employed
Israel Israel
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionvery good work Pin
Sensitive_Heart7-Feb-16 7:08
Sensitive_Heart7-Feb-16 7:08 
AnswerRe: very good work Pin
ntg12328-Mar-16 9:27
ntg12328-Mar-16 9:27 
QuestionRe: very good work Pin
Sensitive_Heart18-Apr-17 10:08
Sensitive_Heart18-Apr-17 10:08 
AnswerRe: very good work Pin
ntg12318-Apr-17 21:32
ntg12318-Apr-17 21:32 
Hello,

First - regarding the design-time error : I've addressed this issue in the article. in a nutshell- there is a different between Vs's DESIGN-time-XAML-compiler (which produces an error) & the RUN-time-XAML-compiler(which accepts & performs the XAML rendering as expected).

second - Tabs-Names(Headers):
MainWindow.xaml line#:895 for the first header.
MainWindow.xaml line#:904 for the second header.
and so on...

BR

g
GeneralRe: very good work Pin
Sensitive_Heart18-Apr-17 21:56
Sensitive_Heart18-Apr-17 21:56 
Questionthere is no work done in background tread Pin
Liero_20-May-13 22:43
Liero_20-May-13 22:43 

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

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