Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1da32c9
Remove MAUI internals usage
jfversluis Jun 24, 2026
95a0d72
Address internals-removal review feedback
jfversluis Jun 24, 2026
7685688
Clarify typed binding removal behavior
jfversluis Jun 25, 2026
26778b4
Guard typed binding converter write-back
jfversluis Jun 25, 2026
af7be07
Pin build workflow Xcode version
jfversluis Jun 25, 2026
90f3634
Cover ScrollView padding overloads
jfversluis Jun 25, 2026
3a941b7
Avoid typed binding source feedback loops
jfversluis Jul 7, 2026
1ba9df6
Update CI Xcode pin
jfversluis Jul 16, 2026
a9dfb93
Address latest review feedback
jfversluis Jul 16, 2026
45a9975
Narrow image and placeholder helpers
jfversluis Jul 16, 2026
3b6e250
Clean image and placeholder docs
jfversluis Jul 16, 2026
fb353f7
Fix nested gesture binding paths
jfversluis Jul 16, 2026
12a6fd5
Use floating Xcode 26 selector
jfversluis Jul 17, 2026
24cd300
Address typed binding review feedback
jfversluis Jul 17, 2026
f3ba4ec
Clarify pathless command parameter binding mode
jfversluis Jul 17, 2026
d8c5348
Update element dynamic resource docs
jfversluis Jul 17, 2026
05d7fce
Harden pathless typed command parameters
jfversluis Jul 17, 2026
2a8c0f1
Merge branch 'main' into jfversluis-fix-maui-internals
TheCodeTraveler Jul 21, 2026
ab03bc9
Skip null value-type convert back writes
jfversluis Jul 21, 2026
5fb1f0d
Update formatting
TheCodeTraveler Jul 21, 2026
82b13fa
Address typed binding review feedback
jfversluis Jul 21, 2026
95bd2a8
Merge branch 'jfversluis-fix-maui-internals' of https://github.com/Co…
TheCodeTraveler Jul 21, 2026
7db54c4
Add Unit Tests
TheCodeTraveler Jul 21, 2026
66d36d3
Potential fix for pull request finding
TheCodeTraveler Jul 21, 2026
7f76e57
Clarify typed binding handler validation
jfversluis Jul 21, 2026
6c8086a
Update handler validation tests
jfversluis Jul 21, 2026
84aaf7f
Align invalid handler exception type
jfversluis Jul 21, 2026
3bc96f3
Update naming
TheCodeTraveler Jul 22, 2026
b942ca7
Handle one-way typed conversion delegates
jfversluis Jul 22, 2026
d865213
Add support for `Span` and `SearchHandler`
TheCodeTraveler Jul 22, 2026
069a2aa
Merge branch 'main' into jfversluis-fix-maui-internals
TheCodeTraveler Jul 22, 2026
5cc38d2
Update TypedBindingExtensionsTests.cs
TheCodeTraveler Jul 22, 2026
2eabec0
Add BindingContext comment
TheCodeTraveler Jul 22, 2026
cc5f536
Remove methods with unused parameters
TheCodeTraveler Jul 22, 2026
25f27d8
Fix failing unit tests, Update XML
TheCodeTraveler Jul 22, 2026
d9b2e7a
Potential fix for pull request finding
TheCodeTraveler Jul 22, 2026
c5ec20f
Create ExpressionPathHelpersTests.cs
TheCodeTraveler Jul 22, 2026
0bec04a
Merge branch 'main' into jfversluis-fix-maui-internals
TheCodeTraveler Jul 22, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System.Reflection;
using System.Runtime.ExceptionServices;
using CommunityToolkit.Maui.Markup.UnitTests.Base;
using NUnit.Framework;
namespace CommunityToolkit.Maui.Markup.UnitTests;

[TestFixture]
class BindablePropertyHelpersTests : BaseMarkupTestFixture
{
const string getPaddingProperty = "GetPaddingProperty";
const string getFontFamilyProperty = "GetFontFamilyProperty";
const string getFontSizeProperty = "GetFontSizeProperty";
const string getFontAttributesProperty = "GetFontAttributesProperty";
const string getTextColorProperty = "GetTextColorProperty";
const string getImageSourceProperty = "GetImageSourceProperty";
const string getImageAspectProperty = "GetImageAspectProperty";
const string getImageIsOpaqueProperty = "GetImageIsOpaqueProperty";
const string getPlaceholderProperty = "GetPlaceholderProperty";
const string getPlaceholderColorProperty = "GetPlaceholderColorProperty";

static readonly SupportedBindablePropertyCase[] supportedCases =
[
new(getPaddingProperty, "Border", static () => new Border(), Border.PaddingProperty),
new(getPaddingProperty, "Button", static () => new Button(), Button.PaddingProperty),
new(getPaddingProperty, "ContentPresenter", static () => new ContentPresenter(), ContentPresenter.PaddingProperty),
new(getPaddingProperty, "ImageButton", static () => new ImageButton(), ImageButton.PaddingProperty),
new(getPaddingProperty, "Label", static () => new Label(), Label.PaddingProperty),
new(getPaddingProperty, "ScrollView", static () => new ScrollView(), ScrollView.PaddingProperty),
new(getPaddingProperty, "Layout", static () => new Grid(), Layout.PaddingProperty),
new(getPaddingProperty, "Page", static () => new Page(), Page.PaddingProperty),
new(getPaddingProperty, "TemplatedView", static () => new ContentView(), TemplatedView.PaddingProperty),

new(getFontFamilyProperty, "Button", static () => new Button(), Button.FontFamilyProperty),
new(getFontFamilyProperty, "DatePicker", static () => new DatePicker(), DatePicker.FontFamilyProperty),
new(getFontFamilyProperty, "FontImageSource", static () => new FontImageSource(), FontImageSource.FontFamilyProperty),
new(getFontFamilyProperty, "SearchBar", static () => new SearchBar(), SearchBar.FontFamilyProperty),
new(getFontFamilyProperty, "InputView", static () => new Entry(), InputView.FontFamilyProperty),
new(getFontFamilyProperty, "Label", static () => new Label(), Label.FontFamilyProperty),
new(getFontFamilyProperty, "Picker", static () => new Picker(), Picker.FontFamilyProperty),
new(getFontFamilyProperty, "RadioButton", static () => new RadioButton(), RadioButton.FontFamilyProperty),
new(getFontFamilyProperty, "SearchHandler", static () => new SearchHandler(), SearchHandler.FontFamilyProperty),
new(getFontFamilyProperty, "Span", static () => new Span(), Span.FontFamilyProperty),
new(getFontFamilyProperty, "TimePicker", static () => new TimePicker(), TimePicker.FontFamilyProperty),

new(getFontSizeProperty, "Button", static () => new Button(), Button.FontSizeProperty),
new(getFontSizeProperty, "DatePicker", static () => new DatePicker(), DatePicker.FontSizeProperty),
new(getFontSizeProperty, "SearchBar", static () => new SearchBar(), SearchBar.FontSizeProperty),
new(getFontSizeProperty, "InputView", static () => new Entry(), InputView.FontSizeProperty),
new(getFontSizeProperty, "Label", static () => new Label(), Label.FontSizeProperty),
new(getFontSizeProperty, "Picker", static () => new Picker(), Picker.FontSizeProperty),
new(getFontSizeProperty, "RadioButton", static () => new RadioButton(), RadioButton.FontSizeProperty),
new(getFontSizeProperty, "SearchHandler", static () => new SearchHandler(), SearchHandler.FontSizeProperty),
new(getFontSizeProperty, "Span", static () => new Span(), Span.FontSizeProperty),
new(getFontSizeProperty, "TimePicker", static () => new TimePicker(), TimePicker.FontSizeProperty),

new(getFontAttributesProperty, "Button", static () => new Button(), Button.FontAttributesProperty),
new(getFontAttributesProperty, "DatePicker", static () => new DatePicker(), DatePicker.FontAttributesProperty),
new(getFontAttributesProperty, "SearchBar", static () => new SearchBar(), SearchBar.FontAttributesProperty),
new(getFontAttributesProperty, "InputView", static () => new Entry(), InputView.FontAttributesProperty),
new(getFontAttributesProperty, "Label", static () => new Label(), Label.FontAttributesProperty),
new(getFontAttributesProperty, "Picker", static () => new Picker(), Picker.FontAttributesProperty),
new(getFontAttributesProperty, "RadioButton", static () => new RadioButton(), RadioButton.FontAttributesProperty),
new(getFontAttributesProperty, "SearchHandler", static () => new SearchHandler(), SearchHandler.FontAttributesProperty),
new(getFontAttributesProperty, "Span", static () => new Span(), Span.FontAttributesProperty),
new(getFontAttributesProperty, "TimePicker", static () => new TimePicker(), TimePicker.FontAttributesProperty),

new(getTextColorProperty, "Button", static () => new Button(), Button.TextColorProperty),
new(getTextColorProperty, "DatePicker", static () => new DatePicker(), DatePicker.TextColorProperty),
new(getTextColorProperty, "SearchBar", static () => new SearchBar(), SearchBar.TextColorProperty),
new(getTextColorProperty, "InputView", static () => new Entry(), InputView.TextColorProperty),
new(getTextColorProperty, "Label", static () => new Label(), Label.TextColorProperty),
new(getTextColorProperty, "Picker", static () => new Picker(), Picker.TextColorProperty),
new(getTextColorProperty, "RadioButton", static () => new RadioButton(), RadioButton.TextColorProperty),
new(getTextColorProperty, "SearchHandler", static () => new SearchHandler(), SearchHandler.TextColorProperty),
new(getTextColorProperty, "Span", static () => new Span(), Span.TextColorProperty),
new(getTextColorProperty, "TimePicker", static () => new TimePicker(), TimePicker.TextColorProperty),

new(getImageSourceProperty, "Image", static () => new Image(), Image.SourceProperty),
new(getImageSourceProperty, "ImageButton", static () => new ImageButton(), ImageButton.SourceProperty),

new(getImageAspectProperty, "Image", static () => new Image(), Image.AspectProperty),
new(getImageAspectProperty, "ImageButton", static () => new ImageButton(), ImageButton.AspectProperty),

new(getImageIsOpaqueProperty, "Image", static () => new Image(), Image.IsOpaqueProperty),
new(getImageIsOpaqueProperty, "ImageButton", static () => new ImageButton(), ImageButton.IsOpaqueProperty),

new(getPlaceholderProperty, "SearchBar", static () => new SearchBar(), SearchBar.PlaceholderProperty),
new(getPlaceholderProperty, "InputView", static () => new Entry(), InputView.PlaceholderProperty),
new(getPlaceholderProperty, "SearchHandler", static () => new SearchHandler(), SearchHandler.PlaceholderProperty),

new(getPlaceholderColorProperty, "SearchBar", static () => new SearchBar(), SearchBar.PlaceholderColorProperty),
new(getPlaceholderColorProperty, "InputView", static () => new Entry(), InputView.PlaceholderColorProperty),
new(getPlaceholderColorProperty, "SearchHandler", static () => new SearchHandler(), SearchHandler.PlaceholderColorProperty)
];

static readonly UnsupportedBindablePropertyCase[] unsupportedCases =
[
new(getPaddingProperty, "Padding"),
new(getFontFamilyProperty, "FontFamily"),
new(getFontSizeProperty, "FontSize"),
new(getFontAttributesProperty, "FontAttributes"),
new(getTextColorProperty, "TextColor"),
new(getImageSourceProperty, "Source"),
new(getImageAspectProperty, "Aspect"),
new(getImageIsOpaqueProperty, "IsOpaque"),
new(getPlaceholderProperty, "Placeholder"),
new(getPlaceholderColorProperty, "PlaceholderColor")
];

[TestCaseSource(nameof(supportedCases))]
public void SupportedBindableReturnsExpectedProperty(SupportedBindablePropertyCase supportedCase)
{
var property = InvokeGetProperty(supportedCase.MethodName, supportedCase.CreateBindable());

Assert.That(property, Is.SameAs(supportedCase.ExpectedProperty));
}

[TestCaseSource(nameof(unsupportedCases))]
public void UnsupportedBindableThrowsNotSupportedException(UnsupportedBindablePropertyCase unsupportedCase)
{
var unsupportedBindable = new BoxView();

var exception = Assert.Throws<NotSupportedException>(() => InvokeGetProperty(unsupportedCase.MethodName, unsupportedBindable));

Assert.That(exception?.Message, Is.EqualTo($"{unsupportedBindable.GetType()} does not expose a public {unsupportedCase.PropertyName} bindable property."));
}

/// <remarks>
/// <see cref="ElementExtensions"/>, <see cref="ImageExtensions"/> and <see cref="PlaceholderExtensions"/> constrain their
/// public API to the supported types, so the internal helper is invoked through reflection here to verify every
/// mapping (including the unsupported fallbacks that the constrained public API cannot reach).
/// </remarks>
static BindableProperty InvokeGetProperty(string methodName, BindableObject bindable)
{
var bindablePropertyHelpersType = typeof(ElementExtensions).Assembly.GetType("CommunityToolkit.Maui.Markup.BindablePropertyHelpers")
?? throw new InvalidOperationException("Unable to locate CommunityToolkit.Maui.Markup.BindablePropertyHelpers");
var method = bindablePropertyHelpersType.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static)
?? throw new InvalidOperationException($"Unable to locate {methodName}");

try
{
return (BindableProperty)(method.Invoke(null, [bindable]) ?? throw new InvalidOperationException($"{methodName} returned null"));
}
catch (TargetInvocationException ex) when (ex.InnerException is not null)
{
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
throw; // Unreachable; keeps the compiler happy
}
}

public sealed record SupportedBindablePropertyCase(string MethodName, string BindableName, Func<BindableObject> CreateBindable, BindableProperty ExpectedProperty)
{
public override string ToString() => $"{MethodName}({BindableName})";
}

public sealed record UnsupportedBindablePropertyCase(string MethodName, string PropertyName)
{
public override string ToString() => $"{MethodName}(BoxView)";
}
}
23 changes: 15 additions & 8 deletions src/CommunityToolkit.Maui.Markup.UnitTests/BindingHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Globalization;
using System.Globalization;
using System.Reflection;
using Microsoft.Maui.Controls.Internals;
using NUnit.Framework;
namespace CommunityToolkit.Maui.Markup.UnitTests;

Expand Down Expand Up @@ -79,14 +78,17 @@ internal static void AssertTypedBindingExists<TBindable, TBindingContext, TSourc
TDest? expectedFallbackValue = default,
TParam? expectedConverterParameter = default) where TBindable : BindableObject
{
var binding = GetTypedBinding(bindable, targetProperty) ?? throw new NullReferenceException();
var binding = GetBinding(bindable, targetProperty) ?? throw new NullReferenceException();

Assert.Multiple(() =>
{
Assert.That(binding, Is.Not.Null);
Assert.That(binding.Mode, Is.EqualTo(expectedBindingMode));

Assert.That(binding.Converter?.ToString(), Is.EqualTo(expectedConverter?.ToString()));
var actualConverter = binding.Converter?.GetType().Name.StartsWith("GetterValueConverter", StringComparison.Ordinal) is true && expectedConverter is null
? null
: binding.Converter;
Assert.That(actualConverter?.ToString(), Is.EqualTo(expectedConverter?.ToString()));

Assert.That(binding.ConverterParameter, Is.EqualTo(expectedConverterParameter));

Expand Down Expand Up @@ -200,8 +202,6 @@ internal static void AssertBindingExists<TDest, TParam>(

internal static Binding? GetBinding(BindableObject bindable, BindableProperty property) => GetBindingBase<Binding>(bindable, property);

internal static TypedBindingBase? GetTypedBinding(BindableObject bindable, BindableProperty property) => GetBindingBase<TypedBindingBase>(bindable, property);

internal static MultiBinding? GetMultiBinding(BindableObject bindable, BindableProperty property) => GetBindingBase<MultiBinding>(bindable, property);

/// <remarks>
Expand All @@ -213,8 +213,15 @@ internal static void AssertBindingExists<TDest, TParam>(
{
getContextMethodInfo ??= typeof(BindableObject).GetMethod("GetContext", BindingFlags.NonPublic | BindingFlags.Instance);

var context = (BindableObject.BindablePropertyContext?)getContextMethodInfo?.Invoke(bindable, [property]);
return (TBinding?)context?.Bindings.GetValue();
var context = getContextMethodInfo?.Invoke(bindable, [property]);
var contextType = context?.GetType();
var bindings = contextType?.GetProperty("Bindings", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(context)
?? contextType?.GetField("Bindings", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(context);
var binding = bindings?.GetType()
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.SingleOrDefault(static method => method.Name is "GetValue" && method.GetParameters().Length is 0)
?.Invoke(bindings, null);
return binding as TBinding;
}

internal static IValueConverter AssertConvert<TValue, TConvertedValue>(this IValueConverter converter, TValue value, object? parameter, TConvertedValue expectedConvertedValue, bool twoWay = false, bool backOnly = false, CultureInfo? culture = null)
Expand Down
Loading
Loading