Me Myself & C#

Manoj Garg’s Tech Bytes – What I learned Today

Posts Tagged ‘Expander’

WPF 101 : Custom Expander Control to show a popup on mouse over

Posted by Manoj Garg on March 29, 2009

This is the second post in this WPF learning series. In this post I will be writing about creating your own custom controls in WPF. I am going to write a custom expander which has similar features as of the WPF expander control plus it will have a feature of showing a Popup on mouse over event of this expander. Content of this popup will be the content of the expander itself. This is the kind of feature where a user wants to see the content of expander while hovering the mouse over the expander header, just like IE8 tab quick preview where it shows a preview of tab content without even opening the tab. So below are our set of requirements:

Requirements

  • It should be an expandable control with a header and content property.
  • It should show a popup when mouse is over the header and control is in collapsed state.
  • Should have an option for showing/hiding the popup.
  • Popup properties like position, size, transparency etc should be configurable.

Choosing the Proper base class

While writing a custom control one must choose the base control which he/she wants to extend with little care with having all the features in mind. For our requirements, we need a base class which provides me a header, a content property. In WPF we have some choices like HeaderedContentControl class and we have an Expander class which itself drives from HeaderedContentControl class. we can use any of them. I decided to use Expander class (well while coding I wasnโ€™t sure what all I wanted to have ๐Ÿ˜‰ ).

Implementation

Visual studio 2008 provides a project of type โ€œWPF Custom Control Libraryโ€ which can be used as a starting point for writing your first custom control. Following figure shows the selection of new project type.

newcustomcontrol

When adding this project type to your visual studio solution, it will add a new project with following items to your solution structure:

  1. CustomControl1.cs: This file contains your custom control class.
  2. Themes Folder with Generic.xaml: This folder will contain the styles for all your custom controls. By default a new style for the customcontrol1 is added to the generic.xaml. One needs to change this style to work with this custom control.

Lets start implementing the new expander with popup. As we have decided we will be using Expander class as the base class for our control. I will be naming this control as CustomExpander.

  1: namespace CustomControls
  2: {
  3:     /// <summary>
  4:     /// This class is an extention to Expander Control. 
  5:     /// This provides a feature for shouwing a popup with the content of the expander on MouseOver
  6:     /// </summary>
  7:     public class CustomExpander : Expander
  8:     {
  9:         #region // .ctor(s)
 10:         static CustomExpander()
 11:         {
 12:             DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomExpander), new FrameworkPropertyMetadata(typeof(CustomExpander)));
 13:         }
 14:         public CustomExpander()
 15:         {
 16:         }
 17:         #endregion
 18:     }
 19: }

The above class definition has a static ctor and a normal default ctor for the CustomExpander class. The static ctor is used to initialize the static properties of the class as well as changing the default style for the control. As highlighted in the above code snippet in line 12, DefaultStyleKeyProperty is used to change the default style for the control by passing the type of the new control.

Now we have to decide upon what all configurable properties we want to expose to the end user of this control. As mentioned in the requirement section above, we will have following dependency properties in our control:

  1. PopUpPlacementModeProperty: This property will allow user to configure the placement of popup in the screen. This is of type PlacementMode.
  2. StaysOpenProperty: This property defines whether the popup will stay open when the popup looses focus. This is of type Boolean.
  3. AllowsTransparencyProperty: This property allows user to set the transparency for the popup. This is a Boolean value.
  4. PopUpWidthProperty: Width of the popup. This is of type Double
  5. PopUpHeightProperty: Height of the popup. This is of type Double

Now lets us decide what controls we will be using to give this control a look of an Expander. For Expander functionality I will use ToggleButton control and a ContentPresenter control. since toggle button has the notion of being in two state (clicked and non clicked), so these state we can use to denote expanded and collapsed state of the expander. For popup functionality we will use wpf PopUp control.

So here is our task, on mouse hover on the togglebutton, set content of the expander as the child ofย  popup. and on click of the expander show the contentpresenter . ๐Ÿ™‚ย  simple enough .. hmmmmmm

But here comes the issue. As we are talking about setting the content of popup same as the content of the expander but any object/ control can be child of only one parent. So we need make sure that while we show the popup or expand the expander only one of them have their content property set and the other one has this as null. To accomplish this we need one more dependency property which will store the content of the expander and we will use this property as the child for popup as well expander but only one at a time.

  1: public static readonly DependencyProperty OldContentProperty =
  2:                    DependencyProperty.Register("OldContent", typeof(object), typeof(CustomExpander));
  3:
  4: private object OldContent
  5: {
  6:      get { return GetValue(OldContentProperty); }
  7:      set { SetValue(OldContentProperty, value); }
  8: }
  9:

Now lets create the style for Expander. First lets design the header for the Expander. So we need a circle with an arrow indicating the expand/collapse direction and a content presenter showing the text of the header. Following code shows the style for the toggle button header.

  1: <!-- Style for the toggle button which wil be used as expander header -->
  2: <Style TargetType="{x:Type ToggleButton}" x:Key="toggleButtonKey">
  3:      <Setter Property="Template">
  4:           <Setter.Value>
  5:                 <ControlTemplate TargetType="{x:Type ToggleButton}">
  6:                        <StackPanel Orientation="Horizontal"
  7:                                  Margin="{TemplateBinding Padding}">
  8:                               <Grid Background="Transparent">
  9:                                    <!-- Circles for the expander header containg the expand direction arrow -->
 10:                                    <Ellipse x:Name="circle"
 11:                                             HorizontalAlignment="Center" VerticalAlignment="Center"
 12:                                             Width="20" Height="20"
 13:                                             Stroke="DarkGray"/>
 14:                                    <Ellipse x:Name="shadow" Visibility="Hidden"
 15:                                             HorizontalAlignment="Center" VerticalAlignment="Center"
 16:                                             Width="17" Height="17" />
 17:
 18:                                    <!-- Expand direction Arrow -->
 19:                                    <Path x:Name="arrow"
 20:                                             VerticalAlignment="Center" HorizontalAlignment="Center"
 21:                                             Stroke="#666" StrokeThickness="2"
 22:                                             Data="M1,1 L4,4 7,1" />
 23:                               </Grid>
 24:
 25:                               <!-- Expander Header Content -->
 26:                               <ContentPresenter ContentSource="{TemplateBinding Content}" Grid.Column="1" Margin="5"/>
 27:                        </StackPanel>
 28:
 29:                        <ControlTemplate.Triggers>
 30:                              <!-- Trigger to change the arrow direction when expanded -->
 31:                              <Trigger Property="IsChecked" Value="true">
 32:                                     <Setter TargetName="arrow"
 33:                                             Property="Data" Value="M 1,4  L 4,1  L 7,4"/>
 34:                              </Trigger>
 35:
 36:                              <!-- Trigger to give a mouse over effect on the circle containing direction arrow -->
 37:                              <Trigger Property="IsMouseOver" Value="true">
 38:                                     <Setter TargetName="circle"
 39:                                             Property="Stroke" Value="#666"/>
 40:                                     <Setter TargetName="arrow"
 41:                                             Property="Stroke" Value="#222"/>
 42:                                     <Setter TargetName="shadow"
 43:                                             Property="Visibility" Value="Visible"/>
 44:                               </Trigger>
 45:                       </ControlTemplate.Triggers>
 46:               </ControlTemplate>
 47:         </Setter.Value>
 48:    </Setter>
 49: </Style>

Now the expander content and popup.

  1: <!-- Expander -->
  2:                             <StackPanel>
  3:                                     <!-- Expander Header -->
  4:                                     <ToggleButton x:Name="expanderHeader"
  5:                                               Style="{StaticResource toggleButtonKey}"
  6:                                               Content="{TemplateBinding Header}"
  7:                                               Padding="{TemplateBinding Padding}"
  8:                                               IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsExpanded,Mode=TwoWay}"/>
  9:
 10:                                     <!-- Expander Content -->
 11:                                     <ContentPresenter x:Name="expanderContent"/>
 12:                             </StackPanel>
 13:
 14:                             <!-- PopUp control which will be showing the content of expander on mouse over -->
 15:                             <Popup x:Name="contentPopUp"
 16:                                    IsOpen="False"
 17:                                    Placement="{TemplateBinding PopUpPlacementMode}"
 18:                                    AllowsTransparency = "{TemplateBinding AllowsTransparency}"
 19:                                    StaysOpen = "{TemplateBinding StaysOpen}"
 20:                                    Width="{TemplateBinding PopUpWidth}"
 21:                                    Height="{TemplateBinding PopUpHeight}">
 22:                             </Popup>

Finally the triggers which would show the popup on mouse over and set the content.

  1: <ControlTemplate.Triggers>
  2:                         <!-- Trigger for showing the popup when Expander control is not expanded and IsMouseOver is true  -->
  3:                         <MultiTrigger>
  4:                             <MultiTrigger.Conditions>
  5:                                 <Condition SourceName="expanderHeader" Property="IsMouseOver" Value="True"></Condition>
  6:                                 <Condition SourceName="expanderHeader" Property="IsChecked" Value="False"></Condition>
  7:                             </MultiTrigger.Conditions>
  8:                             <Setter TargetName="expanderContent"
  9:                                     Property="Content" Value="{x:Null}"></Setter>
 10:                             <Setter TargetName="contentPopUp"
 11:                                     Property="Child" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=OldContent,Mode=OneWay,Converter={StaticResource localConverter}}"></Setter>
 12:                             <Setter TargetName="contentPopUp"
 13:                                     Property="BitmapEffect" Value="{StaticResource outerGlow}"></Setter>
 14:                             <Setter TargetName="contentPopUp"
 15:                                     Property="IsOpen" Value="True"></Setter>
 16:                         </MultiTrigger>
 17:
 18:                         <!-- Trigger for clearing the content of PopUp control when mouse is not over the expander control -->
 19:                         <Trigger SourceName="expanderHeader" Property="IsMouseOver" Value="False">
 20:                             <Setter TargetName="contentPopUp"
 21:                                     Property="Child" Value="{x:Null}"></Setter>
 22:                         </Trigger>
 23:
 24:                         <!-- Trigger for setting the content of expander when IsExpanded is true -->
 25:                         <Trigger SourceName="expanderHeader" Property="IsChecked" Value="True">
 26:                             <Setter TargetName="contentPopUp"
 27:                                     Property="Child" Value="{x:Null}"></Setter>
 28:                             <Setter TargetName="expanderContent"
 29:                                     Property="Content" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=OldContent,Mode=OneWay}"></Setter>
 30:                         </Trigger>
 31:                     </ControlTemplate.Triggers>

So this makes our code complete. But now as we have an option to configure the width and height of the popup. So the behavior of the popup should be something like this, It should zoom in / zoom out it self depending on the size of the content and the dimensions declared by the control user. Here comes the ViewBox wpf control. This control is made to do this. It adjusts its content according to the width/height of the viewbox. So what I will do is, I will make the content of expander as the content of a viewbox and will store this viewbox as the value of OldContent property. This will be done when the template is being applied to the control in OnApplyTemplate method. So I will override this method in our control class and store the content value.

  1: public override void OnApplyTemplate()
  2:         {
  3:             base.OnApplyTemplate();
  4:             object tmpContent = this.Content;
  5:             this.Content = null;
  6:
  7:             Viewbox containerViewBox = new Viewbox();
  8:             containerViewBox.Child = (UIElement)tmpContent;
  9:             OldContent = containerViewBox;
 10:         }

Complete source code is available here.

[PS: Please change the extension of file from .DOC to .ZIP]

Ha-P Coding ๐Ÿ™‚

Posted in C#, WPF, XAML | Tagged: , , , | 2 Comments »