From 58c13c312eabfb00fdb56c7e0f69813359d2fa9a Mon Sep 17 00:00:00 2001 From: Guillaume Nury Date: Tue, 10 Mar 2020 00:00:35 +0100 Subject: [PATCH] Support nullable enum in InputSelect (#19506) As BindConverter supports Nullable enums (here), call it if the underlying type has type of enum. Addresses #13624 --- src/Components/Web/src/Forms/InputSelect.cs | 4 +- .../Web/test/Forms/InputSelectTest.cs | 153 ++++++++++++++++++ 2 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 src/Components/Web/test/Forms/InputSelectTest.cs diff --git a/src/Components/Web/src/Forms/InputSelect.cs b/src/Components/Web/src/Forms/InputSelect.cs index b8cdbeab1f..e1a3ab0eae 100644 --- a/src/Components/Web/src/Forms/InputSelect.cs +++ b/src/Components/Web/src/Forms/InputSelect.cs @@ -17,6 +17,8 @@ namespace Microsoft.AspNetCore.Components.Forms /// [Parameter] public RenderFragment ChildContent { get; set; } + private readonly Type _nullableUnderlyingType = Nullable.GetUnderlyingType(typeof(TValue)); + /// protected override void BuildRenderTree(RenderTreeBuilder builder) { @@ -38,7 +40,7 @@ namespace Microsoft.AspNetCore.Components.Forms validationErrorMessage = null; return true; } - else if (typeof(TValue).IsEnum) + else if (typeof(TValue).IsEnum || (_nullableUnderlyingType != null && _nullableUnderlyingType.IsEnum)) { var success = BindConverter.TryConvertTo(value, CultureInfo.CurrentCulture, out var parsedValue); if (success) diff --git a/src/Components/Web/test/Forms/InputSelectTest.cs b/src/Components/Web/test/Forms/InputSelectTest.cs new file mode 100644 index 0000000000..7c49fe03be --- /dev/null +++ b/src/Components/Web/test/Forms/InputSelectTest.cs @@ -0,0 +1,153 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Test.Helpers; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Forms +{ + public class InputSelectTest + { + [Fact] + public async Task ParsesCurrentValueWhenUsingNotNullableEnumWithNotEmptyValue() + { + // Arrange + var model = new TestModel(); + var rootComponent = new TestInputSelectHostComponent + { + EditContext = new EditContext(model), + ValueExpression = () => model.NotNullableEnum + }; + var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent); + + // Act + inputSelectComponent.CurrentValueAsString = "Two"; + + // Assert + Assert.Equal(TestEnum.Two, inputSelectComponent.CurrentValue); + } + + [Fact] + public async Task ParsesCurrentValueWhenUsingNotNullableEnumWithEmptyValue() + { + // Arrange + var model = new TestModel(); + var rootComponent = new TestInputSelectHostComponent + { + EditContext = new EditContext(model), + ValueExpression = () => model.NotNullableEnum + }; + var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent); + + // Act + inputSelectComponent.CurrentValueAsString = ""; + + // Assert + Assert.Equal(default, inputSelectComponent.CurrentValue); + } + + [Fact] + public async Task ParsesCurrentValueWhenUsingNullableEnumWithNotEmptyValue() + { + // Arrange + var model = new TestModel(); + var rootComponent = new TestInputSelectHostComponent + { + EditContext = new EditContext(model), + ValueExpression = () => model.NullableEnum + }; + var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent); + + // Act + inputSelectComponent.CurrentValueAsString = "Two"; + + // Assert + Assert.Equal(TestEnum.Two, inputSelectComponent.Value); + } + + [Fact] + public async Task ParsesCurrentValueWhenUsingNullableEnumWithEmptyValue() + { + // Arrange + var model = new TestModel(); + var rootComponent = new TestInputSelectHostComponent + { + EditContext = new EditContext(model), + ValueExpression = () => model.NullableEnum + }; + var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent); + + // Act + inputSelectComponent.CurrentValueAsString = ""; + + // Assert + Assert.Null(inputSelectComponent.CurrentValue); + } + + private static TestInputSelect FindInputSelectComponent(CapturedBatch batch) + => batch.ReferenceFrames + .Where(f => f.FrameType == RenderTreeFrameType.Component) + .Select(f => f.Component) + .OfType>() + .Single(); + + private static async Task> RenderAndGetTestInputComponentAsync(TestInputSelectHostComponent hostComponent) + { + var testRenderer = new TestRenderer(); + var componentId = testRenderer.AssignRootComponentId(hostComponent); + await testRenderer.RenderRootComponentAsync(componentId); + return FindInputSelectComponent(testRenderer.Batches.Single()); + } + + enum TestEnum + { + One, + Two, + Tree + } + + class TestModel + { + public TestEnum NotNullableEnum { get; set; } + + public TestEnum? NullableEnum { get; set; } + } + + class TestInputSelect : InputSelect + { + public new TValue CurrentValue => base.CurrentValue; + + public new string CurrentValueAsString + { + get => base.CurrentValueAsString; + set => base.CurrentValueAsString = value; + } + } + + class TestInputSelectHostComponent : AutoRenderComponent + { + public EditContext EditContext { get; set; } + + public Expression> ValueExpression { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Value", EditContext); + builder.AddAttribute(2, "ChildContent", new RenderFragment(childBuilder => + { + childBuilder.OpenComponent>(0); + childBuilder.AddAttribute(0, "ValueExpression", ValueExpression); + childBuilder.CloseComponent(); + })); + builder.CloseComponent(); + } + } + } +}