Skip to content

Introduce SetterSpecificity.FromUnknown to avoid misuse of FromHandler #30122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
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
28 changes: 21 additions & 7 deletions src/Controls/src/Core/BindableObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -286,7 +286,7 @@ internal void RemoveBinding(BindableProperty property, SetterSpecificity specifi
/// <param name="targetProperty">The bindable property on which to apply <paramref name="binding"/>.</param>
/// <param name="binding">The binding to set for <paramref name="targetProperty"/>.</param>
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)
{
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/Core/BindableObjectExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions src/Controls/src/Core/ListView/ListView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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)
{
Expand Down
12 changes: 6 additions & 6 deletions src/Controls/src/Core/Picker/Picker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)
Expand All @@ -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);
}

/// <inheritdoc/>
Expand Down
6 changes: 3 additions & 3 deletions src/Controls/src/Core/RadioButton/RadioButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
11 changes: 11 additions & 0 deletions src/Controls/src/Core/SetterSpecificity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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)
{
Expand Down
4 changes: 2 additions & 2 deletions src/Controls/src/Core/Shell/SearchHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<FocusRequestArgs> FocusChangeRequested;
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/Core/Shell/ShellItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/Core/Shell/ShellSection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
8 changes: 4 additions & 4 deletions src/Controls/src/Core/Window/Window.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
54 changes: 53 additions & 1 deletion src/Controls/tests/Core.UnitTests/BindingBaseUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
13 changes: 13 additions & 0 deletions src/Controls/tests/Core.UnitTests/SetterSpecificityListTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<object>();

list[SetterSpecificity.DefaultValue] = nameof(SetterSpecificity.DefaultValue);
list[SetterSpecificity.ManualValueSetter] = nameof(SetterSpecificity.ManualValueSetter);
list[SetterSpecificity.FromUnknown] = nameof(SetterSpecificity.FromUnknown);

// Should fall back to ManualValueSetter
Assert.Equal(nameof(SetterSpecificity.ManualValueSetter), list.GetClearedValue(SetterSpecificity.FromUnknown));
}
}
}
Loading