Skip to content

Commit 77d3440

Browse files
authored
Merge pull request #40 from phmatray/codex/update-getactualfieldtype-to-handle-expressions
Handle boxed field types in renderer
2 parents fcd473e + 27bf6da commit 77d3440

File tree

2 files changed

+165
-2
lines changed

2 files changed

+165
-2
lines changed

FormCraft.UnitTests/Rendering/FieldRendererServiceTests.cs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,156 @@ public void RenderField_Should_Handle_Empty_Renderer_Collection()
242242
// Should return unsupported field type message
243243
}
244244

245+
[Fact]
246+
public void RenderField_Should_Detect_Correct_Type_For_Simple_MemberExpression()
247+
{
248+
// Arrange
249+
var model = new TestModel { Name = "Test" };
250+
var field = new FieldConfiguration<TestModel, string?>(x => x.Name);
251+
var wrapper = new FieldConfigurationWrapper<TestModel, string?>(field);
252+
Type? detectedType = null;
253+
254+
var mockRenderer = A.Fake<IFieldRenderer>();
255+
A.CallTo(() => mockRenderer.CanRender(A<Type>._, A<IFieldConfiguration<object, object>>._))
256+
.ReturnsLazily((Type type, IFieldConfiguration<object, object> _) =>
257+
{
258+
detectedType = type;
259+
return true;
260+
});
261+
A.CallTo(() => mockRenderer.Render(A<IFieldRenderContext<TestModel>>._))
262+
.Returns(builder => builder.AddContent(0, "Test"));
263+
264+
var service = new FieldRendererService(new[] { mockRenderer }, _serviceProvider);
265+
var onValueChanged = EventCallback.Factory.Create<object?>(this, _ => { });
266+
var onDependencyChanged = EventCallback.Factory.Create(this, () => { });
267+
268+
// Act
269+
service.RenderField(model, wrapper, onValueChanged, onDependencyChanged);
270+
271+
// Assert
272+
// The GetActualFieldType method in FieldRendererService handles UnaryExpression
273+
// and returns the underlying property type
274+
detectedType.ShouldBe(typeof(string));
275+
}
276+
277+
278+
[Fact]
279+
public void RenderField_Should_Detect_Correct_Type_For_Value_Type_MemberExpression()
280+
{
281+
// Arrange
282+
var model = new TestModel { Value = 42 };
283+
var field = new FieldConfiguration<TestModel, int>(x => x.Value);
284+
var wrapper = new FieldConfigurationWrapper<TestModel, int>(field);
285+
Type? detectedType = null;
286+
287+
var mockRenderer = A.Fake<IFieldRenderer>();
288+
A.CallTo(() => mockRenderer.CanRender(A<Type>._, A<IFieldConfiguration<object, object>>._))
289+
.ReturnsLazily((Type type, IFieldConfiguration<object, object> _) =>
290+
{
291+
detectedType = type;
292+
return true;
293+
});
294+
A.CallTo(() => mockRenderer.Render(A<IFieldRenderContext<TestModel>>._))
295+
.Returns(builder => builder.AddContent(0, "Test"));
296+
297+
var service = new FieldRendererService(new[] { mockRenderer }, _serviceProvider);
298+
var onValueChanged = EventCallback.Factory.Create<object?>(this, _ => { });
299+
var onDependencyChanged = EventCallback.Factory.Create(this, () => { });
300+
301+
// Act
302+
service.RenderField(model, wrapper, onValueChanged, onDependencyChanged);
303+
304+
// Assert
305+
// The GetActualFieldType method detects int from the UnaryExpression
306+
detectedType.ShouldBe(typeof(int));
307+
}
308+
309+
[Fact]
310+
public void RenderField_Should_Detect_Correct_Type_For_Nullable_Type()
311+
{
312+
// Arrange
313+
var model = new TestModel { NullableValue = 42 };
314+
var field = new FieldConfiguration<TestModel, int?>(x => x.NullableValue);
315+
var wrapper = new FieldConfigurationWrapper<TestModel, int?>(field);
316+
Type? detectedType = null;
317+
318+
var mockRenderer = A.Fake<IFieldRenderer>();
319+
A.CallTo(() => mockRenderer.CanRender(A<Type>._, A<IFieldConfiguration<object, object>>._))
320+
.ReturnsLazily((Type type, IFieldConfiguration<object, object> _) =>
321+
{
322+
detectedType = type;
323+
return true;
324+
});
325+
A.CallTo(() => mockRenderer.Render(A<IFieldRenderContext<TestModel>>._))
326+
.Returns(builder => builder.AddContent(0, "Test"));
327+
328+
var service = new FieldRendererService(new[] { mockRenderer }, _serviceProvider);
329+
var onValueChanged = EventCallback.Factory.Create<object?>(this, _ => { });
330+
var onDependencyChanged = EventCallback.Factory.Create(this, () => { });
331+
332+
// Act
333+
service.RenderField(model, wrapper, onValueChanged, onDependencyChanged);
334+
335+
// Assert
336+
detectedType.ShouldBe(typeof(int?));
337+
}
338+
339+
340+
[Fact]
341+
public void RenderField_Should_Use_Correct_ActualFieldType_In_Context()
342+
{
343+
// Arrange
344+
var model = new TestModel { DateCreated = DateTime.Now };
345+
var field = new FieldConfiguration<TestModel, DateTime>(x => x.DateCreated);
346+
var wrapper = new FieldConfigurationWrapper<TestModel, DateTime>(field);
347+
IFieldRenderContext<TestModel>? capturedContext = null;
348+
349+
var mockRenderer = A.Fake<IFieldRenderer>();
350+
A.CallTo(() => mockRenderer.CanRender(typeof(DateTime), A<IFieldConfiguration<object, object>>._))
351+
.Returns(true);
352+
A.CallTo(() => mockRenderer.Render(A<IFieldRenderContext<TestModel>>._))
353+
.ReturnsLazily((IFieldRenderContext<TestModel> ctx) =>
354+
{
355+
capturedContext = ctx;
356+
return builder => builder.AddContent(0, "Test");
357+
});
358+
359+
var service = new FieldRendererService(new[] { mockRenderer }, _serviceProvider);
360+
var onValueChanged = EventCallback.Factory.Create<object?>(this, _ => { });
361+
var onDependencyChanged = EventCallback.Factory.Create(this, () => { });
362+
363+
// Act
364+
service.RenderField(model, wrapper, onValueChanged, onDependencyChanged);
365+
366+
// Assert
367+
capturedContext.ShouldNotBeNull();
368+
capturedContext.ActualFieldType.ShouldBe(typeof(DateTime));
369+
}
370+
371+
372+
public enum TestEnum
373+
{
374+
Active,
375+
Inactive,
376+
Pending
377+
}
378+
379+
public class NestedModel
380+
{
381+
public string NestedProperty { get; set; } = string.Empty;
382+
}
383+
245384
public class TestModel
246385
{
247386
public string? Name { get; set; }
248387
public int Value { get; set; }
388+
public int? NullableValue { get; set; }
389+
public DateTime DateCreated { get; set; }
390+
public DateTime? DateModified { get; set; }
391+
public TestEnum Status { get; set; }
392+
public bool IsActive { get; set; }
393+
public decimal Price { get; set; }
394+
public List<string> Tags { get; set; } = new();
395+
public NestedModel NestedModel { get; set; } = new();
249396
}
250397
}

FormCraft/Forms/Rendering/FieldRendererService.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using Microsoft.AspNetCore.Components;
2+
using System.Linq.Expressions;
3+
using System.Reflection;
24

35
namespace FormCraft;
46

@@ -114,12 +116,26 @@ private static Type GetActualFieldType<TModel>(IFieldConfiguration<TModel, objec
114116
return property?.PropertyType ?? typeof(object);
115117
}
116118

117-
return field.ValueExpression.Body.Type;
119+
var expressionBody = field.ValueExpression.Body;
120+
121+
return expressionBody switch
122+
{
123+
MemberExpression
124+
{
125+
Member: PropertyInfo propertyInfo
126+
} => propertyInfo.PropertyType,
127+
UnaryExpression
128+
{
129+
NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked,
130+
Operand: MemberExpression { Member: PropertyInfo unaryPropertyInfo }
131+
} => unaryPropertyInfo.PropertyType,
132+
_ => expressionBody.Type
133+
};
118134
}
119135

120136
private static object GetCurrentValue<TModel>(TModel model, IFieldConfiguration<TModel, object> field)
121137
{
122138
var getter = field.ValueExpression.Compile();
123139
return getter(model);
124140
}
125-
}
141+
}

0 commit comments

Comments
 (0)