WPF : A Strange Layout Issue

The other day I was doing a new View for a WPF app that we are working on, that required a DataTemplate that consisted something like the following:

<DataTemplate DataType="{x:Type local:EmbeddedViewModel}">
    <Expander x:Name="exp" 
    Background="Transparent"
          IsExpanded="True" ExpandDirection="Down" 
          Expanded="Expander_Expanded" 
              Collapsed="Expander_Collapsed">


        <ListView AlternationCount="0" 
             Margin="0" 
             Background="Coral"
             ItemContainerStyle="{DynamicResource ListItemStyle}"
             BorderBrush="Transparent"
             VerticalAlignment="Stretch"
             HorizontalAlignment="Stretch"
             ItemsSource="{Binding SubItems}"
             IsSynchronizedWithCurrentItem="True"
             SelectionMode="Single">

            <ListView.View>
                <GridView>

                    <GridViewColumn  
                        Width="100" Header="FieldA"
                        DisplayMemberBinding="{Binding FieldA}"/>

                    <GridViewColumn  
                        Width="100" Header="FieldB"
                        DisplayMemberBinding="{Binding FieldB}"/>

                </GridView>

            </ListView.View>
        </ListView>
    </Expander>
</DataTemplate>

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Which is all cool, but when I let the View out there into the wild for testing the users were like, yeah that’s ok when there are lots of items, but what happens when there is only 1 item. We would like that 1 item to take up all the available space of the screen. And it got me thinking into how to do this. So this is what I came up with.

A ItemsControl that has a specialized Grid (GridWithChildChangedNoitfication) control as its ItemsPanelTemplate. The specialized grid simply raises an event to signal that it has had a new VisualChild added.

Here is the code for the full ItemsControl setup

<ItemsControl x:Name="items" 
    ItemsSource="{Binding Path=Items, Mode=OneWay}" 
    Background="Transparent" 
    ScrollViewer.VerticalScrollBarVisibility="Auto">

    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <local:GridWithChildChangedNoitfication 
            VerticalAlignment="Stretch" 
            HorizontalAlignment="Stretch"
            Margin="0" Loaded="ItemsGrid_Loaded"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

    <ItemsControl.ItemTemplate>
        <DataTemplate DataType="{x:Type local:EmbeddedViewModel}">
            <Expander x:Name="exp" 
            Background="Transparent"
                  IsExpanded="True" ExpandDirection="Down" 
                  Expanded="Expander_Expanded" 
                      Collapsed="Expander_Collapsed">


                <ListView AlternationCount="0" 
                     Margin="0" 
                     Background="Coral"
                     ItemContainerStyle="{DynamicResource ListItemStyle}"
                     BorderBrush="Transparent"
                     VerticalAlignment="Stretch"
                     HorizontalAlignment="Stretch"
                     ItemsSource="{Binding SubItems}"
                     IsSynchronizedWithCurrentItem="True"
                     SelectionMode="Single">

                    <ListView.View>
                        <GridView>

                            <GridViewColumn  
                                Width="100" Header="FieldA"
                                DisplayMemberBinding="{Binding FieldA}"/>

                            <GridViewColumn  
                                Width="100" Header="FieldB"
                                DisplayMemberBinding="{Binding FieldB}"/>

                        </GridView>

                    </ListView.View>
                </ListView>
            </Expander>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

And here is what the C# code looks like for the specialized Grid (GridWithChildChangedNoitfication).

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;

namespace ShareOfSize
{
    public class GridWithChildChangedNoitfication : Grid
    {

        /// <summary>
        /// Override that allows us to tell when the
        /// VisualChildren collection has changed
        /// </summary>
        protected override void OnVisualChildrenChanged(
            DependencyObject visualAdded, 
            System.Windows.DependencyObject visualRemoved)
        {
            base.OnVisualChildrenChanged(visualAdded, 
                visualRemoved);
            OnGridVisualChildrenChanged(new EventArgs());
        }


        /// <summary>
        /// Raise an event to signal that a VisualChild has been
        /// added/removed
        /// </summary>
        public event EventHandler<EventArgs> 
            GridVisualChildrenChanged;

        protected virtual void 
            OnGridVisualChildrenChanged(EventArgs e)
        {
            EventHandler<EventArgs> handlers = 
                GridVisualChildrenChanged;

            if (handlers != null)
            {
                handlers(this, e);
            }

        }
    }
}

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

The last piece in the puzzle is some code behind that knows how to resize all the children in the ItemsControl when the VisualChild count of the specialized grid changes.

NOTE : This more than likely could be abstracted to a Attached Behaviour via an Attached Property, I’ll leave that as an activity for the user should they wish to do that. For me as it was so UI I did not mind this bit of code behind.

    public partial class Window1 : Window
    {

        private GridWithChildChangedNoitfication itemsGrid;


        public Window1()
        {
            InitializeComponent();
            this.DataContext = new Window1ViewModel();
            this.Unloaded += ViewUnloaded;

        }


        private void ViewUnloaded(object sender, RoutedEventArgs e)
        {
            if(itemsGrid != null)
            {
                itemsGrid.GridVisualChildrenChanged 
                    -= GridChildrenChanged;
            }
        }

        private void GridChildrenChanged(object sender, EventArgs e)
        {
            ResizeRows();
        }


        private void ResizeRows()
        {
            if (itemsGrid != null)
            {
                itemsGrid.RowDefinitions.Clear();
                for (int i = 0; i < itemsGrid.Children.Count; i++)
                {
                    RowDefinition row = new RowDefinition();
                    row.Height = new GridLength(
                        itemsGrid.ActualHeight/
                            itemsGrid.Children.Count,GridUnitType.Pixel);
                    itemsGrid.RowDefinitions.Add(row);
                            
                    itemsGrid.Children[i].SetValue(
                        HorizontalAlignmentProperty, 
                        HorizontalAlignment.Stretch);
                    itemsGrid.Children[i].SetValue(
                        VerticalAlignmentProperty, 
                        VerticalAlignment.Stretch);
                    itemsGrid.Children[i].SetValue(Grid.RowProperty,i);              
                }
            }         
        }



        private void ItemsGrid_Loaded(object sender, RoutedEventArgs e)
        {
            itemsGrid = sender as GridWithChildChangedNoitfication;
            if(itemsGrid != null)
            {
                itemsGrid.GridVisualChildrenChanged +=
                    GridChildrenChanged;
            }
        }

        private void Expander_Expanded(object sender, RoutedEventArgs e)
        {
            Expander expander = sender as Expander;
            var item = ItemsControl.ContainerFromElement(items, 
                (DependencyObject)sender);

            Int32 row = (Int32)(item).GetValue(Grid.RowProperty);

            if (expander.Tag != null)
            {
                itemsGrid.RowDefinitions[row].Height = 
                    new GridLength((Double)expander.Tag,GridUnitType.Pixel);
            }
        }

        private void Expander_Collapsed(object sender, RoutedEventArgs e)
        {
            Expander expander = sender as Expander;
            var item = ItemsControl.ContainerFromElement(items, 
                (DependencyObject)sender);

            Int32 row = (Int32)(item).GetValue(Grid.RowProperty);

            if (expander.Tag == null)
            {
                expander.Tag = itemsGrid.RowDefinitions[row].Height.Value;
            }
            itemsGrid.RowDefinitions[row].Height = 
                new GridLength(40,GridUnitType.Pixel);
        }



    }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Here is what it all looks like

Share

Now I am sure there is some bright spark out there that could have done this with Grid.IsSharedSizeScope I just couldn’t see that myself, so this is what I came up with.

As always here is a small demo app:

http://sachabarber.net/wp-content/uploads/2009/11/ShareOfSize.zip

Advertisements

4 thoughts on “WPF : A Strange Layout Issue

  1. Oleg Mihailik says:

    Well, you can try this one:

  2. Oleg Mihailik says:

    Doesn’t show XAML, does it?

    What I meant was UniformGrid with Cols=1

  3. sacha says:

    Oleg,

    The UniformGrid works in so far as everything initially gets a equal share of the available Height. But as my requirement NEEDS Expanders, you can not use the UniformGrid as it does not expose RowDefinitions required to store the current/old Expander heights.

    Shame really, thanks for the idea though.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: