Skip to content

Commit 21f14ef

Browse files
Fixed issue in TypedBinding not being able to support nullable types properly (#381)
* Fixed issue in TypedBinding with not being able to support nullable types properly * Added unit test to confirm nullable types are now possible with the TypedBinding * Remove Null Forgiving Operator * Update Unit Tests * Update Benchmarks * Increase to .NET 9.0.203 --------- Co-authored-by: Brandon Minnick <[email protected]>
1 parent 0a8799d commit 21f14ef

File tree

8 files changed

+135
-62
lines changed

8 files changed

+135
-62
lines changed

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "9.0.0",
3+
"version": "9.0.203",
44
"rollForward": "latestFeature",
55
"allowPrerelease": false
66
}

src/CommunityToolkit.Maui.Markup.Benchmarks/Benchmarks/ExecuteBindingsBase.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ protected ExecuteBindingsBase()
2828
{
2929
BindingContext = TypedMarkupBindingsLabelViewModel
3030
}.Bind(Label.TextProperty,
31-
getter: (LabelViewModel vm) => vm.Text,
32-
setter: (vm, text) => vm.Text = text,
31+
getter: static (LabelViewModel vm) => vm.Text,
32+
setter: static (vm, text) => vm.Text = text ?? string.Empty,
3333
mode: BindingMode.TwoWay)
3434
.Bind(Label.TextColorProperty,
35-
getter: (LabelViewModel vm) => vm.TextColor,
36-
setter: (vm, textColor) => vm.TextColor = textColor,
35+
getter: static (LabelViewModel vm) => vm.TextColor,
36+
setter: static (vm, textColor) => vm.TextColor = textColor ?? Colors.Transparent,
3737
mode: BindingMode.TwoWay);
3838
TypedMarkupBindingsLabel.EnableAnimations();
3939
}

src/CommunityToolkit.Maui.Markup.Benchmarks/Benchmarks/InitializeBindings.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@ public void InitializeTypedBindingsMarkup()
4040
{
4141
typedMarkupBindingsLabel
4242
.Bind(Label.TextProperty,
43-
getter: (LabelViewModel vm) => vm.Text,
44-
setter: (vm, text) => vm.Text = text,
43+
getter: static (LabelViewModel vm) => vm.Text,
44+
setter: static (vm, text) => vm.Text = text ?? string.Empty,
4545
mode: BindingMode.TwoWay)
4646
.Bind(Label.TextColorProperty,
47-
getter: (LabelViewModel vm) => vm.TextColor,
48-
setter: (vm, textColor) => vm.TextColor = textColor,
47+
getter: static (LabelViewModel vm) => vm.TextColor,
48+
setter: static (vm, textColor) => vm.TextColor = textColor ?? Colors.Transparent,
4949
mode: BindingMode.TwoWay);
5050
}
5151
}

src/CommunityToolkit.Maui.Markup.UnitTests/TypedBindingExtensionsTests.cs

Lines changed: 87 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ public async Task ConfirmReadOnlyTypedBindingWithIValueConverter()
473473
var label = new Label
474474
{
475475
BindingContext = viewModel
476-
}.Bind<Label, ViewModel, Color, string>(Label.TextProperty,
476+
}.Bind<Label, ViewModel, Color?, string>(Label.TextProperty,
477477
static viewModel => viewModel.TextColor,
478478
converter: colorToHexRgbStringConverter);
479479

@@ -644,14 +644,77 @@ void HandleSliderPropertyChanged(object? sender, PropertyChangedEventArgs e)
644644
}
645645
}
646646

647+
/// <summary>
648+
/// Previously this was causing a System.InvalidOperationException : Unable to find target value.
649+
/// For more info: https://github.com/CommunityToolkit/Maui.Markup/issues/354
650+
/// </summary>
651+
[Test]
652+
public async Task ConfirmNullableTypedBindingWithValueNull()
653+
{
654+
ArgumentNullException.ThrowIfNull(viewModel);
655+
656+
bool didViewModelPropertyChangeFire = false;
657+
int viewModelPropertyChangedEventCount = 0;
658+
TaskCompletionSource<string?> viewModelPropertyChangedEventArgsTCS = new();
659+
660+
bool didEntryTextChangeFire = false;
661+
int entryTextChangedEventCount = 0;
662+
663+
var entry = new Entry
664+
{
665+
BindingContext = viewModel,
666+
Keyboard = Keyboard.Numeric
667+
}.Bind(Entry.TextProperty, static (ViewModel viewModel) => viewModel.Age, static (viewModel, age) => viewModel.Age = age);
668+
669+
entry.TextChanged += HandleEntryTextChanged;
670+
viewModel.PropertyChanged += HandleViewModelPropertyChanged;
671+
672+
BindingHelpers.AssertTypedBindingExists(entry, Entry.TextProperty, BindingMode.Default, viewModel);
673+
Assert.That(entry.Text, Is.Null);
674+
675+
entry.Text = "1";
676+
677+
Assert.Multiple(() =>
678+
{
679+
Assert.That(entry.Text, Is.EqualTo("1"));
680+
Assert.That(viewModel.Age, Is.EqualTo(1));
681+
});
682+
683+
viewModel.Age = null;
684+
var viewModelPropertyName = await viewModelPropertyChangedEventArgsTCS.Task;
685+
686+
Assert.Multiple(() =>
687+
{
688+
Assert.That(didViewModelPropertyChangeFire, Is.True);
689+
Assert.That(viewModelPropertyName, Is.EqualTo(nameof(ViewModel.Age)));
690+
Assert.That(viewModelPropertyChangedEventCount, Is.EqualTo(2));
691+
692+
Assert.That(didEntryTextChangeFire, Is.True);
693+
Assert.That(entryTextChangedEventCount, Is.EqualTo(2));
694+
695+
Assert.That(entry.Text, Is.Null);
696+
Assert.That(viewModel.Age, Is.Null);
697+
});
698+
699+
void HandleViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
700+
{
701+
didViewModelPropertyChangeFire = true;
702+
viewModelPropertyChangedEventCount++;
703+
viewModelPropertyChangedEventArgsTCS.TrySetResult(e.PropertyName);
704+
}
705+
706+
void HandleEntryTextChanged(object? sender, TextChangedEventArgs e)
707+
{
708+
didEntryTextChangeFire = true;
709+
entryTextChangedEventCount++;
710+
}
711+
}
712+
647713
class ViewModel : INotifyPropertyChanged
648714
{
649715
public const double DefaultPercentage = 0.5;
650716
public const double DefaultHeightRequest = 500;
651717

652-
double percentage = DefaultPercentage, heightRequest = DefaultHeightRequest;
653-
Color textColor = DefaultColor;
654-
655718
public ViewModel()
656719
{
657720
Command = new Command(() => TextColor = colors.Skip(Random.Shared.Next(colors.Keys.Count())).First().Value);
@@ -669,23 +732,29 @@ public ViewModel()
669732

670733
public double HeightRequest
671734
{
672-
get => heightRequest;
673-
set => SetProperty(ref heightRequest, value);
674-
}
735+
get;
736+
set => SetProperty(ref field, value);
737+
} = DefaultHeightRequest;
675738

676739
public double Percentage
677740
{
678-
get => percentage;
679-
set => SetProperty(ref percentage, value);
680-
}
741+
get;
742+
set => SetProperty(ref field, value);
743+
} = DefaultPercentage;
681744

682-
public Color TextColor
745+
public Color? TextColor
683746
{
684-
get => textColor;
685-
set => SetProperty(ref textColor, value);
747+
get;
748+
set => SetProperty(ref field, value);
749+
} = DefaultColor;
750+
751+
public int? Age
752+
{
753+
get;
754+
set => SetProperty(ref field, value);
686755
}
687756

688-
protected void SetProperty<T>(ref T backingStore, in T value, [CallerMemberName] in string propertyname = "")
757+
protected void SetProperty<T>(ref T backingStore, in T value, [CallerMemberName] in string propertyName = "")
689758
{
690759
if (EqualityComparer<T>.Default.Equals(backingStore, value))
691760
{
@@ -694,21 +763,19 @@ protected void SetProperty<T>(ref T backingStore, in T value, [CallerMemberName]
694763

695764
backingStore = value;
696765

697-
OnPropertyChanged(propertyname);
766+
OnPropertyChanged(propertyName);
698767
}
699768

700769
void OnPropertyChanged([CallerMemberName] string propertyName = "") =>
701770
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
702771
}
703772

704-
class NestedViewModel : ViewModel
773+
sealed class NestedViewModel : ViewModel
705774
{
706-
NestedViewModel? model;
707-
708775
public NestedViewModel? Model
709776
{
710-
get => model;
711-
set => SetProperty(ref model, value);
777+
get;
778+
set => SetProperty(ref field, value);
712779
}
713780
}
714781
}

src/CommunityToolkit.Maui.Markup/TypedBinding.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ sealed class TypedBinding<TSource, TProperty> : TypedBindingBase where TSource :
1212
readonly WeakReference<BindableObject?> weakTarget = new(null);
1313

1414
readonly Func<TSource, TProperty> getter;
15-
readonly Action<TSource, TProperty>? setter;
15+
readonly Action<TSource, TProperty?>? setter;
1616
readonly PropertyChangedProxy[] handlers;
1717
readonly List<WeakReference<Element>> ancestryChain = [];
1818

1919
bool isBindingContextRelativeSource;
2020
SetterSpecificity? specificity;
2121
BindableProperty? targetProperty;
2222

23-
public TypedBinding(Func<TSource, TProperty> getter, Action<TSource, TProperty>? setter, (Func<TSource, object?>, string)[] handlers)
23+
public TypedBinding(Func<TSource, TProperty> getter, Action<TSource, TProperty?>? setter, (Func<TSource, object?>, string)[] handlers)
2424
{
2525
ArgumentNullException.ThrowIfNull(handlers);
2626

@@ -248,7 +248,13 @@ void ApplyCore(TSource? sourceObject, BindableObject target, BindableProperty pr
248248
var needsSetter = (mode == BindingMode.TwoWay && fromTarget) || mode == BindingMode.OneWayToSource;
249249
if (needsSetter && sourceObject is not null)
250250
{
251-
var value = GetTargetValue(target.GetValue(property), typeof(TProperty)) ?? throw new InvalidOperationException("Unable to find target value");
251+
var value = GetTargetValue(target.GetValue(property), typeof(TProperty?));
252+
253+
if (value is null)
254+
{
255+
setter?.Invoke(sourceObject, default);
256+
return;
257+
}
252258

253259
if (!BindingExpressionHelper.TryConvert(ref value, property, typeof(TProperty), false))
254260
{

src/CommunityToolkit.Maui.Markup/TypedBindingExtensions.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public static partial class TypedBindingExtensions
1111
public static TBindable BindCommand<TBindable, TCommandBindingContext>(
1212
this TBindable bindable,
1313
Expression<Func<TCommandBindingContext, ICommand>> getter,
14-
Action<TCommandBindingContext, ICommand>? setter = null,
14+
Action<TCommandBindingContext, ICommand?>? setter = null,
1515
BindingMode mode = BindingMode.Default,
1616
TCommandBindingContext? source = default)
1717
where TBindable : BindableObject
@@ -29,11 +29,11 @@ public static TBindable BindCommand<TBindable, TCommandBindingContext>(
2929
public static TBindable BindCommand<TBindable, TCommandBindingContext, TParameterBindingContext, TParameterSource>(
3030
this TBindable bindable,
3131
Expression<Func<TCommandBindingContext, ICommand>> getter,
32-
Action<TCommandBindingContext, ICommand>? setter = null,
32+
Action<TCommandBindingContext, ICommand?>? setter = null,
3333
TCommandBindingContext? source = default,
3434
BindingMode commandBindingMode = BindingMode.Default,
3535
Expression<Func<TParameterBindingContext, TParameterSource>>? parameterGetter = null,
36-
Action<TParameterBindingContext, TParameterSource>? parameterSetter = null,
36+
Action<TParameterBindingContext, TParameterSource?>? parameterSetter = null,
3737
BindingMode parameterBindingMode = BindingMode.Default,
3838
TParameterBindingContext? parameterSource = default)
3939
where TBindable : BindableObject
@@ -68,7 +68,7 @@ public static TBindable Bind<TBindable, TBindingContext, TSource>(
6868
this TBindable bindable,
6969
BindableProperty targetProperty,
7070
Expression<Func<TBindingContext, TSource>> getter,
71-
Action<TBindingContext, TSource>? setter = null,
71+
Action<TBindingContext, TSource?>? setter = null,
7272
BindingMode mode = BindingMode.Default,
7373
string? stringFormat = null,
7474
TBindingContext? source = default)
@@ -93,7 +93,7 @@ public static TBindable Bind<TBindable, TBindingContext, TSource, TDest>(
9393
this TBindable bindable,
9494
BindableProperty targetProperty,
9595
Expression<Func<TBindingContext, TSource>> getter,
96-
Action<TBindingContext, TSource>? setter = null,
96+
Action<TBindingContext, TSource?>? setter = null,
9797
BindingMode mode = BindingMode.Default,
9898
Func<TSource?, TDest>? convert = null,
9999
Func<TDest?, TSource>? convertBack = null,
@@ -124,7 +124,7 @@ public static TBindable Bind<TBindable, TBindingContext, TSource, TDest>(
124124
this TBindable bindable,
125125
BindableProperty targetProperty,
126126
Expression<Func<TBindingContext, TSource>> getter,
127-
Action<TBindingContext, TSource>? setter = null,
127+
Action<TBindingContext, TSource?>? setter = null,
128128
BindingMode mode = BindingMode.Default,
129129
IValueConverter? converter = null,
130130
string? stringFormat = null,
@@ -154,7 +154,7 @@ public static TBindable Bind<TBindable, TBindingContext, TSource, TDest>(
154154
BindableProperty targetProperty,
155155
Func<TBindingContext, TSource> getter,
156156
(Func<TBindingContext, object?>, string)[] handlers,
157-
Action<TBindingContext, TSource>? setter = null,
157+
Action<TBindingContext, TSource?>? setter = null,
158158
BindingMode mode = BindingMode.Default,
159159
IValueConverter? converter = null,
160160
string? stringFormat = null,
@@ -185,7 +185,7 @@ public static TBindable Bind<TBindable, TBindingContext, TSource, TParam, TDest>
185185
this TBindable bindable,
186186
BindableProperty targetProperty,
187187
Expression<Func<TBindingContext, TSource>> getter,
188-
Action<TBindingContext, TSource>? setter = null,
188+
Action<TBindingContext, TSource?>? setter = null,
189189
BindingMode mode = BindingMode.Default,
190190
Func<TSource?, TParam?, TDest>? convert = null,
191191
Func<TDest?, TParam?, TSource>? convertBack = null,
@@ -221,7 +221,7 @@ public static TBindable Bind<TBindable, TBindingContext, TSource, TParam, TDest>
221221
this TBindable bindable,
222222
BindableProperty targetProperty,
223223
Expression<Func<TBindingContext, TSource>> getter,
224-
Action<TBindingContext, TSource>? setter = null,
224+
Action<TBindingContext, TSource?>? setter = null,
225225
BindingMode mode = BindingMode.Default,
226226
IValueConverter? converter = null,
227227
TParam? converterParameter = default,

src/CommunityToolkit.Maui.Markup/TypedBindingsExtensions.Handlers.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public static TBindable BindCommand<TBindable, TCommandBindingContext>(
1212
this TBindable bindable,
1313
Func<TCommandBindingContext, ICommand> getter,
1414
(Func<TCommandBindingContext, object?>, string)[] handlers,
15-
Action<TCommandBindingContext, ICommand>? setter = null,
15+
Action<TCommandBindingContext, ICommand?>? setter = null,
1616
BindingMode mode = BindingMode.Default,
1717
TCommandBindingContext? source = default)
1818
where TBindable : BindableObject
@@ -32,12 +32,12 @@ public static TBindable BindCommand<TBindable, TCommandBindingContext, TParamete
3232
this TBindable bindable,
3333
Func<TCommandBindingContext, ICommand> getter,
3434
(Func<TCommandBindingContext, object?>, string)[] handlers,
35-
Action<TCommandBindingContext, ICommand>? setter = null,
35+
Action<TCommandBindingContext, ICommand?>? setter = null,
3636
TCommandBindingContext? source = default,
3737
BindingMode commandBindingMode = BindingMode.Default,
3838
Func<TParameterBindingContext, TParameterSource>? parameterGetter = null,
3939
(Func<TParameterBindingContext, object?>, string)[]? parameterHandlers = null,
40-
Action<TParameterBindingContext, TParameterSource>? parameterSetter = null,
40+
Action<TParameterBindingContext, TParameterSource?>? parameterSetter = null,
4141
TParameterBindingContext? parameterSource = default,
4242
BindingMode parameterBindingMode = BindingMode.Default)
4343
where TBindable : BindableObject
@@ -77,7 +77,7 @@ public static TBindable Bind<TBindable, TBindingContext, TSource>(
7777
BindableProperty targetProperty,
7878
Func<TBindingContext, TSource> getter,
7979
(Func<TBindingContext, object?>, string)[] handlers,
80-
Action<TBindingContext, TSource>? setter = null,
80+
Action<TBindingContext, TSource?>? setter = null,
8181
BindingMode mode = BindingMode.Default,
8282
string? stringFormat = null,
8383
TBindingContext? source = default)
@@ -104,7 +104,7 @@ public static TBindable Bind<TBindable, TBindingContext, TSource, TDest>(
104104
BindableProperty targetProperty,
105105
Func<TBindingContext, TSource> getter,
106106
(Func<TBindingContext, object?>, string)[] handlers,
107-
Action<TBindingContext, TSource>? setter = null,
107+
Action<TBindingContext, TSource?>? setter = null,
108108
BindingMode mode = BindingMode.Default,
109109
Func<TSource?, TDest>? convert = null,
110110
Func<TDest?, TSource>? convertBack = null,
@@ -137,7 +137,7 @@ public static TBindable Bind<TBindable, TBindingContext, TSource, TParam, TDest>
137137
BindableProperty targetProperty,
138138
Func<TBindingContext, TSource> getter,
139139
(Func<TBindingContext, object?>, string)[] handlers,
140-
Action<TBindingContext, TSource>? setter = null,
140+
Action<TBindingContext, TSource?>? setter = null,
141141
BindingMode mode = BindingMode.Default,
142142
Func<TSource?, TParam?, TDest>? convert = null,
143143
Func<TDest?, TParam?, TSource>? convertBack = null,
@@ -175,7 +175,7 @@ public static TBindable Bind<TBindable, TBindingContext, TSource, TParam, TDest>
175175
BindableProperty targetProperty,
176176
Func<TBindingContext, TSource> getter,
177177
(Func<TBindingContext, object?>, string)[] handlers,
178-
Action<TBindingContext, TSource>? setter = null,
178+
Action<TBindingContext, TSource?>? setter = null,
179179
BindingMode mode = BindingMode.Default,
180180
IValueConverter? converter = null,
181181
TParam? converterParameter = default,

0 commit comments

Comments
 (0)