Question

Concise question:
Is it possible to measure the contents of an unselected Tab?

Problem Summary:
Imagine you have a TabControl with two tabs. The first tab contains a Grid with a TextBlock in it. You select the second tab. After some time passes, the TextBlock's Text field changes to a very long string. You want to measure the size of the first tab's content before it is visible.

If you simply do a measure, it will not catch the fact that the string has changed - you can see in the WPF Visualizer that the TextBlock has the new string in its Text field, but the TextBlock refuses to re-measure. If you directly measure the string, you can detect the new desired size, but this is not a good solution; I want to be able to measure the total content of the first tab, not just strings.

Anyway, apologies for the long example code, this is hard to reduce further. When the window appears, the code waits for two seconds and then swaps tabs. It then changes the string. The measure loop changes the background to blue when it detects the string size change, and red when it detects the actual content size change. Red only happens when you switch tabs, but I'd like to be able to get Red to occur without having to switch back to the first tab.

Code Behind:

    public MainWindow()
    {
        InitializeComponent();

        // this worker waits a bit so the first tab renders, 
        // then it switches tabs and changes the string.
        BackgroundWorker worker = new BackgroundWorker();
        worker.DoWork += delegate
        {
            Thread.Sleep(2000);
            Application.Current.Dispatcher.BeginInvoke(new Action(delegate
            {
                TestTabControl.SelectedIndex = 1;
                TestBlock.Text = "This is a long string that ought to change the measure of the textblock to sizes never before seen by human eyes";
                this.Background = Brushes.Green;
            }));
        };
        worker.RunWorkerAsync();

        // This worker constantly measures the text block 
        worker = new BackgroundWorker();
        worker.DoWork += delegate
        {
            while (true)
            {
                if (Application.Current != null)
                {
                    Application.Current.Dispatcher.BeginInvoke(new Action(delegate
                    {
                        MeasureFirstTab();
                    }));
                }
                else
                {
                    break;
                }
                Thread.Sleep(100);
            }
        };
        worker.RunWorkerAsync();
    }

    // Turn the background red when the tab width changes
    public void MeasureFirstTab()
    {
        FirstTabContent.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
        if (MeasureString(TestBlock.Text).Width > 500)
        {
            this.Background = Brushes.Blue;
        }

        if (FirstTabContent.DesiredSize.Width > 500)
        {
            this.Background = Brushes.Red;
        }  
    }

    private Size MeasureString(string candidate)
    {
        var formattedText = new FormattedText(
            candidate,
            CultureInfo.CurrentUICulture,
            FlowDirection.LeftToRight,
            new Typeface(TestBlock.FontFamily, TestBlock.FontStyle, TestBlock.FontWeight, TestBlock.FontStretch),
            TestBlock.FontSize,
            Brushes.Black);

        return new Size(formattedText.Width, formattedText.Height);
    }
}

XAML:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <TabControl Name="TestTabControl">

        <TabItem Header="Changes">
            <Grid Name="FirstTabContent">
                <TextBlock Name="TestBlock" Text="small"/>
            </Grid>
        </TabItem>
        <TabItem Header="Short">                
            <TextBlock Text="Short"/>                
        </TabItem>
    </TabControl>        
</Grid>
Was it helpful?

Solution

Answering for posterity and for anyone who googles this in the future.

As noted in the comment to my OP, TabControl virtualizes unselected tabs, which apparently results in the measure pass only calculating the previous desired size.

So, the solution is to remove the content from the tab item, add it to a Grid, measure the content, and then add it back to the tab.

Here's the new measure code:

    // Turn the background red when the tab width changes
    public void MeasureFirstTab()
    {
        // Remember the previous selected item
        object selectedItem = TestTabControl.SelectedItem;
        Grid measureBox = new Grid();
        UIElement content;

        // Iterate through all items
        foreach (TabItem obj in TestTabControl.Items)
        {
            // Get the tab content into the grid
            TestTabControl.SelectedItem = obj;
            content = (UIElement)obj.Content;
            obj.Content = null;
            measureBox.Children.Add(content);

            // Measure the content
            content.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));

            if (content.DesiredSize.Width > 500)
            {
                this.Background = Brushes.Red;
            }

            // Return the content to its rightful owner
            measureBox.Children.Clear();
            obj.Content = content;
        }

        // Reset the tab control
        TestTabControl.SelectedItem = selectedItem;

    }
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top