Skip to content

Commit aad88d6

Browse files
[C] Port the specificity concept to Bindings (#17215)
* [C] Port the specificity concept to Bindings Allow having multiple layers of Bindings, useful for e.g. AppThemeBinding, Style, VSM - fixes #16538 * test for 17354
1 parent 8ab6da9 commit aad88d6

7 files changed

Lines changed: 237 additions & 30 deletions

File tree

src/Controls/src/Core/BindableObject.cs

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,19 @@ public void RemoveBinding(BindableProperty property)
253253
{
254254
BindablePropertyContext context = GetContext(property ?? throw new ArgumentNullException(nameof(property)));
255255

256-
if (context?.Binding != null)
257-
RemoveBinding(property, context);
256+
var specificity = SetterSpecificity.FromBinding;
257+
if (context != null && context.Bindings.Count > 0)
258+
specificity = context.Bindings.Last().Key;
259+
260+
RemoveBinding(property, specificity);
261+
}
262+
263+
internal void RemoveBinding(BindableProperty property, SetterSpecificity specificity)
264+
{
265+
BindablePropertyContext context = GetContext(property ?? throw new ArgumentNullException(nameof(property)));
266+
267+
if (context != null && context.Bindings.Count > 0)
268+
RemoveBinding(property, context, specificity);
258269
}
259270

260271
/// <summary>
@@ -286,11 +297,24 @@ internal void SetBinding(BindableProperty targetProperty, BindingBase binding, S
286297
context.Values[SetterSpecificity.FromBinding] = kvp.Value;
287298
}
288299

289-
context.Binding?.Unapply();
290-
context.BindingSpecificity = specificity;
300+
BindingBase oldBinding = null;
301+
SetterSpecificity oldSpecificity = default;
302+
if (context.Bindings.Count > 0)
303+
{
304+
var b_p = context.Bindings.Last();
305+
oldSpecificity = b_p.Key;
306+
oldBinding = b_p.Value;
307+
}
308+
309+
if (oldBinding != null && specificity.CompareTo(oldSpecificity) < 0)
310+
{
311+
context.Bindings[specificity] = binding;
312+
return;
313+
}
314+
315+
oldBinding?.Unapply();
291316

292-
BindingBase oldBinding = context.Binding;
293-
context.Binding = binding ?? throw new ArgumentNullException(nameof(binding));
317+
context.Bindings[specificity] = binding ?? throw new ArgumentNullException(nameof(binding));
294318

295319
targetProperty.BindingChanging?.Invoke(this, oldBinding, binding);
296320

@@ -319,9 +343,11 @@ public static void SetInheritedBindingContext(BindableObject bindable, object va
319343
if (bpContext != null && oldContext == null)
320344
oldContext = bpContext.Values.LastOrDefault().Value;
321345

322-
if (bpContext != null && bpContext.Binding != null)
346+
var binding = bpContext?.Bindings.Values.LastOrDefault();
347+
348+
if (binding != null)
323349
{
324-
bpContext.Binding.Context = value;
350+
binding.Context = value;
325351
bindable._inheritedContext = null;
326352
}
327353
else
@@ -372,12 +398,7 @@ protected virtual void OnPropertyChanging([CallerMemberName] string propertyName
372398
protected void UnapplyBindings()
373399
{
374400
foreach (var context in _properties.Values)
375-
{
376-
if (context.Binding == null)
377-
continue;
378-
379-
context.Binding.Unapply();
380-
}
401+
context.Bindings.Values.LastOrDefault()?.Unapply();
381402
}
382403

383404
internal bool GetIsBound(BindableProperty targetProperty)
@@ -386,7 +407,7 @@ internal bool GetIsBound(BindableProperty targetProperty)
386407
throw new ArgumentNullException(nameof(targetProperty));
387408

388409
BindablePropertyContext bpcontext = GetContext(targetProperty);
389-
return bpcontext != null && bpcontext.Binding != null;
410+
return bpcontext != null && bpcontext.Bindings.Count > 0;
390411
}
391412

392413
internal virtual void OnRemoveDynamicResource(BindableProperty property)
@@ -398,6 +419,9 @@ internal virtual void OnSetDynamicResource(BindableProperty property, string key
398419
}
399420

400421
internal void RemoveDynamicResource(BindableProperty property)
422+
=> RemoveDynamicResource(property, SetterSpecificity.DynamicResourceSetter);
423+
424+
internal void RemoveDynamicResource(BindableProperty property, SetterSpecificity specificity)
401425
{
402426
if (property == null)
403427
throw new ArgumentNullException(nameof(property));
@@ -595,7 +619,7 @@ void SetValueActual(BindableProperty property, BindablePropertyContext context,
595619
if ((context.Attributes & BindableContextAttributes.IsDynamicResource) != 0 && clearDynamicResources)
596620
RemoveDynamicResource(property);
597621

598-
BindingBase binding = context.Binding;
622+
BindingBase binding = context.Bindings.Values.LastOrDefault();
599623

600624
if (!silent && (!sameValue || raiseOnEqual))
601625
{
@@ -618,15 +642,18 @@ internal void ApplyBindings(bool skipBindingContext, bool fromBindingContextChan
618642
for (int i = 0, propLength = prop.Length; i < propLength; i++)
619643
{
620644
BindablePropertyContext context = prop[i];
621-
BindingBase binding = context.Binding;
645+
var kvp = context.Bindings.LastOrDefault();
646+
var specificity = kvp.Key;
647+
var binding = kvp.Value;
648+
622649
if (binding == null)
623650
continue;
624651

625652
if (skipBindingContext && ReferenceEquals(context.Property, BindingContextProperty))
626653
continue;
627654

628655
binding.Unapply(fromBindingContextChanged: fromBindingContextChanged);
629-
binding.Apply(BindingContext, this, context.Property, fromBindingContextChanged, context.BindingSpecificity);
656+
binding.Apply(BindingContext, this, context.Property, fromBindingContextChanged, specificity);
630657
}
631658
}
632659

@@ -669,13 +696,31 @@ BindablePropertyContext CreateAndAddContext(BindableProperty property)
669696
[MethodImpl(MethodImplOptions.AggressiveInlining)]
670697
BindablePropertyContext GetOrCreateContext(BindableProperty property) => GetContext(property) ?? CreateAndAddContext(property);
671698

672-
void RemoveBinding(BindableProperty property, BindablePropertyContext context)
699+
void RemoveBinding(BindableProperty property, BindablePropertyContext context, SetterSpecificity specificity)
673700
{
674-
context.Binding.Unapply();
701+
var count = context.Bindings.Count;
675702

676-
property.BindingChanging?.Invoke(this, context.Binding, null);
703+
if (count == 0)
704+
return; //used to fail;
705+
706+
var currentbinding = context.Bindings.Values.Last();
707+
var binding = context.Bindings[specificity];
708+
var isCurrent = binding == currentbinding;
709+
710+
if (isCurrent)
711+
{
712+
binding.Unapply();
713+
714+
currentbinding = null;
715+
if (count > 1)
716+
currentbinding = context.Bindings.Values.ElementAt(count-2);
717+
718+
property.BindingChanging?.Invoke(this, binding, currentbinding);
719+
720+
currentbinding?.Apply(BindingContext, this, property, false, context.Bindings.Keys.ElementAt(count - 2));
721+
}
677722

678-
context.Binding = null;
723+
context.Bindings.Remove(specificity);
679724
}
680725

681726
/// <summary>
@@ -740,9 +785,7 @@ internal class BindablePropertyContext
740785
{
741786
public BindableContextAttributes Attributes;
742787

743-
//TODO should be a list of bindings/specificity
744-
public BindingBase Binding;
745-
public SetterSpecificity BindingSpecificity = SetterSpecificity.FromBinding;
788+
public SortedList<SetterSpecificity, BindingBase> Bindings = new();
746789

747790
public Queue<SetValueArgs> DelayedSetters;
748791
public BindableProperty Property;

src/Controls/src/Core/BindableObjectExtensions.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System;
33
using System.Collections.Generic;
44
using Microsoft.Maui.Graphics;
5+
using System.Linq;
56

67
namespace Microsoft.Maui.Controls
78
{
@@ -11,11 +12,13 @@ public static class BindableObjectExtensions
1112
internal static void RefreshPropertyValue(this BindableObject self, BindableProperty property, object value)
1213
{
1314
var ctx = self.GetContext(property);
14-
if (ctx?.Binding is not null)
15+
if (ctx != null && ctx.Bindings.Count >0)
1516
{
17+
var binding = ctx.Bindings.Last().Value;
18+
1619
// support bound properties
1720
if (!ctx.Attributes.HasFlag(BindableObject.BindableContextAttributes.IsBeingSet))
18-
ctx.Binding.Apply(false);
21+
binding.Apply(false);
1922
}
2023
else
2124
{

src/Controls/src/Core/Setter.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ internal void Apply(BindableObject target, SetterSpecificity specificity)
7171
if (Property == null)
7272
return;
7373

74-
//FIXME: use Specificity everywhere
75-
var fromStyle = specificity.Style > 0;
7674
if (Value is BindingBase binding)
7775
targetObject.SetBinding(Property, binding.Clone(), specificity);
7876
else if (Value is DynamicResource dynamicResource)
@@ -96,7 +94,10 @@ internal void UnApply(BindableObject target, SetterSpecificity specificity)
9694

9795
if (Property == null)
9896
return;
99-
97+
if (Value is BindingBase binding)
98+
targetObject.RemoveBinding(Property, specificity);
99+
else if (Value is DynamicResource dynamicResource)
100+
targetObject.RemoveDynamicResource(Property, specificity);
100101
targetObject.ClearValue(Property, specificity);
101102
}
102103
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
4+
xmlns:local="clr-namespace:Microsoft.Maui.Controls.Xaml.UnitTests"
5+
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Maui16538">
6+
<ContentPage.Resources>
7+
<Color x:Key="Primary">#512BD4</Color>
8+
<Color x:Key="White">White</Color>
9+
<Color x:Key="Gray200">#C8C8C8</Color>
10+
<Color x:Key="Gray600">#404040</Color>
11+
<Color x:Key="Gray950">#141414</Color>
12+
<Style TargetType="Button">
13+
<!--<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Primary}}" />-->
14+
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
15+
<!--<Setter Property="FontFamily" Value="OpenSansRegular"/>
16+
<Setter Property="FontSize" Value="14"/>
17+
<Setter Property="BorderWidth" Value="0"/>
18+
<Setter Property="CornerRadius" Value="8"/>
19+
<Setter Property="Padding" Value="14,10"/>
20+
<Setter Property="MinimumHeightRequest" Value="44"/>
21+
<Setter Property="MinimumWidthRequest" Value="44"/>-->
22+
<Setter Property="VisualStateManager.VisualStateGroups">
23+
<VisualStateGroupList>
24+
<VisualStateGroup x:Name="CommonStates">
25+
<VisualState x:Name="Normal" />
26+
<VisualState x:Name="Disabled">
27+
<VisualState.Setters>
28+
<!--<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />-->
29+
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
30+
</VisualState.Setters>
31+
</VisualState>
32+
<VisualState x:Name="PointerOver" />
33+
</VisualStateGroup>
34+
</VisualStateGroupList>
35+
</Setter>
36+
</Style>
37+
</ContentPage.Resources>
38+
<Button x:Name="button0" IsEnabled="false"/>
39+
</ContentPage>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using Microsoft.Maui.ApplicationModel;
2+
using Microsoft.Maui.Controls.Core.UnitTests;
3+
using Microsoft.Maui.Controls.Shapes;
4+
using Microsoft.Maui.Devices;
5+
using Microsoft.Maui.Graphics;
6+
using Microsoft.Maui.Dispatching;
7+
8+
9+
using NUnit.Framework;
10+
using Microsoft.Maui.UnitTests;
11+
12+
namespace Microsoft.Maui.Controls.Xaml.UnitTests;
13+
14+
public partial class Maui16538
15+
{
16+
17+
public Maui16538() => InitializeComponent();
18+
19+
public Maui16538(bool useCompiledXaml)
20+
{
21+
//this stub will be replaced at compile time
22+
}
23+
24+
[TestFixture]
25+
class Test
26+
{
27+
[SetUp] public void Setup() {
28+
Application.SetCurrentApplication(new MockApplication());
29+
DispatcherProvider.SetCurrent(new DispatcherProviderStub());
30+
}
31+
32+
33+
[TearDown] public void TearDown() => AppInfo.SetCurrent(null);
34+
35+
[Test]
36+
public void VSMandAppTheme([Values(false, true)] bool useCompiledXaml)
37+
{
38+
39+
Application.Current.UserAppTheme = AppTheme.Dark;
40+
var page = new Maui16538(useCompiledXaml);
41+
Button button = page.button0;
42+
Assert.That(button.BackgroundColor, Is.EqualTo(Color.FromHex("404040")));
43+
button.IsEnabled = true;
44+
Assert.That(button.BackgroundColor, Is.EqualTo(Colors.White));
45+
Application.Current.UserAppTheme = AppTheme.Light;
46+
Assert.That(button.BackgroundColor, Is.EqualTo(Color.FromHex("512BD4")));
47+
}
48+
}
49+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
4+
xmlns:local="clr-namespace:Microsoft.Maui.Controls.Xaml.UnitTests"
5+
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Maui17354">
6+
<Grid WidthRequest="500" ColumnDefinitions="*" x:Name="grid">
7+
<VisualStateManager.VisualStateGroups>
8+
<VisualStateGroupList>
9+
<VisualStateGroup x:Name="CommonStates">
10+
<VisualState x:Name="PointerOver">
11+
<VisualState.Setters>
12+
<Setter Property="BackgroundColor" Value="{AppThemeBinding Dark=Red, Light=White}"/>
13+
</VisualState.Setters>
14+
</VisualState>
15+
<VisualState x:Name="Normal">
16+
<VisualState.Setters>
17+
<Setter Property="BackgroundColor" Value="Transparent" />
18+
</VisualState.Setters>
19+
</VisualState>
20+
</VisualStateGroup>
21+
</VisualStateGroupList>
22+
</VisualStateManager.VisualStateGroups>
23+
<Label Text="I'm a grid. Hover over me to change background color"/>
24+
</Grid>
25+
</ContentPage>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Linq;
5+
using Microsoft.Maui.ApplicationModel;
6+
using Microsoft.Maui.Controls.Core.UnitTests;
7+
using Microsoft.Maui.Controls.Shapes;
8+
using Microsoft.Maui.Devices;
9+
using Microsoft.Maui.Graphics;
10+
using NUnit.Framework;
11+
12+
namespace Microsoft.Maui.Controls.Xaml.UnitTests;
13+
14+
public partial class Maui17354 : ContentPage
15+
{
16+
17+
public Maui17354() => InitializeComponent();
18+
19+
public Maui17354(bool useCompiledXaml)
20+
{
21+
//this stub will be replaced at compile time
22+
}
23+
24+
[TestFixture]
25+
class Test
26+
{
27+
[SetUp] public void Setup() => AppInfo.SetCurrent(new MockAppInfo());
28+
[TearDown] public void TearDown() => AppInfo.SetCurrent(null);
29+
30+
[Test]
31+
public void VSMandAppTheme([Values(false, true)] bool useCompiledXaml)
32+
{
33+
var page = new Maui17354(useCompiledXaml);
34+
var grid = page.grid;
35+
36+
Assert.That(grid.BackgroundColor, Is.EqualTo(Colors.Transparent));
37+
38+
Assert.True(VisualStateManager.GoToState(grid, "PointerOver"));
39+
Assert.That(grid.BackgroundColor, Is.EqualTo(Colors.White));
40+
41+
Assert.True(VisualStateManager.GoToState(grid, "Normal"));
42+
Assert.That(grid.BackgroundColor, Is.EqualTo(Colors.Transparent));
43+
44+
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)