James Ko
James Ko

Reputation: 34499

Windows Phone: XAML Style 'resets' itself when applied in C#

EDIT: I have found a minimal way to reproduce the problem. Starting a new Windows Phone app template in Visual Studio and adding this to your MainPage.xaml:

<Page.Resources>
    <Style x:Name="TileStyle" TargetType="Button">
        <Setter Property="BorderThickness" Value="0,0,0,0" />
    </Style>
</Page.Resources>

<Button x:Name="btn" Style="{StaticResource TileStyle}" />

and this to your code-behind file (in the OnNavigatedTo event handler):

btn.Style = TileStyle;

reproduces the problem when you navigate to the page.

Original text: I am trying to create a Windows Phone application that is a minimal version of Whack-A-Mole. On my Page for the game, I have two Styles for Buttons written in XAML, which represent a hole being either occupied or not occupied.

<Page.Resources>
    <Style x:Name="TileStyle" TargetType="Button">
        <Setter Property="BorderThickness" Value="0,0,0,0" />
        <Setter Property="Width" Value="125"/>
        <Setter Property="Height" Value="125" />
    </Style>

    <Style x:Name="CircleStyle" TargetType="Button"
           BasedOn="{StaticResource TileStyle}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Grid>
                        <Rectangle Fill="{TemplateBinding Background}" />
                        <Image Source="/Assets/white_circle.png" />
                        <ContentPresenter HorizontalAlignment="Center"
                                          VerticalAlignment="Center"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</Page.Resources>

<!--later on...-->

<Grid>
    <Button Style="{StaticResource TileStyle}" Click="Button_Click"/>
    <Button Style="{StaticResource TileStyle}" Click="Button_Click"/>
    <Button Style="{StaticResource TileStyle}" Click="Button_Click"/>
    ...
</Grid>

The first style is essentially just a blank, black, 125x125 square; you can only tell that it's there by clicking on it. The second style is BasedOn that one, and it displays a white_circle.png in place of a blank square.

In my code-behind C# file, I have two methods that utilize these styles: one is called when the Page loads and the other is called when a Button is clicked.

protected async void OnNavigatedTo(NavigationEventArgs e)
            while (!hasUserWonGame)
                //sleeps for 1-3 secs
                //decides where circle should go at by generating random int
            b.Style = CircleStyle; //this works fine!
            //more logic, waits
            if (!hasUserWonRound)
                b.Style = TileStyle; //reverts to TileStyle (this does not work!)

    private void Button_Click(object sender, RoutedEventArgs e)
        //decides if user is clicking on a circle
        if (areTilesDisplayingCircle[index - 1])
            //user has won the round!
            hasUserWonRound = true;
            b.Style = TileStyle; //this also does not work!
            //more logic, updates score, decides if user has won the entire game

When the user first navigates to the page, the Buttons are all black and invisible, as they should be. The white circle appears in the space as intended. However, when it disappears from the spot, the circle leaves behind a Button with no Style applied. (See image.)

buttons

Upvotes: 0

Views: 193

Answers (1)

Peter Duniho
Peter Duniho

Reputation: 70652

Thank you for providing the minimal repro case. That makes it very easy to explain what's going on (well, at least in that case…one hopes that your real-world scenario actually is similar, and this does seem likely).

The problem stems from your use of x:Name instead of x:Key for your style resources. In this case, it does not do what you think or would hope it does. In particular, while the compiler does create the field named TileStyle based on the specified x:Name value for the resource, and while the XAML compiler permits the use of x:Name in lieu of a proper key specificed by x:Key, the two don't connect.

The compiler-generated code to retrieve the value for the TileStyle field uses a mechanism incompatible with the resource dictionary, i.e. it calls the FindName() method, passing the name you provided. But that method is for finding named objects within the FrameworkElement's object graph; it won't (and is not intended to) find objects in the resource dictionary.

Basically, the compiler sees x:Name and happily emits its boilerplate for implementing a field backing an element. It's just blindly following the rule for implementing x:Name, which turns out not to work for resources.

So what happens when you call FindName(), passing a name that doesn't exist? It returns null. So the TileStyle field gets (or rather keeps, since that's the default) the value of null.

And what happens when you set a Button object's Style property to null? It just resets everything to the defaults! The Button initially looks fine (well, in your real-world scenario) because the {StaticResource TileStyle} syntax is the correct syntax for dealing with resources, and x:Name is permitted as a substitute for the resource x:Key attribute.

In your minimal repro example, the most straight-forward fix is to initialize the resources correctly (use of the x:Name syntax is for a specific Storyboard-based scenario and is not generally the correct way to do it), and then to explicitly implement your TileStyle field:

<Page.Resources>
    <Style x:Key="TileStyle" TargetType="Button">
        <Setter Property="BorderThickness" Value="0,0,0,0" />
    </Style>
</Page.Resources>

Then in your code-behind:

partial class MainPage : Page
{
    private Style TileStyle;

    public MainPage()
    {
        InitializeComponent();

        TileStyle = (Style)this.Resources["TileStyle"];

        // etc.
    }
}

With that, you should now have non-null values for your style fields, and assigning them to the Style properties should update the style correctly instead of resetting it.

That said, it's not clear to me that, while the above is clearly the solution for your minimal repro case, it would apply to your real-world scenario. In particular, in your question you claim that assigning the value of CircleStyle to a Button's Style property does work. But assuming the CircleStyle field is initialized the same as the TileStyle field, it's not clear why that would work while assigning TileStyle would not.

I.e. I would expect the CircleStyle field to be null also, but it obviously cannot be if assigning it does change the style to what you want. And if it's not null, then why would TileStyle be null? But, I can't answer anything about that without a good, minimal, complete code example that addresses that scenario precisely.


Finally, I will note that there is probably a better way to do this than to hard-code the styles in the code-behind. Unfortunately, I'm not familiar enough with the Phone API to know what that might be.

In WPF, I would use a StyleSelector or Trigger to change the object appearance based on some bound property in an underlying model object. But after a fair amount of time experimenting, I found I just do not yet know enough about the Phone API to know how those would work. The Trigger class appears to either not even be supported, or at least not be permitted in a Style.Triggers collection, and I wound up falling down a rabbit hole trying to figure out how to do a Grid-based ItemsControl in Phone (in WPF it's not hard, but there's a little trick involved in binding to the Grid.Row and Grid.Column attached properties that doesn't work in Phone because apparently Phone doesn't support using Binding in the Value property of a style's Setter).

Basically, there's a ton missing from the Silverlight/Phone/WinRT implementation of a XAML-based API, as compared to WPF. My previous experience with WPF isn't helping me much with the above issues because so much of the features I'd rely on in WPF to implement these behaviors just doesn't exist in the other APIs (shame on Microsoft!).

In the meantime, I hope that the above code-behind-based approach does address your specific issue. If it doesn't, then you will need to post a better, but still minimal and complete code example.

Upvotes: 1

Related Questions