Skip to content

Commit 386e4c6

Browse files
.BindCommand(): Search Base Types of Microsoft.Maui.* Classes for CommandProperty and CommandParameterProperty (#83)
* Update `DefaultBindableProperty.Get*` logic * Update DefaultBindablePropertiesTests.cs * Update DefaultBindablePropertiesTests.cs
1 parent 5597730 commit 386e4c6

File tree

3 files changed

+54
-201
lines changed

3 files changed

+54
-201
lines changed

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

Lines changed: 26 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -16,213 +16,77 @@ namespace CommunityToolkit.Maui.Markup.UnitTests
1616
[TestFixture]
1717
class DefaultBindablePropertiesTests : BaseMarkupTestFixture
1818
{
19-
[Test]
20-
public void AllBindableElementsInCoreHaveDefaultBindablePropertyOrAreExcluded()
21-
{
22-
const string na = "not applicable", tbd = "to be determined";
23-
IReadOnlyDictionary<Type, string> excludedTypeReasons = new Dictionary<Type, string>
24-
{ // Key: type, Value: reason why it does not have a default bindable property
25-
{ typeof(Application), na },
26-
{ typeof(AdaptiveTrigger), na },
27-
{ typeof(BaseMenuItem), na },
28-
{ typeof(BaseShellItem), na },
29-
{ typeof(Behavior), na },
30-
{ typeof(BindableObject), na },
31-
{ typeof(CarouselView), na },
32-
{ typeof(Cell), na },
33-
{ typeof(ColumnDefinition), na },
34-
{ typeof(CompareStateTrigger), na },
35-
{ typeof(DataTrigger), na },
36-
{ typeof(DeviceStateTrigger), na },
37-
{ typeof(DragGestureRecognizer), na },
38-
{ typeof(DropGestureRecognizer), na },
39-
{ typeof(Element), na },
40-
{ typeof(EventTrigger), na },
41-
{ typeof(FontImageSource), na },
42-
{ typeof(FormattedString), na },
43-
{ typeof(GestureElement), na },
44-
{ typeof(GestureRecognizer), na },
45-
{ typeof(GradientStop), na },
46-
{ typeof(GridItemsLayout), na },
47-
{ typeof(GroupableItemsView), na },
48-
{ typeof(HorizontalStackLayout), na },
49-
{ typeof(ImageSource), na },
50-
{ typeof(InputView), na },
51-
{ typeof(ItemsLayout), na },
52-
{ typeof(LinearItemsLayout), na },
53-
{ typeof(LinearGradientBrush), na },
54-
{ typeof(MenuBar), na },
55-
{ typeof(MultiTrigger), na },
56-
{ typeof(NavigableElement), na },
57-
{ typeof(OpenGLView), na },
58-
{ typeof(OrientationStateTrigger), na },
59-
{ typeof(PanGestureRecognizer), na },
60-
{ typeof(PinchGestureRecognizer), na },
61-
{ typeof(RadialGradientBrush), na },
62-
{ typeof(RoundRectangleGeometry), na },
63-
{ typeof(RowDefinition), na },
64-
{ typeof(SelectableItemsView), na },
65-
{ typeof(StateTrigger), na },
66-
{ typeof(StateTriggerBase), na },
67-
{ typeof(StructuredItemsView), na },
68-
{ typeof(SwipeItems), na },
69-
{ typeof(TableRoot), na },
70-
{ typeof(TableSection), na },
71-
{ typeof(TableView), na },
72-
{ typeof(Trigger), na },
73-
{ typeof(TriggerBase), na },
74-
{ typeof(VerticalStackLayout), na },
75-
{ typeof(View), na },
76-
{ typeof(ViewCell), na },
77-
{ typeof(VisualElement), na },
78-
{ typeof(WebViewSource), na },
79-
{ typeof(Microsoft.Maui.Controls.Compatibility.AbsoluteLayout), na },
80-
{ typeof(Microsoft.Maui.Controls.Compatibility.FlexLayout), na },
81-
{ typeof(Microsoft.Maui.Controls.Compatibility.Grid), na },
82-
{ typeof(Microsoft.Maui.Controls.Compatibility.RelativeLayout), na },
83-
{ typeof(Microsoft.Maui.Controls.Compatibility.StackLayout), na },
84-
{ typeof(AppLinkEntry), tbd },
85-
{ typeof(FlyoutItem), tbd },
86-
{ typeof(Shell), tbd },
87-
{ typeof(ShellContent), tbd },
88-
{ typeof(ShellGroupItem), tbd },
89-
{ typeof(ShellItem), tbd },
90-
{ typeof(ShellSection), tbd },
91-
{ typeof(Tab), tbd },
92-
{ typeof(TabBar), tbd },
93-
{ typeof(ArcSegment), tbd },
94-
{ typeof(BezierSegment), tbd },
95-
{ typeof(CompositeTransform), tbd },
96-
{ typeof(EllipseGeometry), tbd },
97-
{ typeof(Geometry), tbd },
98-
{ typeof(GeometryGroup), tbd },
99-
{ typeof(LineGeometry), tbd },
100-
{ typeof(LineSegment), tbd },
101-
{ typeof(MatrixTransform), tbd },
102-
{ typeof(Path), tbd },
103-
{ typeof(PathFigure), tbd },
104-
{ typeof(PathGeometry), tbd },
105-
{ typeof(PathSegment), tbd },
106-
{ typeof(PolyBezierSegment), tbd },
107-
{ typeof(PolyLineSegment), tbd },
108-
{ typeof(PolyQuadraticBezierSegment), tbd },
109-
{ typeof(QuadraticBezierSegment), tbd },
110-
{ typeof(RectangleGeometry), tbd },
111-
{ typeof(RotateTransform), tbd },
112-
{ typeof(ScaleTransform), tbd },
113-
{ typeof(SkewTransform), tbd },
114-
{ typeof(Shape), tbd },
115-
{ typeof(Transform), tbd },
116-
{ typeof(TransformGroup), tbd },
117-
{ typeof(TranslateTransform), tbd },
118-
{ typeof(Ellipse), tbd },
119-
{ typeof(Line), tbd },
120-
{ typeof(Polygon), tbd },
121-
{ typeof(Polyline), tbd },
122-
{ typeof(Rectangle), tbd },
123-
{ typeof(ScrollView), tbd },
124-
{ typeof(RoundRectangle), tbd },
125-
};
126-
127-
var failMessage = new StringBuilder();
128-
var bindableObjectTypes = typeof(BindableObject).Assembly.GetExportedTypes()
129-
.Where(t => typeof(BindableObject).IsAssignableFrom(t) && !t.IsAbstract && !typeof(Layout).IsAssignableFrom(t) && !t.ContainsGenericParameters);
130-
131-
// The logical default property for a Layout is for its child view(s), which is not a bindable property.
132-
// So we exclude Layouts from this test. Note that it is still perfectly OK to define a default
133-
// bindable property for a Layout where that makes sense.
134-
// We also do not support specifying default properties for unconstructed generic types.
135-
136-
foreach (var type in bindableObjectTypes)
137-
{
138-
if (excludedTypeReasons.TryGetValue(type, out var exclusionReason))
139-
{
140-
Console.WriteLine($"Info: no default BindableProperty defined for BindableObject type {type.FullName} because {exclusionReason}");
141-
continue;
142-
}
143-
144-
if (DefaultBindableProperties.GetFor(type) is null)
145-
{
146-
failMessage.AppendLine(type.FullName);
147-
var propertyNames = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
148-
.Where(f => f.FieldType == typeof(BindableProperty)).Select(f => f?.DeclaringType?.Name + "." + f?.Name).ToList();
149-
150-
if (propertyNames.Count > 0)
151-
{
152-
failMessage.AppendLine("\tCandidate properties:");
153-
foreach (var propertyName in propertyNames)
154-
{
155-
failMessage.Append('\t').AppendLine(propertyName);
156-
}
157-
}
158-
}
159-
}
160-
161-
if (failMessage.Length > 0)
162-
{
163-
Assert.Fail(
164-
$"Missing default BindableProperty / exclusion for BindableObject types:\n{failMessage}\n" +
165-
$"Either register these types in {typeof(DefaultBindableProperties).FullName} or exclude them in this test");
166-
}
167-
}
168-
16919
[Test]
17020
public void GetDefaultBindablePropertyForBuiltInType()
171-
=> Assert.That(DefaultBindableProperties.GetFor(new Label()), Is.Not.Null);
21+
=> Assert.That(DefaultBindableProperties.GetDefaultProperty<Label>(), Is.Not.Null);
17222

17323
[Test]
17424
public void GetDefaultBindablePropertyForDerivedType()
175-
=> Assert.That(DefaultBindableProperties.GetFor(new DerivedFromBoxView()), Is.Not.Null);
25+
=> Assert.That(DefaultBindableProperties.GetDefaultProperty<DerivedFromBoxView>(), Is.Not.Null);
17626

17727
[Test]
17828
public void GetDefaultBindablePropertyForUnsupportedType()
17929
=> Assert.Throws<ArgumentException>(
180-
() => DefaultBindableProperties.GetFor(new CustomView()),
30+
() => DefaultBindableProperties.GetDefaultProperty<CustomView>(),
18131
"No default bindable property is registered for BindableObject type XamarinFormsMarkupUnitTestsDefaultBindablePropertiesViews.CustomView" +
18232
"\r\nEither specify a property when calling Bind() or register a default bindable property for this BindableObject type");
18333

18434
[Test]
18535
public void RegisterDefaultBindableProperty()
18636
{
187-
var v = new CustomViewWithText();
188-
Assert.Throws<ArgumentException>(() => DefaultBindableProperties.GetFor(v));
37+
Assert.Throws<ArgumentException>(() => DefaultBindableProperties.GetDefaultProperty<CustomViewWithText>());
18938

19039
DefaultBindableProperties.Register(CustomViewWithText.TextProperty);
19140
}
19241

42+
[Test]
43+
public void GetDefaultBindablePropertiesForBuiltInType()
44+
=> Assert.That(DefaultBindableProperties.GetDefaultProperty<Button>(), Is.Not.Null);
45+
46+
[Test]
47+
public void GetDefaultBindablePropertiesForDerivedType()
48+
=> Assert.That(DefaultBindableProperties.GetDefaultProperty<DerivedFromButton>(), Is.Not.Null);
49+
50+
[Test]
51+
public void GetDefaultBindablePropertiesForMauiDerivedType()
52+
=> Assert.That(DefaultBindableProperties.GetDefaultProperty<MenuFlyoutItem>(), Is.Not.Null);
53+
19354
[Test]
19455
public void GetDefaultBindableCommandPropertiesForBuiltInType()
195-
=> Assert.That(DefaultBindableProperties.GetForCommand(new Button()), Is.Not.Null);
56+
=> Assert.That(DefaultBindableProperties.GetCommandAndCommandParameterProperty<Button>(), Is.Not.Null);
19657

19758
[Test]
19859
public void GetDefaultBindableCommandPropertiesForDerivedType()
199-
=> Assert.That(DefaultBindableProperties.GetFor(new DerivedFromButton()), Is.Not.Null);
60+
=> Assert.That(DefaultBindableProperties.GetCommandAndCommandParameterProperty<DerivedFromButton>(), Is.Not.Null);
61+
62+
[Test]
63+
public void GetDefaultBindableCommandPropertiesForMauiDerivedType()
64+
=> Assert.That(DefaultBindableProperties.GetCommandAndCommandParameterProperty<MenuFlyoutItem>(), Is.Not.Null);
20065

20166
[Test]
20267
public void GetDefaultBindableCommandPropertiesForUnsupportedType()
20368
=> Assert.Throws<ArgumentException>(
204-
() => DefaultBindableProperties.GetFor(new CustomView()),
69+
() => DefaultBindableProperties.GetDefaultProperty<CustomView>(),
20570
"No command + command parameter properties are registered for BindableObject type XamarinFormsMarkupUnitTestsDefaultBindablePropertiesViews.CustomView" +
20671
"\r\nRegister command + command parameter properties for this BindableObject type");
20772

20873
[Test]
20974
public void RegisterDefaultBindableCommandProperties()
21075
{
211-
var v = new CustomViewWithCommand();
212-
Assert.Throws<ArgumentException>(() => DefaultBindableProperties.GetForCommand(v));
76+
Assert.Throws<ArgumentException>(() => DefaultBindableProperties.GetCommandAndCommandParameterProperty<CustomViewWithCommand>());
21377

21478
DefaultBindableProperties.RegisterForCommand((CustomViewWithCommand.CommandProperty, CustomViewWithCommand.CommandParameterProperty));
21579
}
21680

21781
[TearDown]
21882
public override void TearDown()
21983
{
220-
if (DefaultBindableProperties.GetFor(typeof(CustomViewWithText)) != null)
84+
if (DefaultBindableProperties.TryGetDefaultProperty<CustomViewWithText>(out _))
22185
{
22286
DefaultBindableProperties.Unregister(CustomViewWithText.TextProperty);
22387
}
22488

225-
if (DefaultBindableProperties.GetForCommand(typeof(CustomViewWithCommand)) != (null, null))
89+
if (DefaultBindableProperties.TryGetCommandAndCommandParameterProperty<CustomViewWithCommand>(out _, out _))
22690
{
22791
DefaultBindableProperties.UnregisterForCommand(CustomViewWithCommand.CommandProperty);
22892
}

src/CommunityToolkit.Maui.Markup/BindableObjectExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public static TBindable Bind<TBindable>(
9292
object? fallbackValue = null) where TBindable : BindableObject
9393
{
9494
bindable.Bind(
95-
DefaultBindableProperties.GetFor(bindable),
95+
DefaultBindableProperties.GetDefaultProperty<TBindable>(),
9696
path, mode, converter, converterParameter, stringFormat, source, targetNullValue, fallbackValue);
9797

9898
return bindable;
@@ -112,7 +112,7 @@ public static TBindable Bind<TBindable, TSource, TDest>(
112112
{
113113
var converter = new FuncConverter<TSource, TDest, object>(convert, convertBack);
114114
bindable.Bind(
115-
DefaultBindableProperties.GetFor(bindable),
115+
DefaultBindableProperties.GetDefaultProperty<TBindable>(),
116116
path, mode, converter, null, stringFormat, source, targetNullValue, fallbackValue);
117117

118118
return bindable;
@@ -133,7 +133,7 @@ public static TBindable Bind<TBindable, TSource, TParam, TDest>(
133133
{
134134
var converter = new FuncConverter<TSource, TDest, TParam>(convert, convertBack);
135135
bindable.Bind(
136-
DefaultBindableProperties.GetFor(bindable),
136+
DefaultBindableProperties.GetDefaultProperty<TBindable>(),
137137
path, mode, converter, converterParameter, stringFormat, source, targetNullValue, fallbackValue);
138138

139139
return bindable;
@@ -152,7 +152,7 @@ public static TBindable BindCommand<TBindable>(
152152
string? parameterPath = bindingContextPath,
153153
object? parameterSource = null) where TBindable : BindableObject
154154
{
155-
(var commandProperty, var parameterProperty) = DefaultBindableProperties.GetForCommand(bindable);
155+
(var commandProperty, var parameterProperty) = DefaultBindableProperties.GetCommandAndCommandParameterProperty<TBindable>();
156156

157157
bindable.SetBinding(commandProperty, new Binding(path: path, source: source));
158158

src/CommunityToolkit.Maui.Markup/DefaultBindableProperties.cs

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Reflection;
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Reflection;
23

34
namespace CommunityToolkit.Maui.Markup;
45

@@ -133,43 +134,36 @@ internal static void Unregister(BindableProperty property)
133134
bindableObjectTypeDefaultProperty.Remove(property.DeclaringType.FullName);
134135
}
135136

136-
internal static BindableProperty GetFor(BindableObject bindableObject)
137+
internal static BindableProperty GetDefaultProperty<T>() where T : BindableObject
137138
{
138-
var type = bindableObject.GetType();
139-
var defaultProperty = GetFor(type);
140-
141-
if (defaultProperty is null)
139+
if (!TryGetDefaultProperty<T>(out var defaultProperty))
142140
{
143141
throw new ArgumentException(
144-
"No default bindable property is registered for BindableObject type " + type.FullName +
142+
"No default bindable property is registered for BindableObject type " + typeof(T).FullName +
145143
"\r\nEither specify a property when calling Bind() or register a default bindable property for this BindableObject type");
146144
}
147145

148146
return defaultProperty;
149147
}
150148

151-
internal static BindableProperty? GetFor(Type? bindableObjectType)
149+
internal static bool TryGetDefaultProperty<T>([NotNullWhen(true)] out BindableProperty? defaultBindableProperty) where T : BindableObject
152150
{
153-
BindableProperty? defaultProperty = null;
151+
defaultBindableProperty = null;
152+
var bindableObjectType = typeof(T);
154153

155154
do
156155
{
157156
var bindableObjectTypeName = bindableObjectType?.FullName;
158-
if (bindableObjectTypeName is not null && bindableObjectTypeDefaultProperty.TryGetValue(bindableObjectTypeName, out defaultProperty))
159-
{
160-
break;
161-
}
162-
163-
if (bindableObjectTypeName?.StartsWith($"{nameof(Microsoft)}.{nameof(Microsoft.Maui)}.", StringComparison.Ordinal) is true)
157+
if (bindableObjectTypeName is not null && bindableObjectTypeDefaultProperty.TryGetValue(bindableObjectTypeName, out defaultBindableProperty))
164158
{
165-
break;
159+
return true;
166160
}
167161

168162
bindableObjectType = bindableObjectType?.GetTypeInfo().BaseType;
169163
}
170-
while (bindableObjectType != null);
164+
while (bindableObjectType is not null && bindableObjectType.GetType() != typeof(BindableObject));
171165

172-
return defaultProperty;
166+
return false;
173167
}
174168

175169
internal static void UnregisterForCommand(BindableProperty commandProperty)
@@ -182,43 +176,38 @@ internal static void UnregisterForCommand(BindableProperty commandProperty)
182176
bindableObjectTypeDefaultCommandAndParameterProperties.Remove(commandProperty.DeclaringType.FullName);
183177
}
184178

185-
internal static (BindableProperty, BindableProperty) GetForCommand(BindableObject bindableObject)
179+
internal static (BindableProperty CommandProperty, BindableProperty CommandParameterProperty) GetCommandAndCommandParameterProperty<T>() where T : BindableObject
186180
{
187-
var type = bindableObject.GetType();
188-
(var commandProperty, var parameterProperty) = GetForCommand(type);
189-
if (commandProperty is null || parameterProperty is null)
181+
if (!TryGetCommandAndCommandParameterProperty<T>(out var commandProperty, out var parameterProperty))
190182
{
191183
throw new ArgumentException(
192-
"No command + command parameter properties are registered for BindableObject type " + type.FullName +
184+
"No command + command parameter properties are registered for BindableObject type " + typeof(T).FullName +
193185
"\r\nRegister command + command parameter properties for this BindableObject type");
194186
}
195187

196188
return (commandProperty, parameterProperty);
197189
}
198190

199-
internal static (BindableProperty?, BindableProperty?) GetForCommand(Type? bindableObjectType)
191+
internal static bool TryGetCommandAndCommandParameterProperty<T>([NotNullWhen(true)] out BindableProperty? commandProperty, [NotNullWhen(true)] out BindableProperty? commandParameterProperty) where T : BindableObject
200192
{
201-
(BindableProperty?, BindableProperty?) commandAndParameterProperties = (null, null);
193+
commandProperty = commandParameterProperty = null;
194+
var bindableObjectType = typeof(T);
202195

203196
do
204197
{
205-
var bindableObjectTypeName = bindableObjectType?.FullName;
198+
var bindableObjectTypeName = bindableObjectType.FullName;
206199
if (bindableObjectTypeName is not null && bindableObjectTypeDefaultCommandAndParameterProperties.TryGetValue(bindableObjectTypeName, out var dictionaryResult))
207200
{
208-
commandAndParameterProperties.Item1 = dictionaryResult.Item1;
209-
commandAndParameterProperties.Item2 = dictionaryResult.Item2;
210-
break;
211-
}
201+
commandProperty = dictionaryResult.Item1;
202+
commandParameterProperty = dictionaryResult.Item2;
212203

213-
if (bindableObjectTypeName?.StartsWith($"{nameof(Microsoft)}.{nameof(Microsoft.Maui)}.", StringComparison.Ordinal) is true)
214-
{
215-
break;
204+
return true;
216205
}
217206

218207
bindableObjectType = bindableObjectType?.GetTypeInfo().BaseType;
219208
}
220-
while (bindableObjectType != null);
209+
while (bindableObjectType is not null && bindableObjectType.GetType() != typeof(BindableObject));
221210

222-
return commandAndParameterProperties;
211+
return false;
223212
}
224213
}

0 commit comments

Comments
 (0)