At what point is a UIElement loaded?

703 views Asked by At

I have the following code:

<Window x:Class="Demo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Canvas Name="Canvas_Main" />
</Window>

public partial class MainWindow : Window
{

    public MainWindow()
    {
        InitializeComponent();
        Loaded += MainWindow_Loaded;
    }

    private void MainWindow_Loaded(Object sender, RoutedEventArgs e)
    {
        Rectangle lastRectangle = null;
        Random random = new Random(0);
        for (Int32 counter = 0; counter < 5; counter++)
        {
            Rectangle rectangle = new Rectangle();
            rectangle.Fill = Brushes.Blue;
            rectangle.Width = random.Next(100, 200);
            rectangle.Height = counter * 100;
            Canvas_Main.Children.Add(rectangle);
            if (lastRectangle == null) {
                Canvas.SetLeft(rectangle, 0);
                Canvas.SetTop(rectangle, 0);
            }
            else
            {
                Canvas.SetLeft(rectangle, lastRectangle.ActualWidth);
                Canvas.SetTop(rectangle, 0);
            }
            lastRectangle = rectangle;
        }
    }

}

This isn't working as expected (laying each rectangle diagonally next to each other), as lastRectangle.ActualWidth is 0. As I understand things from this answer, it is because lastRectangle has not been measured and arranged.

I am curious, at what point would the measuring and arranging be done, if not when added to a container that is already visible and loaded?

2

There are 2 answers

1
BionicCode On BEST ANSWER

The Framework.Loaded event of an element is raised after the measure and arrange layout pass, but before the rendering of the element.

The complete layout pass is initialized when UIElement.InvalidateMeasure for an asynchronous layout pass or UIElement.UpdateLayout for a synchronous layout pass was invoked on the element.

In your scenario the Window.Loadedevent handler is invoked, which means that the Window and the Canvas are both loaded (but not rendered).
Now you start to add new UIElement elements to the Canvas.

Canvas.Children.Add should invoke the InvalidateMeasure method. Because InvalidateMeasure triggers an asynchronous layout pass, the Canvas and therefore the current child element will be enqueued into the layout queue and the context continues execution (adding more rectangles).
Because there is already a pending layout pass due to the freshly added element, you should avoid calling UIElement.Measure manually (this are recursive calls and quite expensive when considering performance).

Once the context has completed, the elements in the queue, that are waiting for their layout pass and final rendering, will be handled and MeasureOverride and ArrangeOverride are invoked recursively on those elements (Canvas and its children).
As a result, UIElement.RenderSize can be calculated by the layout system.
At this moment, the new FrameworkElement.ActualWidth will be available.

This is the moment the FrameworkElement.Loaded event of the added elements (the rectangles) is finally raised.

To solve your problem you have to either use Rectangle.Width instead
or wait for each Rectangle.Laoded event before adding the next:

private int ShapeCount { get; set; }
private const int MaxShapes = 5;
private Point ShapeBPosition { get; set; }

private void MainWindow_Loaded(Object sender, RoutedEventArgs e)
{
  this.ShapePosition = new Point();
  AddRectangle(this.ShapePosition);
}

private void AddRectangle(Point position)
{
  Random random = new Random();
  Rectangle rectangle = new Rectangle();
  rectangle.Fill = Brushes.Blue;

  rectangle.Width = random.Next(100, 200);
  rectangle.Height = ++this.ShapeCount * 100;

  Canvas_Main.Children.Add(rectangle);
  Canvas.SetLeft(rectangle, position.X);
  Canvas.SetTop(rectangle, position.Y);

  rectangle.Loaded += OnRectangleLoaded;
}

private void OnRectangleLoaded(object sender, RoutedEventArgs e)
{
  var rectangle = sender as Rectangle;

  rectangle.Loaded -= OnRectangleLoaded;
  if (this.ShapeCount == MainWindow.MaxShapes)
  {
    return;
  }

  // this.ShapePosition is struct => modify copy
  Point position = this.ShapePosition; 
  position.Offset(rectangle.ActualWidth, 0);
  this.ShapePosition = position;
  AddRectangle(this.ShapePosition);
}
2
Artfunkel On

The measuring and arranging has been completed for the window. But you are now creating new child controls, which will ordinarily not be measured/arranged until the window's next layout pass.

You can force this to happen immediately by calling the Measure method of each rectangle at the end of the loop.