'Why child control ignores the size set with Arrange in parent container?

I wrote an EditorPanel container, which orders controls in a label/editor order. The complete source of the control follows:

public class EditorPanel : Panel
{
    private enum GeneralAlignment
    {
        Begin,
        Center,
        End,
        Stretch
    }

    private static GeneralAlignment ToGeneralAlignment(VerticalAlignment verticalAlignment)
    {
        switch (verticalAlignment)
        {
            case VerticalAlignment.Top:
                return GeneralAlignment.Begin;                    
            case VerticalAlignment.Center:
                return GeneralAlignment.Center;
            case VerticalAlignment.Bottom:
                return GeneralAlignment.End;
            case VerticalAlignment.Stretch:
                return GeneralAlignment.Stretch;
            default:
                throw new InvalidEnumArgumentException("Unsupported vertical alignment!");
        }
    }

    private static GeneralAlignment ToGeneralAlignment(HorizontalAlignment horizontalAlignment)
    {
        switch (horizontalAlignment)
        {
            case HorizontalAlignment.Left:
                return GeneralAlignment.Begin;
            case HorizontalAlignment.Center:
                return GeneralAlignment.Center;
            case HorizontalAlignment.Right:
                return GeneralAlignment.End;
            case HorizontalAlignment.Stretch:
                return GeneralAlignment.Stretch;
            default:
                throw new InvalidEnumArgumentException("Unsupported horizontal alignment!");
        }
    }

    private Size DesiredSizeWithMargin(UIElement element)
    {
        if (element == null)
            return Size.Empty;

        if (element is FrameworkElement frameworkElement)
            return new Size(frameworkElement.DesiredSize.Width + frameworkElement.Margin.Left + frameworkElement.Margin.Right,
              frameworkElement.DesiredSize.Height + frameworkElement.Margin.Top + frameworkElement.Margin.Bottom);
        else
            return element.DesiredSize;
    }

    private static (double elementStart, double elementSize) EvalPlacement(UIElement element,
        double placementRectStart,
        double placementRectSize,
        double elementMarginBegin,
        double elementMarginEnd,
        double elementDesiredSize,
        GeneralAlignment elementAlignment)
    {
        double resultSize;
        double resultStart;

        switch (elementAlignment)
        {
            case GeneralAlignment.Begin:
                resultSize = Math.Max(0, Math.Min(elementDesiredSize, placementRectSize - (elementMarginBegin + elementMarginEnd)));
                resultStart = placementRectStart + elementMarginBegin;
                break;

            case GeneralAlignment.Center:
                resultSize = Math.Max(0, Math.Min(elementDesiredSize, placementRectSize - (elementMarginBegin + elementMarginEnd)));
                resultStart = placementRectStart + (placementRectSize - (resultSize + elementMarginBegin + elementMarginEnd)) / 2 + elementMarginBegin;
                break;

            case GeneralAlignment.End:
                resultSize = Math.Max(0, Math.Min(elementDesiredSize, placementRectSize - (elementMarginBegin + elementMarginEnd)));
                resultStart = placementRectStart + placementRectSize - elementMarginEnd - resultSize;
                break;

            case GeneralAlignment.Stretch:
                resultSize = Math.Max(0, placementRectSize - (elementMarginBegin + elementMarginEnd));
                resultStart = placementRectStart + elementMarginBegin;
                break;

            default:
                throw new InvalidEnumArgumentException("Unsupported alignment!");
        }

        return (resultStart, resultSize);
    }


    private void ArrangeWithAlignment(UIElement element, Rect placementRect, Size cachedDesiredSize)
    {
        if (cachedDesiredSize == Size.Empty)
            cachedDesiredSize = DesiredSizeWithMargin(element);

        Thickness elementMargin = new Thickness();
        HorizontalAlignment elementHorizontalAlignment = HorizontalAlignment.Stretch;
        VerticalAlignment elementVerticalAlignment = VerticalAlignment.Top;

        if (element is FrameworkElement frameworkElement)
        {
            elementMargin = frameworkElement.Margin;
            elementHorizontalAlignment = frameworkElement.HorizontalAlignment;
            elementVerticalAlignment = frameworkElement.VerticalAlignment;
        }

        (double elementTop, double elementHeight) = EvalPlacement(element, 
            placementRect.Top, 
            placementRect.Height, 
            elementMargin.Top, 
            elementMargin.Bottom, 
            cachedDesiredSize.Height,
            ToGeneralAlignment(elementVerticalAlignment));

        (double elementLeft, double elementWidth) = EvalPlacement(element,
            placementRect.Left,
            placementRect.Width,
            elementMargin.Left,
            elementMargin.Right,
            cachedDesiredSize.Width,
            ToGeneralAlignment(elementHorizontalAlignment));
       
        element.Arrange(new Rect(elementLeft, elementTop, elementWidth, elementHeight));
    }

    protected override Size MeasureOverride(Size availableSize)
    {
        double maxLabelWidth = 0.0;
        double maxEditorWidth = 0.0;
        double totalLabelEditorPairHeight = 0.0;

        for (int i = 0; i < InternalChildren.Count; i += 2)
        {
            // Measure label
            InternalChildren[i].Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
            Size labelDesiredSize = DesiredSizeWithMargin(InternalChildren[i]);

            // Measure editor (if any)
            Size editorDesiredSize = Size.Empty;
            if (i + 1 < InternalChildren.Count)
            {
                InternalChildren[i + 1].Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
                editorDesiredSize = DesiredSizeWithMargin(InternalChildren[i + 1]);
            }

            maxLabelWidth = Math.Max(maxLabelWidth, labelDesiredSize.Width);
            maxEditorWidth = Math.Max(maxEditorWidth, editorDesiredSize.Width);
            totalLabelEditorPairHeight += Math.Max(labelDesiredSize.Height, editorDesiredSize.Height);
        }

        // This is required height, regardless of how much space is available
        double resultHeight = totalLabelEditorPairHeight;

        // If space is not constrained, pick as much as labels & editors want. Else, use
        // as much, as is given.
        double resultWidth = double.IsInfinity(availableSize.Width) ? maxLabelWidth + maxEditorWidth : availableSize.Width;

        return new Size(resultWidth, resultHeight);
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        // Label area width

        double labelAreaWidth = 0;
        for (int i = 0; i < InternalChildren.Count; i += 2)
            labelAreaWidth = Math.Max(labelAreaWidth, DesiredSizeWithMargin(InternalChildren[i]).Width);

        labelAreaWidth = Math.Min(labelAreaWidth, finalSize.Width);

        // Editor area width

        double editorAreaWidth = Math.Max(0, finalSize.Width - labelAreaWidth);

        // Arranging controls

        double y = 0;
        int controlIndex = 0;

        while (controlIndex < InternalChildren.Count)
        {
            // Retrieve label and editor

            UIElement label = InternalChildren[controlIndex++];
            Size labelDesiredSize = DesiredSizeWithMargin(label);

            UIElement editor = (controlIndex < InternalChildren.Count) ? InternalChildren[controlIndex++] : null;
            Size editorDesiredSize = DesiredSizeWithMargin(editor);                

            double rowHeight = Math.Max(labelDesiredSize.Height, editorDesiredSize.Height);

            var labelArea = new Rect(0, y, labelAreaWidth, rowHeight);
            ArrangeWithAlignment(label, labelArea, label.DesiredSize);

            // Arrange editor

            if (editor != null)
            {
                var editorArea = new Rect(labelAreaWidth, y, editorAreaWidth, rowHeight);
                ArrangeWithAlignment(editor, editorArea, editor.DesiredSize);
            }

            y += Math.Max(labelDesiredSize.Height, editorDesiredSize.Height);
        }

        return finalSize;
    }
}

Example of usage may look like following:

<Border BorderBrush="Black" BorderThickness="1"
        Width="400" Height="100">
    <controls:EditorPanel>
        <Label>First label</Label>
        <TextBox />
        <Label>Second label</Label>
        <TextBox />
    </controls:EditorPanel>
</Border>

When I run the application, it looks like following:

Editor Panel

However, if I write more text in the textbox, it starts to escape the boundaries set by the EditorPanel itself.

Editor Panel - broken

What is interesting is that the container seems to work properly and allocates proper amount of space for the control:

Allocated space

But the control seems to ignore it completely and ends up bigger than it actually should be:

Too big control

That leads to my question: why the child control ignores the space it was given through the Arrange call?



Solution 1:[1]

It turns out, that I was too generous during the Measure stage. I put more effort into checking, how much space labels and editors can occupy and now everything works.

protected override Size MeasureOverride(Size availableSize)
{
    // Measure labels

    List<Size> labelSizes = new List<Size>();
    for (int i = 0; i < InternalChildren.Count; i += 2)
    {
        // Measure label
        Thickness labelMargin = new Thickness(0);
        if (InternalChildren[i] is FrameworkElement frameworkElement)
            labelMargin = frameworkElement.Margin;

        InternalChildren[i].Measure(new Size(availableSize.Width - (labelMargin.Left + labelMargin.Right), availableSize.Height - (labelMargin.Top + labelMargin.Bottom)));
        labelSizes.Add(DesiredSizeWithMargin(InternalChildren[i]));
    }

    double maxLabelWidth = labelSizes.Max(ls => ls.Width);

    // Measure editors

    List<Size> editorSizes = new List<Size>();
    for (int i = 1; i < InternalChildren.Count; i += 2)
    {
        Thickness editorMargin = new Thickness(0);
        if (InternalChildren[i] is FrameworkElement frameworkElement)
            editorMargin = frameworkElement.Margin;

        InternalChildren[i].Measure(new Size(availableSize.Width - maxLabelWidth - (editorMargin.Left + editorMargin.Right), availableSize.Height - (editorMargin.Top + editorMargin.Bottom)));
        editorSizes.Add(DesiredSizeWithMargin(InternalChildren[i]));
    }

    double maxEditorWidth = editorSizes.Max(es => es.Width);

    // Equalize count

    while (editorSizes.Count < labelSizes.Count)
        editorSizes.Add(Size.Empty);

    // Evaluate total height

    double totalLabelEditorPairHeight = labelSizes.Zip(editorSizes)
        .Select(sizes => Math.Max(sizes.First.Height, sizes.Second.Height))
        .Sum();

    // This is required height, regardless of how much space is available
    double resultHeight = totalLabelEditorPairHeight;

    // If space is not constrained, pick as much as labels & editors want. Else, use
    // as much, as is given.
    double resultWidth = double.IsInfinity(availableSize.Width) ? maxLabelWidth + maxEditorWidth : availableSize.Width;

    return new Size(resultWidth, resultHeight);
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Spook