diff --git a/src/Controls/src/Core/BindableObject.cs b/src/Controls/src/Core/BindableObject.cs
index f205dbe7e611..3818f0d7fec6 100644
--- a/src/Controls/src/Core/BindableObject.cs
+++ b/src/Controls/src/Core/BindableObject.cs
@@ -129,9 +129,9 @@ void ClearValueCore(BindableProperty property, SetterSpecificity specificity)
return;
var original = bpcontext.Values.GetSpecificityAndValue();
- if (original.Key == SetterSpecificity.FromHandler)
+ if (original.Key == SetterSpecificity.FromHandler || original.Key == SetterSpecificity.FromUnknown)
{
- bpcontext.Values.Remove(SetterSpecificity.FromHandler);
+ bpcontext.Values.Remove(original.Key);
}
var newValue = bpcontext.Values.GetClearedValue(specificity);
@@ -286,7 +286,7 @@ internal void RemoveBinding(BindableProperty property, SetterSpecificity specifi
/// The bindable property on which to apply .
/// The binding to set for .
public void SetBinding(BindableProperty targetProperty, BindingBase binding)
- => SetBinding(targetProperty, binding, binding != null && targetProperty != null && binding.GetRealizedMode(targetProperty) == BindingMode.TwoWay ? SetterSpecificity.FromHandler : SetterSpecificity.FromBinding);
+ => SetBinding(targetProperty, binding, binding != null && targetProperty != null && binding.GetRealizedMode(targetProperty) == BindingMode.TwoWay ? SetterSpecificity.FromUnknown : SetterSpecificity.FromBinding);
internal void SetBinding(BindableProperty targetProperty, BindingBase binding, SetterSpecificity specificity)
{
@@ -613,11 +613,11 @@ void SetValueActual(BindableProperty property, BindablePropertyContext context,
var original = specificityAndValue.Value;
var originalSpecificity = specificityAndValue.Key;
- //if the last value was set from handler, override it
- if (specificity != SetterSpecificity.FromHandler
- && originalSpecificity == SetterSpecificity.FromHandler)
+ // If the originalSpecificity is FromHandler or FromUnknown,
+ // determine whether it should be overridden.
+ if (ShouldOverrideSpecificity(originalSpecificity, specificity))
{
- context.Values.Remove(SetterSpecificity.FromHandler);
+ context.Values.Remove(originalSpecificity);
originalSpecificity = context.Values.GetSpecificity();
}
@@ -716,6 +716,20 @@ void ApplyBinding(BindablePropertyContext context, bool fromBindingContextChange
binding.Apply(BindingContext, this, context.Property, fromBindingContextChanged, specificity);
}
+ static bool ShouldOverrideSpecificity(SetterSpecificity orginal, SetterSpecificity current)
+ {
+ // FromHandler gets overridden by anything except itself
+ if (orginal == SetterSpecificity.FromHandler)
+ return current != SetterSpecificity.FromHandler;
+
+ // FromUnknown gets overridden by anything except itself
+ if (orginal == SetterSpecificity.FromUnknown)
+ return current != SetterSpecificity.FromUnknown;
+
+ // For all other cases, don't override
+ return false;
+ }
+
static void BindingContextPropertyBindingChanging(BindableObject bindable, BindingBase oldBindingBase, BindingBase newBindingBase)
{
object context = bindable._inheritedContext?.Target;
diff --git a/src/Controls/src/Core/BindableObjectExtensions.cs b/src/Controls/src/Core/BindableObjectExtensions.cs
index bceb514ebe83..e07f247feff9 100644
--- a/src/Controls/src/Core/BindableObjectExtensions.cs
+++ b/src/Controls/src/Core/BindableObjectExtensions.cs
@@ -25,7 +25,7 @@ internal static void RefreshPropertyValue(this BindableObject self, BindableProp
else
{
// support normal/code properties
- self.SetValue(property, value, SetterSpecificity.FromHandler);
+ self.SetValue(property, value, SetterSpecificity.FromUnknown);
}
}
diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/EntryCellRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/EntryCellRenderer.cs
index 3a8d5794a201..d615261c0ddf 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/EntryCellRenderer.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/EntryCellRenderer.cs
@@ -89,7 +89,7 @@ void OnTextChanged(string text)
var entryCell = (EntryCell)Cell;
entryCell
- .SetValue(EntryCell.TextProperty, text, specificity: SetterSpecificity.FromHandler);
+ .SetValue(EntryCell.TextProperty, text, specificity: SetterSpecificity.FromUnknown);
}
void UpdateHeight()
diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/EntryCellRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/EntryCellRenderer.cs
index 3b506c726b52..14e26be324ba 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/EntryCellRenderer.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/EntryCellRenderer.cs
@@ -96,7 +96,7 @@ static void OnTextFieldTextChanged(object sender, EventArgs eventArgs)
var model = (EntryCell)cell.Cell;
model
- .SetValue(EntryCell.TextProperty, cell.TextField.Text, specificity: SetterSpecificity.FromHandler);
+ .SetValue(EntryCell.TextProperty, cell.TextField.Text, specificity: SetterSpecificity.FromUnknown);
}
static void UpdateHorizontalTextAlignment(EntryCellTableViewCell cell, EntryCell entryCell)
diff --git a/src/Controls/src/Core/ListView/ListView.cs b/src/Controls/src/Core/ListView/ListView.cs
index 73e14502dd54..70734b72b284 100644
--- a/src/Controls/src/Core/ListView/ListView.cs
+++ b/src/Controls/src/Core/ListView/ListView.cs
@@ -521,7 +521,7 @@ public void NotifyRowTapped(int groupIndex, int inGroupIndex, Cell cell = null)
// Set SelectedItem before any events so we don't override any changes they may have made.
if (SelectionMode != ListViewSelectionMode.None)
- SetValueCore(SelectedItemProperty, cell?.BindingContext, SetValueFlags.ClearOneWayBindings | SetValueFlags.ClearDynamicResource | (changed ? SetValueFlags.RaiseOnEqual : 0), SetValuePrivateFlags.Default, SetterSpecificity.FromHandler);
+ SetValueCore(SelectedItemProperty, cell?.BindingContext, SetValueFlags.ClearOneWayBindings | SetValueFlags.ClearDynamicResource | (changed ? SetValueFlags.RaiseOnEqual : 0), SetValuePrivateFlags.Default, SetterSpecificity.FromUnknown);
cell?.OnTapped();
@@ -547,7 +547,7 @@ public void NotifyRowTapped(int groupIndex, int inGroupIndex, Cell cell, bool is
// Set SelectedItem before any events so we don't override any changes they may have made.
if (SelectionMode != ListViewSelectionMode.None)
- SetValueCore(SelectedItemProperty, cell?.BindingContext, SetValueFlags.ClearOneWayBindings | SetValueFlags.ClearDynamicResource | (changed ? SetValueFlags.RaiseOnEqual : 0), SetValuePrivateFlags.Default, SetterSpecificity.FromHandler);
+ SetValueCore(SelectedItemProperty, cell?.BindingContext, SetValueFlags.ClearOneWayBindings | SetValueFlags.ClearDynamicResource | (changed ? SetValueFlags.RaiseOnEqual : 0), SetValuePrivateFlags.Default, SetterSpecificity.FromUnknown);
if (isContextMenuRequested || cell == null)
{
diff --git a/src/Controls/src/Core/Picker/Picker.cs b/src/Controls/src/Core/Picker/Picker.cs
index 399235196aa4..8a44c9bc00f7 100644
--- a/src/Controls/src/Core/Picker/Picker.cs
+++ b/src/Controls/src/Core/Picker/Picker.cs
@@ -372,7 +372,7 @@ void ClampSelectedIndex()
var oldIndex = SelectedIndex;
var newIndex = SelectedIndex.Clamp(-1, Items.Count - 1);
//FIXME use the specificity of the caller
- SetValue(SelectedIndexProperty, newIndex, SetterSpecificity.FromHandler);
+ SetValue(SelectedIndexProperty, newIndex, SetterSpecificity.FromUnknown);
// If the index has not changed, still need to change the selected item
if (newIndex == oldIndex)
UpdateSelectedItem(newIndex);
@@ -384,10 +384,10 @@ void UpdateSelectedIndex(object selectedItem)
if (ItemsSource != null)
{
- SetValue(SelectedIndexProperty, ItemsSource.IndexOf(selectedItem), SetterSpecificity.FromHandler);
+ SetValue(SelectedIndexProperty, ItemsSource.IndexOf(selectedItem), SetterSpecificity.FromUnknown);
return;
}
- SetValue(SelectedIndexProperty, Items.IndexOf(selectedItem), SetterSpecificity.FromHandler);
+ SetValue(SelectedIndexProperty, Items.IndexOf(selectedItem), SetterSpecificity.FromUnknown);
}
void UpdateSelectedItem(int index)
@@ -396,18 +396,18 @@ void UpdateSelectedItem(int index)
if (index == -1)
{
- SetValue(SelectedItemProperty, null, SetterSpecificity.FromHandler);
+ SetValue(SelectedItemProperty, null, SetterSpecificity.FromUnknown);
return;
}
if (ItemsSource != null)
{
var item = index < ItemsSource.Count ? ItemsSource[index] : null;
- SetValue(SelectedItemProperty, item, SetterSpecificity.FromHandler);
+ SetValue(SelectedItemProperty, item, SetterSpecificity.FromUnknown);
return;
}
- SetValue(SelectedItemProperty, Items[index], SetterSpecificity.FromHandler);
+ SetValue(SelectedItemProperty, Items[index], SetterSpecificity.FromUnknown);
}
///
diff --git a/src/Controls/src/Core/RadioButton/RadioButton.cs b/src/Controls/src/Core/RadioButton/RadioButton.cs
index 6a40dedf8119..ab25189c940a 100644
--- a/src/Controls/src/Core/RadioButton/RadioButton.cs
+++ b/src/Controls/src/Core/RadioButton/RadioButton.cs
@@ -365,7 +365,7 @@ void ApplyIsCheckedState()
void SelectRadioButton(object sender, EventArgs e)
{
if (IsEnabled)
- SetValue(IsCheckedProperty, true, specificity: SetterSpecificity.FromHandler);
+ SetValue(IsCheckedProperty, true, specificity: SetterSpecificity.FromUnknown);
}
void OnIsCheckedPropertyChanged(bool isChecked)
@@ -428,7 +428,7 @@ void HandleRadioButtonGroupSelectionChanged(RadioButton selected, RadioButtonGro
return;
}
- SetValue(IsCheckedProperty, false, specificity: SetterSpecificity.FromHandler);
+ SetValue(IsCheckedProperty, false, specificity: SetterSpecificity.FromUnknown);
}
void HandleRadioButtonGroupValueChanged(Element layout, RadioButtonGroupValueChanged args)
@@ -438,7 +438,7 @@ void HandleRadioButtonGroupValueChanged(Element layout, RadioButtonGroupValueCha
return;
}
- SetValue(IsCheckedProperty, true, specificity: SetterSpecificity.FromHandler);
+ SetValue(IsCheckedProperty, true, specificity: SetterSpecificity.FromUnknown);
}
static View BuildDefaultTemplate()
diff --git a/src/Controls/src/Core/RadioButton/RadioButtonGroupController.cs b/src/Controls/src/Core/RadioButton/RadioButtonGroupController.cs
index 5f19ddccf18f..9556bb3a36c1 100644
--- a/src/Controls/src/Core/RadioButton/RadioButtonGroupController.cs
+++ b/src/Controls/src/Core/RadioButton/RadioButtonGroupController.cs
@@ -106,7 +106,7 @@ void AddRadioButton(RadioButton radioButton)
if (object.Equals(radioButton.Value, this.SelectedValue))
{
- radioButton.SetValue(RadioButton.IsCheckedProperty, true, specificity: SetterSpecificity.FromHandler);
+ radioButton.SetValue(RadioButton.IsCheckedProperty, true, specificity: SetterSpecificity.FromUnknown);
}
}
diff --git a/src/Controls/src/Core/SetterSpecificity.cs b/src/Controls/src/Core/SetterSpecificity.cs
index 11d5d2f2d13f..06bfabb4c053 100644
--- a/src/Controls/src/Core/SetterSpecificity.cs
+++ b/src/Controls/src/Core/SetterSpecificity.cs
@@ -20,6 +20,7 @@ internal readonly struct SetterSpecificity
{
const byte ExtrasVsm = 0x01;
const byte ExtrasHandler = 0xFF;
+ const byte ExtrasUnknown = 0xFE;
public const ushort ManualTriggerBaseline = 2;
@@ -38,12 +39,16 @@ internal readonly struct SetterSpecificity
// handler always apply, but are removed when anything else comes in. see SetValueActual
public static readonly SetterSpecificity FromHandler = new SetterSpecificity(0xFF, 0, 0, 0, 0, 0, 0, 0);
+ // Similar to FromHandler, with slightly lower priority
+ public static readonly SetterSpecificity FromUnknown = new SetterSpecificity(ExtrasUnknown, 0, 0, 0, 0, 0, 0, 0);
+
// We store all information in one single UInt64 value to have the fastest comparison possible
readonly ulong _value;
public bool IsDefault => _value == 0ul;
public bool IsHandler => _value == 0xFFFFFFFFFFFFFFFF;
+ public bool IsUnknown => _value == 0x8000000000000000;
public bool IsVsm => (_value & 0x0100000000000000) != 0;
public bool IsVsmImplicit => (_value & 0x0000000004000000) != 0;
public bool IsManual => ((_value >> 28) & 0xFFFF) == 1;
@@ -115,6 +120,12 @@ public SetterSpecificity(byte extras, ushort manual, byte isDynamicResource, byt
return;
}
+ if (extras == ExtrasUnknown)
+ {
+ _value = 0x8000000000000000;
+ return;
+ }
+
// If no style is set, set it to a value which supersedes any other style value
if (style == 0)
{
diff --git a/src/Controls/src/Core/Shell/SearchHandler.cs b/src/Controls/src/Core/Shell/SearchHandler.cs
index bf6e893d5197..54c68deb6fb4 100644
--- a/src/Controls/src/Core/Shell/SearchHandler.cs
+++ b/src/Controls/src/Core/Shell/SearchHandler.cs
@@ -56,7 +56,7 @@ static void OnIsFocusedPropertyChanged(BindableObject bindable, object oldvalue,
[EditorBrowsable(EditorBrowsableState.Never)]
public void SetIsFocused(bool value)
{
- SetValue(IsFocusedPropertyKey, value, specificity: SetterSpecificity.FromHandler);
+ SetValue(IsFocusedPropertyKey, value, specificity: SetterSpecificity.FromUnknown);
}
[EditorBrowsable(EditorBrowsableState.Never)]
public event EventHandler FocusChangeRequested;
@@ -297,7 +297,7 @@ void ISearchHandlerController.ClearPlaceholderClicked()
void ISearchHandlerController.ItemSelected(object obj)
{
OnItemSelected(obj);
- SetValue(SelectedItemPropertyKey, obj, specificity: SetterSpecificity.FromHandler);
+ SetValue(SelectedItemPropertyKey, obj, specificity: SetterSpecificity.FromUnknown);
}
void ISearchHandlerController.QueryConfirmed()
diff --git a/src/Controls/src/Core/Shell/ShellItem.cs b/src/Controls/src/Core/Shell/ShellItem.cs
index 494bb5c3a7c3..b4c2579a6ca0 100644
--- a/src/Controls/src/Core/Shell/ShellItem.cs
+++ b/src/Controls/src/Core/Shell/ShellItem.cs
@@ -273,7 +273,7 @@ void OnVisibleChildRemoved(Element child)
if (CurrentItem == child)
{
if (ShellItemController.GetItems().Count == 0)
- ClearValue(CurrentItemProperty, specificity: SetterSpecificity.FromHandler);
+ ClearValue(CurrentItemProperty, specificity: SetterSpecificity.FromUnknown);
else
SetValueFromRenderer(CurrentItemProperty, ShellItemController.GetItems()[0]);
}
diff --git a/src/Controls/src/Core/Shell/ShellSection.cs b/src/Controls/src/Core/Shell/ShellSection.cs
index 5f648989cde7..5a1e83fa3b39 100644
--- a/src/Controls/src/Core/Shell/ShellSection.cs
+++ b/src/Controls/src/Core/Shell/ShellSection.cs
@@ -721,7 +721,7 @@ void OnVisibleChildRemoved(Element child)
var contentItems = ShellSectionController.GetItems();
if (contentItems.Count == 0)
{
- ClearValue(CurrentItemProperty, specificity: SetterSpecificity.FromHandler);
+ ClearValue(CurrentItemProperty, specificity: SetterSpecificity.FromUnknown);
}
else
{
diff --git a/src/Controls/src/Core/Window/Window.cs b/src/Controls/src/Core/Window/Window.cs
index 270d1f1e4cde..214058467be0 100644
--- a/src/Controls/src/Core/Window/Window.cs
+++ b/src/Controls/src/Core/Window/Window.cs
@@ -220,10 +220,10 @@ void IWindow.FrameChanged(Rect frame)
var shouldTriggerSizeChanged = (Width != frame.Width) || (Height != frame.Height);
- SetValue(XProperty, frame.X, SetterSpecificity.FromHandler);
- SetValue(YProperty, frame.Y, SetterSpecificity.FromHandler);
- SetValue(WidthProperty, frame.Width, SetterSpecificity.FromHandler);
- SetValue(HeightProperty, frame.Height, SetterSpecificity.FromHandler);
+ SetValue(XProperty, frame.X, SetterSpecificity.FromUnknown);
+ SetValue(YProperty, frame.Y, SetterSpecificity.FromUnknown);
+ SetValue(WidthProperty, frame.Width, SetterSpecificity.FromUnknown);
+ SetValue(HeightProperty, frame.Height, SetterSpecificity.FromUnknown);
_batchFrameUpdate--;
if (_batchFrameUpdate < 0)
diff --git a/src/Controls/src/Core/Xaml/Diagnostics/BindablePropertyDiagnostics.cs b/src/Controls/src/Core/Xaml/Diagnostics/BindablePropertyDiagnostics.cs
index 2b2452d7de38..12e99dd4cfb6 100644
--- a/src/Controls/src/Core/Xaml/Diagnostics/BindablePropertyDiagnostics.cs
+++ b/src/Controls/src/Core/Xaml/Diagnostics/BindablePropertyDiagnostics.cs
@@ -28,7 +28,7 @@ public static ValueSource GetValueSource(BindableObject bindable, BindableProper
return new ValueSource(BaseValueSource.Style);
if (specificity == SetterSpecificity.Trigger)
return new ValueSource(BaseValueSource.StyleTrigger);
- if (specificity == SetterSpecificity.FromHandler)
+ if (specificity == SetterSpecificity.FromHandler || specificity == SetterSpecificity.FromUnknown)
return new ValueSource(BaseValueSource.Unknown, isCurrent: true);
if (specificity.IsVsm)
diff --git a/src/Controls/tests/Core.UnitTests/BindingBaseUnitTests.cs b/src/Controls/tests/Core.UnitTests/BindingBaseUnitTests.cs
index a639edb2293a..18b0c0773c33 100644
--- a/src/Controls/tests/Core.UnitTests/BindingBaseUnitTests.cs
+++ b/src/Controls/tests/Core.UnitTests/BindingBaseUnitTests.cs
@@ -145,12 +145,64 @@ public void StringFormatTwoWay()
var bo = new MockBindable { BindingContext = vm };
bo.SetBinding(property, binding);
- bo.SetValue(property, "Baz", SetterSpecificity.FromHandler);
+ bo.SetValue(property, "Baz", SetterSpecificity.FromUnknown);
Assert.Equal("Baz", vm.Text);
Assert.Equal("Foo Baz", bo.GetValue(property));
}
+ [Fact("TwoWay bindings should apply with FromUnknown specificity")]
+ public void TwoWayBindingSpecificity()
+ {
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable));
+ var binding = new Binding("Text", BindingMode.TwoWay);
+
+ var vm = new MockViewModel { Text = "initial" };
+ var bo = new MockBindable { BindingContext = vm };
+ bo.SetBinding(property, binding);
+
+ var context = bo.GetContext(property);
+ Assert.Equal(SetterSpecificity.FromUnknown, context.Values.GetSpecificity());
+ }
+
+ [Fact("FromUnknown should override FromHandler")]
+ public void FromUnknownOverridesFromHandler()
+ {
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable));
+
+ var bo = new MockBindable();
+ bo.SetValue(property, "HandlerValue", SetterSpecificity.FromHandler);
+ bo.SetValue(property, "UnknownValue", SetterSpecificity.FromUnknown);
+
+ // Assert value
+ Assert.Equal("UnknownValue", bo.GetValue(property));
+
+ // Assert specificity
+ var context = bo.GetContext(property);
+ Assert.Equal(SetterSpecificity.FromUnknown, context.Values.GetSpecificity());
+ var fromHandlerValue = context.Values[SetterSpecificity.FromHandler];
+ Assert.Null(fromHandlerValue);
+ }
+
+ [Fact("FromHandler should override FromUnknown")]
+ public void FromHandlerOverridesFromUnknown()
+ {
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable));
+
+ var bo = new MockBindable();
+ bo.SetValue(property, "UnknownValue", SetterSpecificity.FromUnknown);
+ bo.SetValue(property, "HandlerValue", SetterSpecificity.FromHandler);
+
+ // Assert value
+ Assert.Equal("HandlerValue", bo.GetValue(property));
+
+ // Assert specificity
+ var context = bo.GetContext(property);
+ Assert.Equal(SetterSpecificity.FromHandler, context.Values.GetSpecificity());
+ var fromUnknownValue = context.Values[SetterSpecificity.FromUnknown];
+ Assert.Null(fromUnknownValue);
+ }
+
[Fact("You should get an exception when trying to change a binding after it's been applied")]
public void ChangeAfterApply()
{
diff --git a/src/Controls/tests/Core.UnitTests/SetterSpecificityListTests.cs b/src/Controls/tests/Core.UnitTests/SetterSpecificityListTests.cs
index d54cde3f52aa..fee1d086a316 100644
--- a/src/Controls/tests/Core.UnitTests/SetterSpecificityListTests.cs
+++ b/src/Controls/tests/Core.UnitTests/SetterSpecificityListTests.cs
@@ -193,5 +193,18 @@ public void GetClearedValueForSpecificity()
Assert.Equal(nameof(SetterSpecificity.DefaultValue), list.GetClearedValue(SetterSpecificity.ManualValueSetter));
Assert.Equal(nameof(SetterSpecificity.ManualValueSetter), list.GetClearedValue(SetterSpecificity.FromHandler));
}
+
+ [Fact]
+ public void GetClearedValueForFromUnknown()
+ {
+ var list = new SetterSpecificityList