views:

62

answers:

3

Hi, I recognise this is a popular question but I couldn't find anything that answered it exactly, but I apologise if I've missed something in my searches.

I'm trying to create and then measure a control at runtime using the following code (the measurements will then be used to marquee-style scroll the controls - each control is a different size):

Label lb = new Label();
lb.DataContext= task;
Style style = (Style)FindResource("taskStyle");
lb.Style = style;

cnvMain.Children.Insert(0,lb);

width = lb.RenderSize.Width;
width = lb.ActualWidth;
width = lb.Width;

The code creates a Label control and applies a style to it. The style contains my control template which binds to the object "task". When I create the item it appears perfectly, however when I try to measure the control using any of the properties above I get the following results (I step through and inspect each property in turn):

lb.Width = NaN
lb.RenderSize.Width = 0
lb.ActualWidth = 0

Is there any way to get the rendered height and width of a control you create at runtime?

UPDATE:

Sorry for deselecting your solutions as answers. They work on a basic example system I set up, but seemingly not with my complete solution.

I think it may be something to do with the style, so sorry for the mess, but I'm pasting the entire thing here.

First, resources:

    <Storyboard x:Key="mouseOverGlowLeave">
        <DoubleAnimation From="8" To="0" Duration="0:0:1" BeginTime="0:0:2" Storyboard.TargetProperty="GlowSize" Storyboard.TargetName="Glow"/>
    </Storyboard>

    <Storyboard x:Key="mouseOverTextLeave">
        <ColorAnimation From="{StaticResource buttonLitColour}" To="Gray" Duration="0:0:3" Storyboard.TargetProperty="Color" Storyboard.TargetName="ForeColour"/>
    </Storyboard>

    <Color x:Key="buttonLitColour" R="30" G="144" B="255" A="255" />

    <Storyboard x:Key="mouseOverText">
        <ColorAnimation From="Gray" To="{StaticResource buttonLitColour}" Duration="0:0:1" Storyboard.TargetProperty="Color" Storyboard.TargetName="ForeColour"/>
    </Storyboard>

And the style itself:

    <Style x:Key="taskStyle" TargetType="Label">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate>
                    <Canvas x:Name="cnvCanvas">
                        <Border  Margin="4" BorderBrush="Black" BorderThickness="3" CornerRadius="16">
                            <Border.Background>
                                <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                                    <GradientStop Offset="1" Color="SteelBlue"/>
                                    <GradientStop Offset="0" Color="dodgerBlue"/>
                                </LinearGradientBrush>
                            </Border.Background>
                            <DockPanel Margin="8">
                                <DockPanel.Resources>
                                    <Style TargetType="TextBlock">
                                        <Setter Property="FontFamily" Value="corbel"/>
                                        <Setter Property="FontSize" Value="40"/>
                                    </Style>
                                </DockPanel.Resources>
                                <TextBlock VerticalAlignment="Center" Margin="4" Text="{Binding Path=Priority}"/>
                                <DockPanel DockPanel.Dock="Bottom">
                                    <Border Margin="4" BorderBrush="SteelBlue" BorderThickness="1" CornerRadius="4">
                                        <TextBlock Margin="4" DockPanel.Dock="Right" Text="{Binding Path=Estimate}"/>
                                    </Border>
                                    <Border Margin="4" BorderBrush="SteelBlue" BorderThickness="1" CornerRadius="4">
                                        <TextBlock Margin="4" DockPanel.Dock="Left" Text="{Binding Path=Due}"/>
                                    </Border>
                                </DockPanel>
                                <DockPanel DockPanel.Dock="Top">
                                    <Border Margin="4" BorderBrush="SteelBlue" BorderThickness="1" CornerRadius="4">
                                        <TextBlock  Margin="4" Foreground="LightGray" DockPanel.Dock="Left" Text="{Binding Path=List}"/>
                                    </Border>
                                    <Border Margin="4" BorderBrush="SteelBlue" BorderThickness="1" CornerRadius="4">
                                        <TextBlock Margin="4" Foreground="White" DockPanel.Dock="Left" Text="{Binding Path=Name}"/>
                                    </Border>
                                </DockPanel>
                            </DockPanel>
                        </Border>
                    </Canvas>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Also, the code I'm using:

Label lb = new Label(); //the element I'm styling and adding
  lb.DataContext= task; //set the data context to a custom class
  Style style = (Style)FindResource("taskStyle"); //find the above style
  lb.Style = style;

  cnvMain.Children.Insert(0,lb); //add the style to my canvas at the top, so other overlays work
  lb.UpdateLayout(); //attempt a full layout update - didn't work
  Dispatcher.Invoke(DispatcherPriority.Render, new Action(() =>
  {
      //Synchronize on control rendering
      double width = lb.RenderSize.Width;
      width = lb.ActualWidth;
      width = lb.Width;
  })); //this didn't work either
  lb.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); //nor did this
  double testHeight = lb.DesiredSize.Height;
  double testWidth = lb.RenderSize.Width; //nor did this
  width2 = lb.ActualWidth; //or this
  width2 = lb.Width; //or this

//positioning for my marquee code - position off-screen to start with
  Canvas.SetTop(lb, 20);
  Canvas.SetTop(lb, -999);

//tried it here too, still didn't work
  lb.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
  testHeight = lb.DesiredSize.Height;
//add my newly-created label to a list which I access later to operate the marquee
  taskElements.AddLast(lb);
//still didn't work
  lb.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
  testHeight = lb.DesiredSize.Height;
//tried a mass-measure to see if that would work - it didn't
  foreach (UIElement element in taskElements)
  {
      element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
  }

Each time any of those is called the value is returned as either 0 or not a number, yet when the label is rendered it clearly has a size as it is visible. I have tried running this code from a button press when the label is visible on-screen, but that produces the same results.

Is it because of the style I'm using? Databindings perhaps? Is what I've done wrong blindingly obvious or do the WPF Gremlins just really hate me?

Second Update:

After further searching I've discovered that Measure() only applies to the original element size. If a control template modifies this by actually adding controls then the best method would probably be to measure each one, but I admit that's more than a little messy.

The compiler must have some way of measuring all of the contents of a control as it must use that to place items in, for example, stack-panels. There must be some way to access it, but for now I'm entirely out of ideas.

+1  A: 

I am pretty new to WPF, but here's my understanding.

When you update or create controls programatically, there is a non-obvious mechanism that is also firing (for a beginner like me anyway - although I've done windows message processing before, this caught me out...). Updates and creation of controls queue associated messages onto the UI dispatch queue, where the important point is that these will get processed at some point in the future. Certain properties depend on these messages being processed e.g. ActualWidth. The messages in this case cause the control to be rendered and then the properties associated with a rendered control to be updated.

It is not obvious when creating controls programatically that there is asynchronous message processing happening and that you have to wait for these messages to be processed before some of the properties will have been updated e.g. ActualWidth.

If you wait for existing messages to be processed before accessing ActualWidth, then it will have been updated:

    //These operations will queue messages on dispatch queue
    Label lb = new Label();
    canvas.Children.Insert(0, lb);

    //Queue this operation on dispatch queue
    Dispatcher.Invoke(DispatcherPriority.Render, new Action(() =>
    {
        //Previous messages associated with creating and adding the control
        //have been processed, so now this happens after instead of before...
        double width = lb.RenderSize.Width;
        width = lb.ActualWidth;
        width = lb.Width;    
    }));

Update

In response to your comment. If you wish to add other code, you can organize your code as follows. The important bit is that the dispatcher call ensures that you wait until the controls have been rendered before your code that depends on them being rendered executes:

    Label lb = new Label();
    canvas.Children.Insert(0, lb);

    Dispatcher.Invoke(DispatcherPriority.Render, new Action(() =>
    {
        //Synchronize on control rendering
    }));

    double width = lb.RenderSize.Width;
    width = lb.ActualWidth;
    width = lb.Width;

    //Other code...
chibacity
Ah that's excellent thankyou.Is there an event of some sort, or boolean value to check to find out when the existing messages have been processed? I have over code that relies on checking these values as well that needs to run sequentially after control creation. If not I'm sure I can find a solution of some sort.
Nick Udell
+1  A: 

The control won't have a size until WPF does a layout pass. I think that happens asynchronously. If you're doing this in your constructor, you can hook the Loaded event -- the layout will have happened by then, and any controls you added in the constructor will have been sized.

However, another way is to ask the control to calculate what size it wants to be. To do that, you call Measure, and pass it a suggested size. In this case, you want to pass an infinite size (meaning the control can be as large as it likes), since that's what Canvas is going to do in the layout pass:

lb.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Debug.WriteLine(lb.DesiredSize.Width);

The call to Measure doesn't actually change the control's Width, RenderSize, or ActualWidth. It just tells the Label to calculate what size it wants to be, and put that value in its DesiredSize (a property whose sole purpose is to hold the result of the last Measure call).

Joe White
That's a very elegant solution, but both are workable for different situations, so I'll keep them both set as answers. I think this one fits my problem best, thank you.
Nick Udell
A: 

Solved it!

The issue was in my XAML. At the highest level of my label's template there was a parent canvas that had no height or width field. Since this did not have to modify its size for its children it was constantly set to 0,0. By removing it and replacing the root node with a border, which must resize to fit its children, the height and width fields are updated and propagated back to my code on Measure() calls.

Nick Udell