Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/Controls/src/Core/Setter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ internal void Apply(BindableObject target, SetterSpecificity specificity)
var targetObject = target;

if (!string.IsNullOrEmpty(TargetName) && target is Element element)
targetObject = element.FindByName(TargetName) as BindableObject ?? throw new XamlParseException($"Cannot resolve '{TargetName}' as Setter Target for '{target}'.");
targetObject = FindTargetByName(element, TargetName) ?? throw new XamlParseException($"Cannot resolve '{TargetName}' as Setter Target for '{target}'.");

if (Property == null)
return;
Expand All @@ -90,7 +90,7 @@ internal void UnApply(BindableObject target, SetterSpecificity specificity)
var targetObject = target;

if (!string.IsNullOrEmpty(TargetName) && target is Element element)
targetObject = element.FindByName(TargetName) as BindableObject ?? throw new ArgumentNullException(nameof(targetObject));
targetObject = FindTargetByName(element, TargetName) ?? throw new ArgumentNullException(nameof(targetObject));

if (Property == null)
return;
Expand All @@ -100,5 +100,25 @@ internal void UnApply(BindableObject target, SetterSpecificity specificity)
targetObject.RemoveDynamicResource(Property, specificity);
targetObject.ClearValue(Property, specificity);
}

static BindableObject FindTargetByName(Element element, string name)
{
// Try standard lookup first (works for same or child namescopes)
if (element.FindByName(name) is BindableObject target)
return target;

// Walk up parent tree to handle ControlTemplate namescope boundaries
var current = element.Parent;
while (current != null)
{
var namescope = current.GetNameScope();
if (namescope?.FindByName(name) is BindableObject parentTarget)
return parentTarget;

current = current.Parent;
}

return null;
}
}
}
51 changes: 51 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue26977.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issue26977">
<ContentPage.ControlTemplate>
<ControlTemplate>
<ContentPresenter/>
</ControlTemplate>
</ContentPage.ControlTemplate>

<VerticalStackLayout x:Name="RootStackLayout"
Padding="30,0"
Spacing="25"
VerticalOptions="Center">
<HorizontalStackLayout Spacing="10">
<Label Text="Active State"
VerticalOptions="Center"/>
<Switch x:Name="StateSwitch"
AutomationId="StateSwitch"
Toggled="OnStateSwitchToggled"/>
</HorizontalStackLayout>

<Label x:Name="TargetLabel"
AutomationId="TargetLabel"/>

<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="Group1">
<VisualState Name="State1">
<VisualState.Setters>
<Setter TargetName="TargetLabel"
Property="Label.Text"
Value="Value from Setter in State1"/>
<Setter TargetName="TargetLabel"
Property="Label.BackgroundColor"
Value="Orange"/>
</VisualState.Setters>
</VisualState>
<VisualState Name="State2">
<VisualState.Setters>
<Setter TargetName="TargetLabel"
Property="Label.Text"
Value="Value from Setter in State2"/>
<Setter TargetName="TargetLabel"
Property="Label.BackgroundColor"
Value="Aqua"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</VerticalStackLayout>
</ContentPage>
20 changes: 20 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue26977.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 26977, "Setter.TargetName + ControlTemplate crash", PlatformAffected.Android)]
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PlatformAffected should be set to PlatformAffected.All instead of PlatformAffected.Android. According to the PR description, this fix was tested on Android, Windows, iOS, and Mac. The issue is a VisualStateManager namescope resolution problem that affects all platforms where ControlTemplates create namescope boundaries, not just Android.

Suggested change
[Issue(IssueTracker.Github, 26977, "Setter.TargetName + ControlTemplate crash", PlatformAffected.Android)]
[Issue(IssueTracker.Github, 26977, "Setter.TargetName + ControlTemplate crash", PlatformAffected.All)]

Copilot uses AI. Check for mistakes.
public partial class Issue26977 : ContentPage
{
public Issue26977()
{
InitializeComponent();
}

void OnStateSwitchToggled(object sender, ToggledEventArgs e)
{
if (sender is not Switch stateSwitch)
{
return;
}

VisualStateManager.GoToState(RootStackLayout, stateSwitch.IsToggled ? "State1" : "State2");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue26977 : _IssuesUITest
{
public Issue26977(TestDevice device) : base(device) { }

public override string Issue => "Setter.TargetName + ControlTemplate crash";

[Test]
[Category(UITestCategories.Page)]
public void SetterTargetNameWithControlTemplateShouldNotCrash()
{
App.WaitForElement("StateSwitch");
App.Tap("StateSwitch");

var normalText = App.FindElement("TargetLabel").GetText();
Assert.That(normalText, Is.EqualTo("Value from Setter in State1"),
"Label text should change to 'Value from Setter in State1' after the switch is toggled");
}
}
Loading