Easy form layout in WPF Part 3 – Adding Groups

This is the third and final post in a series, you may want to start from the beginning:

  1. Easy form layout in WPF Part 1 – Introducing FormPanel.
  2. Easy form layout in WPF Part 2 – How to deal with more complicated scenarios
  3. Easy form layout in WPF Part 3 – Adding Groups (You are here).

Let’s divide the controls from out previous example into groups and produce this dialog box:

Putting multiple FormPanel panels in the window, each in its own GroupBox, is simple – but we need somehow to synchronize the sizes across multiple controls (and we still want to minimize typing).

Let’s look at the XAML for this window (only the form panels part this time)

<l:FormGroupHost>
    <l:FormGroup Header="General Details">
        <TextBlock Text="Title:"/>
        <TextBox/>
        <TextBlock Text="Area:"/>
        <ComboBox/>
        <TextBlock Text="Category:"/>
        <ComboBox/>
        <TextBlock Text="Assigned To:"/>
        <ComboBox/>
        <TextBlock Text="Status:"/>
        <ComboBox/>
        <TextBlock Text="Estimate:"/>
        <TextBox/>
    </l:FormGroup>
    <l:FormGroup Header="User Defined Fields">
        <TextBlock Text="Tags:"/>
        <TextBox/>
        <TextBlock Text="Version:"/>
        <TextBox/>
    </l:FormGroup>
</l:FormGroupHost>

Ok, what’s going on here? there’s a new FormGroup that works like a FormPanel except it has an Header property that probably draws the GroupBox and a FormPanelHost that displays all the groups one after the other and maybe takes care of synchronizing the sizes between panels.

Let’s look at the code for FormGroup:

    public class FormGroup : HeaderedItemsControl
    {
    }

That is one short class, let’s look at it’s XAML style:

<Style TargetType="l:FormGroup">
    <Setter Property="ItemsPanel">
        <Setter.Value>
            <ItemsPanelTemplate>
                <l:FormPanel/>
            </ItemsPanelTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="l:FormGroup">
                <GroupBox Header="{TemplateBinding Header}" Padding="5">
                    <ItemsPresenter/>
                </GroupBox>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
 

So, what do we have here? FormGroup is an HeaderedItemsControl and it has a control template that simply put all it’s children inside a GroupBox, it also has a panel template that makes is use a FormTemplate.

FormGroup is just a FormPanel inside a GroupBox, the clever part is that children of an ItemsControl are added to it’s panel so we don’t have to type the group box and panel XAML for every group.

Now let’s look at FormGroupHost:

    public class FormGroupHost : ItemsControl, IFormPanelCoordinator
    {
        protected override Size MeasureOverride(Size constraint)
        {
            Size result = base.MeasureOverride(constraint);
            DoSizing();
            return result;
        }

        private void DoSizing()
        {
            List groups = new List();

            for (int i = 0; i < Items.Count; ++i)
            {
                var group = ItemContainerGenerator.ContainerFromIndex(i) as FormGroup;
                if (group != null && group.Items.Count > 0)
                {
                    var container = group.ItemContainerGenerator.ContainerFromIndex(0);
                    var panel = VisualTreeHelper.GetParent(container) as FormPanel;
                    if (panel != null)
                    {
                        panel.Coordinator = this;
                        groups.Add(panel);
                    }
                }
            }

            double labelMaxWidth = 0;
            double labelMaxHeight = 0;
            double controlMaxWidth = 0;
            double controlMaxHeight = 0;

            foreach (var current in groups)
            {
                labelMaxWidth = Math.Max(labelMaxWidth, current.LabelSize.Width);
                labelMaxHeight = Math.Max(labelMaxHeight, current.LabelSize.Height);
                controlMaxWidth = Math.Max(controlMaxWidth, current.ControlSize.Width);
                controlMaxHeight = Math.Max(controlMaxHeight, current.ControlSize.Height);
            }

            foreach (var current in groups)
            {
                current.LabelSize = new Size(
                    labelMaxWidth, labelMaxHeight);
                current.ControlSize = new Size(
                    controlMaxWidth, controlMaxHeight);
            }
        }

        #region IFormPanelCoordinator Members

        void IFormPanelCoordinator.ControlOrLabelSizeChanged(FormPanel sender)
        {
            DoSizing();
        }

        #endregion
    }

finally some code, now it’s really quite simple:

  1. Iterate over all FormGroups, extract form panel, get label and control size and set myself as the coordinator (finally we find out what the coordinator is for) for the panel.
  2. Iterate over all panels again, set the maximum label and control size

We do this on MeasureOverride (when the system tells us our layout changed) and when one of the panels notifies us (via the IFormPanelCoordinator interface) that its sizing changed.

FormGroupHost is also an ItemsControl but it ahs no style, by default this will put all the children in a StackPanel (we can change that with a style or by setting ItemsPanel if we want).

You can download the code, VS2008 project that targets .net 3.5SP1 here: FormPanelApp sample code.

The project also contains OkCancelFrame another typing saving control for MVVM that I may write about in the future.

posted @ Tuesday, August 10, 2010 4:01 PM

Comments on this entry:

# re: Easy form layout in WPF Part 3 – Adding Groups

Left by Rajesh Kumar at 9/7/2010 7:30 PM

The logic fails for the checkbox.
Any decent solution for this?

# re: Easy form layout in WPF Part 3 – Adding Groups

Left by Sakthivel at 7/22/2011 1:10 PM

Thank you for such an article and its very interesting. Once again thank you.

Your comment:



 (will not be displayed)


 
 
 
Please add 6 and 5 and type the answer here: