Named control exists on Windows 10 but not on Windows 8.1

133 Views Asked by At

I have the following code in a Windows 8.1 Store App. This code runs perfectly fine on Windows 10 but crashes on Windows 8.1. The second named control in MainPage.xaml.cs is null on Win 8.1 but not on Windows 10. It's not a timing issue as the named control still won't be populated in any subsequent event handler following the page load. What on earth is going on here?

To summarize, I have a ContentControl with a ContentPresenter defined in its Template. That ContentControl is then instantiated on a page, with a named child control (using "x:Name") as its Content. On Windows 10, that named control exists in code-behind. On Windows 8.1 it is null

MyUserControl1.xaml

<ContentControl
x:Class="App1.MyUserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App1"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<ContentControl.Template>
    <ControlTemplate>
        <ContentPresenter Content="{Binding Content, RelativeSource={RelativeSource TemplatedParent}}" />
    </ControlTemplate>
</ContentControl.Template>

MainPage.xaml

<Page
x:Class="App1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App1"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <TextBlock x:Name="TextBlock1" 
               VerticalAlignment="Center" 
               HorizontalAlignment="Center" 
               TextAlignment="Center"
               FontSize="50" 
               TextWrapping="Wrap" />

    <local:MyUserControl1 Grid.Column="1">
        <TextBlock x:Name="TextBlock2" 
                   VerticalAlignment="Center" 
                   HorizontalAlignment="Center" 
                   TextAlignment="Center"
                   FontSize="50" 
                   TextWrapping="Wrap" />
    </local:MyUserControl1>

</Grid>

MainPage.xaml.cs

using Windows.UI.Xaml.Controls;

namespace App1
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            TextBlock1.Text = "This works";
            TextBlock2.Text = "This does not work because TextBlock2 is null";
        }
    }
}
1

There are 1 best solutions below

5
Jerry Nixon On

Of course you cannot reference this. Your TextBlock2 is explicitly being set BY YOU to be the content of another control fundamentally out of scope. After rendering is complete, your TextBlock2 is no longer a child of your MainPage but instead a child of the ControlTemplate in your UserControl. Windows 10 is behaving EXACTLY how it should, and it appears you have discovered a bug in the Windows 8 rendering engine, if it worked.

One

There are a few workarounds. The first is the textbook approach of adding a property to your UserControl that adds access to this control. Because you are allowing the content to be dynamic, the operation inside that property (or method) would also need to be dynamic. Something like GetControl<TextBlock>("TextBlock1") which could hunt for you.

public bool TryGetControl<T>(string name, out T control)
{
    try
    {
        var children = RecurseChildren(this.MyUserControl);
        control = children
            .Where(x => Equals(x.Name, name))
            .OfType<T>()
            .First();
        return true;
    }
    catch
    {
        control = default(T);
        return false;
    }
}

public List<Control> RecurseChildren(DependencyObject parent)
{
    var list = new List<Control>();
    var count = VisualTreeHelper.GetChildrenCount(parent);
    var children = Enumerable.Range(0, count - 1)
        .Select(x => VisualTreeHelper.GetChild(parent, x));
    list.AddRange(children.OfType<Control>());
    foreach (var child in children)
    {
        list.AddRange(RecurseChildren(child));
    }
    return list;
}

Two

The second thing you could do is simply hunt for the control through the child hierarchy of the UserControl from the page itself. The logic would be the same as number one (above) but it would execute inside the page and not be part of your UserControl logic. You already do this sort of thing when you need to find the ScrollViewer in a ListView or maybe the inner Border in a Button for some reason.

It turns out I have already explained this in a blog article you can use as reference: http://blog.jerrynixon.com/2012/09/how-to-access-named-control-inside-xaml.html

Three

And here's a third, perhaps simplest way to do it. Handle the Loaded event of TextBlock1 in your MainPage and set a field to the value of the sender in the handler method. You can even cast it to TextBlock so everything is typed. This gives you simple access, but has the potential downside of timing. If you try to access the field value before it is set, you might find it is null. But, in most cases, this works the easiest.

So, that's three ways to handle it. I think it is very important that you recognize that this is EXPECTED behavior since a XAML element can have only one parent and you are setting that parent through the Content property of your ContentPresenter. That being said, it might be expected behavior, but it is not obviously intuitive.

Best of luck.