1
votes

I want a control whose behavior is as follows:

  • Act like a Grid
  • Each child control is embedded in a horizontal Expander (whose header is binded to the control's Tag property)
  • Each of these Expander has its own ColumnDefinition
  • Only one of these expanders can be expanded at a time
  • Non-expanded Expanders' ColumnDefinition have a width set to Auto
  • The expanded Expander's one is * (Star)

It has to use these exact controls (Grid/Expander), and not some custom ones, so my application's style can automatically apply to them.

I can't seem to find something already made, no built-in solution seems to exist (if only there was a "filling" StackPanel...) and the only solution I can come up with is to make my own Grid implementation, which seems... daunting.

Is there a solution to find or implement such a control?

Here's what I have for now. It doesn't handle the "single-expanded" nor the filling. I don't really know if StackPanel or Expander is to blame for this.

<ItemsControl>
    <ItemsControl.Resources>
        <DataTemplate x:Key="verticalHeader">
            <ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type Expander}}, Path=Header}" />
        </DataTemplate>
        <Style TargetType="{x:Type Expander}"
               BasedOn="{StaticResource {x:Type Expander}}">
            <Setter Property="HeaderTemplate"
                    Value="{StaticResource verticalHeader}" />
            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
            <Setter Property="HorizontalAlignment" Value="Stretch"/>
            <Setter Property="ExpandDirection"
                    Value="Right" />
        </Style>
    </ItemsControl.Resources>
    <ItemsControl.Template>
        <ControlTemplate>
            <!-- Damn you, StackPanel! -->
            <StackPanel Orientation="Horizontal" IsItemsHost="True"/>
        </ControlTemplate>
    </ItemsControl.Template>
    <Expander Header="Exp1">
        <TextBlock Text="111111111" Background="Red"/>
    </Expander>
    <Expander Header="Exp2">
        <TextBlock Text="222222222" Background="Blue"/>
    </Expander>
    <Expander Header="Exp3">
        <TextBlock Text="333333333" Background="Green"/>
    </Expander>
</ItemsControl>
1
Are you using the ItemsSource to populate the content or would it work in a plain StackPanel?Joe
I don't understand your question @Joe; right now, I have a ItemsControl without ItemsSource, using a StackPanel as its ControlTemplate, as you can see in my XAML. The solution I'm searching for should handle plain controls or ItemsSource.Kilazur
See my answer. The solution only works with a Grid container, not ItemsControl with ItemsSource, though it might be able to be expanded for that. If populating expanders with ItemsSource is a key requriement it might not be ideal.Joe

1 Answers

1
votes

My first thought is to perform this kind of action with a Behavior. This is some functionality that you can add to existing XAML controls that give you some additional customization.

I've only looked at it for something that's not using an ItemsSource as I used a Grid with Columns etc. But in just a plain grid, you can add a behavior that listens for it's childrens Expanded and Collapsed events like this:

public class ExpanderBehavior : Behavior<Grid>
{
    private List<Expander> childExpanders = new List<Expander>();

    protected override void OnAttached()
    {
        //since we are accessing it's children, we have to wait until initialise is complete for it's children to be added
        AssociatedObject.Initialized += (gridOvject, e) =>
        {
            foreach (Expander expander in AssociatedObject.Children)
            {
                //store this so we can quickly contract other expanders (though we could just access Children again)
                childExpanders.Add(expander);

                //track expanded events
                expander.Expanded += (expanderObject, e2) =>
                {
                    //contract all other expanders
                    foreach (Expander otherExpander in childExpanders)
                    {
                        if (expander != otherExpander && otherExpander.IsExpanded)
                        {
                            otherExpander.IsExpanded = false;
                        }
                    }

                    //set width to star for the correct column
                    int index = Grid.GetColum(expanderObject as Expander);

                    AssociatedObject.ColumnDefinitions[index].Width = new GridLength(1, GridUnitType.Star);
                };

                //track Collapsed events
                expander.Collapsed += (o2, e2) =>
                {
                    //reset all to auto
                    foreach (ColumnDefinition colDef in AssociatedObject.ColumnDefinitions)
                    {
                        colDef.Width = GridLength.Auto;
                    }
                };
            }
        };
    }
}

Use it like this, note you have to add System.Windows.Interactivity as a reference to your project:

<Window ...
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
        xmlns:local="...">
    <Window.Resources>
        <DataTemplate x:Key="verticalHeader">
            <ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type Expander}}, Path=Header}" />
        </DataTemplate>
        <Style TargetType="{x:Type Expander}"
               BasedOn="{StaticResource {x:Type Expander}}">
            <Setter Property="HeaderTemplate"
                    Value="{StaticResource verticalHeader}" />
            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
            <Setter Property="HorizontalAlignment" Value="Stretch"/>
            <Setter Property="ExpandDirection"
                    Value="Right" />
        </Style>

        <local:ExpanderBehavior x:Key="ExpanderBehavor"/>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>

        <i:Interaction.Behaviors>
            <local:ExpanderBehavior/>
        </i:Interaction.Behaviors>

        <Expander Header="Exp1">
            <TextBlock Text="111111111" Background="Red"/>
        </Expander>
        <Expander Header="Exp2" Grid.Column="1">
            <TextBlock Text="222222222" Background="Blue"/>
        </Expander>
        <Expander Header="Exp3" Grid.Column="2">
            <TextBlock Text="333333333" Background="Green"/>
        </Expander>
    </Grid>
</Window>

The final result:

enter image description here


Edit: Working with ItemsControl - add it to the grid that hosts the items, and add a little to manage the column mapping

public class ItemsSourceExpanderBehavior : Behavior<Grid>
{
    private List<Expander> childExpanders = new List<Expander>();

    protected override void OnAttached()
    {
        AssociatedObject.Initialized += (gridOvject, e) =>
        {
            //since we are accessing it's children, we have to wait until initialise is complete for it's children to be added
            for (int i = 0; i < AssociatedObject.Children.Count; i++)
            {
                Expander expander = AssociatedObject.Children[i] as Expander;

                //sort out the grid columns
                AssociatedObject.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
                Grid.SetColumn(expander, i);

                childExpanders.Add(expander);

                //track expanded events
                expander.Expanded += (expanderObject, e2) =>
                {
                    foreach (Expander otherExpander in childExpanders)
                    {
                        if (expander != otherExpander && otherExpander.IsExpanded)
                        {
                            otherExpander.IsExpanded = false;
                        }
                    }

                    //set width to auto
                    int index = AssociatedObject.Children.IndexOf(expanderObject as Expander);

                    AssociatedObject.ColumnDefinitions[index].Width = new GridLength(1, GridUnitType.Star);
                };

                //track Collapsed events
                expander.Collapsed += (o2, e2) =>
                {
                    foreach (ColumnDefinition colDef in AssociatedObject.ColumnDefinitions)
                    {
                        colDef.Width = GridLength.Auto;
                    }
                };
            }
        };
    }
}

Used:

<ItemsControl>
    <ItemsControl.Template>
        <ControlTemplate>
            <Grid IsItemsHost="True">
                <i:Interaction.Behaviors>
                    <local:ItemsSourceExpanderBehavior/>
                </i:Interaction.Behaviors>
            </Grid>
        </ControlTemplate>
    </ItemsControl.Template>
    <Expander Header="Exp1">
        <TextBlock Text="111111111" Background="Red"/>
    </Expander>
    <Expander Header="Exp2">
        <TextBlock Text="222222222" Background="Blue"/>
    </Expander>
    <Expander Header="Exp3">
        <TextBlock Text="333333333" Background="Green"/>
    </Expander>
</ItemsControl>

Note, that you'll have to add some logic to manage new/removed children if you have any changes to your ItemsSource!