Solved

Property & Style to apply to WPF controls to make them collapsible.

Posted on 2010-08-13
8
1,277 Views
Last Modified: 2013-12-17
I've created a series of controls that can be collapsed and expanded by clicking a little button above the control. My problem now is I feel as though I've copy pasted a lot of code and their should be a way to apply the same thing to each of these controls without so much code replication.

I've gone a certain distance by creating a style for the button, and for the collapsible control that responds to the button, and applying those styles to each page that uses them - but each page still requires it's own copy of the Property "isCollapsed" in its codebehind, as well as an event handler for the button that collapses and expands it.

I was hoping a veteran of WPF might find a way that this property and event handler can also somehow be handled in the style so that no codebehind is required at all.


This is what code the pages using collapsible regions have (It may be useful to open up the image below to follow along with the xaml):
    <Grid>
        ...
        <Grid ...>
            ...
            <!-- The Expand / Collapse button and header for this subsection  -->
            <Border Grid.Column="0" MouseDown="Expand_MouseDown" Style="{StaticResource CollapseLinkBorder}">
                <TextBlock Style="{StaticResource CollapseLink}" />
            </Border>
            <!-- The control title -->
            <TextBlock Grid.Column="2" Style="{StaticResource ControlSectionHeader}">Existing Exposure Sets</TextBlock>
        </Grid>
        
        <!-- This is the panel that will be shown or hidden based on whether the control is collapsed -->
        <DockPanel ... Style="{StaticResource CollapsibleRegion}">
        ...
        </DockPanel>
    </Grid>

Open in new window


Note that there's a styled border containing a styled button which has a mousedown event. This is the same for everything that uses the control. There's then the collapsible area, which might be a Grid, DockPanel, CustomPage, ... any UIElement.

This is the code that each page must have in the codebehind to support the IsCollapsed property:
    public partial class ExistingExposureSets:UserControl, INotifyPropertyChanged
    {
        bool isCollapsed;
        public bool IsCollapsed
        {
            get { return isCollapsed; }
            set
            {
                isCollapsed = value;
                NotifyPropertyChanged("IsCollapsed");
            }
        }

        public ExistingExposureSets()
        {
            this.DataContext = this;
            isCollapsed = false;
            InitializeComponent();
        }

        private void Expand_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            IsCollapsed = IsCollapsed ^ true;
        }

        #region INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;
        protected void NotifyPropertyChanged(String propertyName)
        {
            if(PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion

    }

Open in new window


Ideally, all of this code (except the constructor) could be removed completely.

Finally, here are the styles responsible for making it look the way it does, and actually work:
    <!-- The next three are used on all controls with a collapsible region -->
    <Style TargetType="{x:Type TextBlock}" x:Key="CollapseLink">
        <Setter Property="FontWeight" Value="Bold" />
        <Setter Property="Margin" Value="4,0,4,0" />
        <Setter Property="VerticalAlignment" Value="Center" />
        <Setter Property="HorizontalAlignment" Value="Center" />
        <Setter Property="Text" Value="-" />
        <Style.Triggers>
            <DataTrigger Binding="{Binding ElementName=thisControl, Path=IsCollapsed}" Value="true">
                <Setter Property="Text" Value="+" />
            </DataTrigger>
        </Style.Triggers>
    </Style>
    
    <Style TargetType="{x:Type Border}" x:Key="CollapseLinkBorder">
        <Setter Property="CornerRadius" Value="4,4,4,4" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="BorderBrush" Value="Black" />
        <Setter Property="Width" Value="16" />
        <Setter Property="Cursor" Value="Hand" />
        <Setter Property="Background" Value="{StaticResource CollapseButtonBackground}" />
    </Style>

    <Style x:Key="CollapsibleRegion">
        <Setter Property="UIElement.Visibility" Value="Visible" />
        <Style.Triggers>
            <DataTrigger Binding="{Binding ElementName=thisControl, Path=IsCollapsed}" Value="true">
                <Setter Property="UIElement.Visibility" Value="Collapsed" />
            </DataTrigger>
        </Style.Triggers>
    </Style>

Open in new window

As you can see, even getting this much code into a style required a hack - I can only bind to the correct IsCollapsed property by naming all of the pages containing this style "thisControl". I suppose this would be cleaner if I bound to the first ancestor of type "Page" but I couldn't figure that one out.

I appreciate any advice on reducing the amount of duplicate code in controls that use this feature as much as possible.

For instance, if some sort of Attached Property or Dependency Property (which I know very little about) could be defined in the style xaml, then I would be in the clear. Mostly, I want to avoid having to have an IsCollapsed property defined for every collapsible region on my UI.

Thank you!

Alain
ExpandCollapse.PNG
0
Comment
Question by:alainbryden
  • 4
  • 2
8 Comments
 
LVL 25

Expert Comment

by:apeter
ID: 33435912
Can't you have ExistingExposureSets class has base class for all of your XAML which requires collapsable property. So that the codebehind is inherited automatically and your implementation is at one place ?
0
 
LVL 20

Author Comment

by:alainbryden
ID: 33437137
Hmm, that is a good idea. Every class already inherits a base class though, and only some pages additionally need this iscollapsed property. Is multiple inheritance supported in C#?
0
 
LVL 25

Accepted Solution

by:
apeter earned 250 total points
ID: 33437604
Multiple inheritance is not supported but you can one more "multi level" inheritence class in between.
0
3 Use Cases for Connected Systems

Our Dev teams are like yours. They’re continually cranking out code for new features/bugs fixes, testing, deploying, testing some more, responding to production monitoring events and more. It’s complex. So, we thought you’d like to see what’s working for us.

 
LVL 20

Author Comment

by:alainbryden
ID: 33445226
Ugh, I tried my best to create a class CollapsibleControl that has UserControl as its base class, but then when I try to create a xaml file that uses CollapsibleControl instead of UserControl as the root element, everything goes to hell. I don't think changing the base class is going to solve this one :/

--
Alain
using System;
using System.Windows.Controls;
using System.ComponentModel;

namespace QuartzShareUI.InterfaceComponents.Shared
{
    public abstract class CollapsibleControl:UserControl, INotifyPropertyChanged
    {
        bool isCollapsed;
        public bool IsCollapsed
        {
            get { return isCollapsed; }
            set
            {
                isCollapsed = value;
                NotifyPropertyChanged("IsCollapsed");
            }
        }

        public CollapsibleControl()
        {
            isCollapsed = false;
        }

        private void Expand_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            IsCollapsed = IsCollapsed ^ true;
        }

        #region INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;
        protected void NotifyPropertyChanged(String propertyName)
        {
            if(PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion
    }
}

Open in new window

0
 
LVL 20

Author Comment

by:alainbryden
ID: 33445984
Figured this out just now.

The trick is to use a style toggle button, and use it's IsChecked property in place of having to create an IsCollapsed property:

[code]
    <Style TargetType="{x:Type ToggleButton}" x:Key="CollapseLink">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ToggleButton}">
                    <Border CornerRadius="4,4,4,4" BorderBrush="Black" BorderThickness="1"
                            Width="16" Cursor="Hand" Background="{StaticResource CollapseButtonBackground}">
                        <ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Center"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="FontWeight" Value="Bold" />
        <Setter Property="Content" Value="-" />
        <Style.Triggers>
            <DataTrigger Binding="{Binding ElementName=IsCollapsed, Path=IsChecked}" Value="true">
                <Setter Property="Content" Value="+" />
            </DataTrigger>
        </Style.Triggers>
    </Style>

    <Style x:Key="CollapsibleRegion">
        <Setter Property="UIElement.Visibility" Value="Visible" />
        <Style.Triggers>
            <DataTrigger Binding="{Binding ElementName=IsCollapsed, Path=IsChecked}" Value="true">
                <Setter Property="UIElement.Visibility" Value="Collapsed" />
            </DataTrigger>
        </Style.Triggers>
    </Style>
[/code]

Now all we need is to place one of these toggle buttons in the document, being sure to name it "IsCollapsed", and apply the collapsible region style to the main content's root element, and we can get rid of all those properties and event triggers in the code behind, as well as naming the control "thisControl"

[code]
        <Grid >
            ...
            <!-- The Expand / Collapse button and header for this subsection  -->
            <ToggleButton Grid.Column="0" x:Name="IsCollapsed" Style="{StaticResource CollapseLink}" />
            <!-- The control title -->
            <TextBlock Grid.Column="2" Style="{StaticResource ControlSectionHeader}">Conversions</TextBlock>
        </Grid>

        <!-- This contains the main content for the conversion section -->
        <Grid Grid.Row="1" Margin="4,4,0,0" Style="{StaticResource CollapsibleRegion}">
        ...
[/code]

--
Alain
0
 
LVL 27

Assisted Solution

by:MikeToole
MikeToole earned 250 total points
ID: 33478596
In WPF the Expander control does this already:

        <Expander   Header="An Expanding Region" ExpandDirection="Down">
            <StackPanel>
                <TextBlock>One</TextBlock>
                <TextBlock>Two</TextBlock>
                <TextBlock>Three</TextBlock>
            </StackPanel>
        </Expander>

It defaults to an up/down arrow for the expand?collapse button, but I guess that could be styled
0
 
LVL 20

Author Comment

by:alainbryden
ID: 33532405
Well, the solution was my last post (33445984) but what counts is the points for assistance.

--
Alain
0

Featured Post

Live: Real-Time Solutions, Start Here

Receive instant 1:1 support from technology experts, using our real-time conversation and whiteboard interface. Your first 5 minutes are always free.

Question has a verified solution.

If you are experiencing a similar issue, please ask a related question

A theme is a collection of property settings that allow you to define the look of pages and controls, and then apply the look consistently across pages in an application. Themes can be made up of a set of elements: skins, style sheets, images, and o…
Entity Framework is a powerful tool to help you interact with the DataBase but still doesn't help much when we have a Stored Procedure that returns more than one resultset. The solution takes some of out-of-the-box thinking; read on!
This Micro Tutorial will teach you how to censor certain areas of your screen. The example in this video will show a little boy's face being blurred. This will be demonstrated using Adobe Premiere Pro CS6.
This is Part 3 in a 3-part series on Experts Exchange to discuss error handling in VBA code written for Excel. Part 1 of this series discussed basic error handling code using VBA. http://www.experts-exchange.com/videos/1478/Excel-Error-Handlin…

815 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

12 Experts available now in Live!

Get 1:1 Help Now