diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs index dfc7f42b31..8934baca73 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs @@ -254,15 +254,30 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers private void GenerateCheckBox(ModelExplorer modelExplorer, TagHelperOutput output) { - if (typeof(bool) != modelExplorer.ModelType) + if (modelExplorer.ModelType == typeof(string)) + { + if (modelExplorer.Model != null) + { + bool potentialBool; + if (!bool.TryParse(modelExplorer.Model.ToString(), out potentialBool)) + { + throw new InvalidOperationException(Resources.FormatInputTagHelper_InvalidStringResult( + ForAttributeName, + modelExplorer.Model.ToString(), + typeof(bool).FullName)); + } + } + } + else if (modelExplorer.ModelType != typeof(bool)) { throw new InvalidOperationException(Resources.FormatInputTagHelper_InvalidExpressionResult( - "", - ForAttributeName, - modelExplorer.ModelType.FullName, - typeof(bool).FullName, - "type", - "checkbox")); + "", + ForAttributeName, + modelExplorer.ModelType.FullName, + typeof(bool).FullName, + typeof(string).FullName, + "type", + "checkbox")); } // Prepare to move attributes from current element to generated just below. diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs index 5ef947f86e..094e3d2d0a 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } /// - /// Unexpected '{1}' expression result type '{2}' for {0}. '{1}' must be of type '{3}' if '{4}' is '{5}'. + /// Unexpected '{1}' expression result type '{2}' for {0}. '{1}' must be of type '{3}' or '{4}' that can be parsed as a '{3}' if '{5}' is '{6}'. /// internal static string InputTagHelper_InvalidExpressionResult { @@ -67,11 +67,27 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } /// - /// Unexpected '{1}' expression result type '{2}' for {0}. '{1}' must be of type '{3}' if '{4}' is '{5}'. + /// Unexpected '{1}' expression result type '{2}' for {0}. '{1}' must be of type '{3}' or '{4}' that can be parsed as a '{3}' if '{5}' is '{6}'. /// - internal static string FormatInputTagHelper_InvalidExpressionResult(object p0, object p1, object p2, object p3, object p4, object p5) + internal static string FormatInputTagHelper_InvalidExpressionResult(object p0, object p1, object p2, object p3, object p4, object p5, object p6) { - return string.Format(CultureInfo.CurrentCulture, GetString("InputTagHelper_InvalidExpressionResult"), p0, p1, p2, p3, p4, p5); + return string.Format(CultureInfo.CurrentCulture, GetString("InputTagHelper_InvalidExpressionResult"), p0, p1, p2, p3, p4, p5, p6); + } + + /// + /// Unexpected expression result value '{1}' for {0}. '{1}' cannot be parsed as a '{2}'. + /// + internal static string InputTagHelper_InvalidStringResult + { + get { return GetString("InputTagHelper_InvalidStringResult"); } + } + + /// + /// Unexpected expression result value '{1}' for {0}. '{1}' cannot be parsed as a '{2}'. + /// + internal static string FormatInputTagHelper_InvalidStringResult(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("InputTagHelper_InvalidStringResult"), p0, p1, p2); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx index ade3082337..48ad0dca73 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Resources.resx @@ -127,7 +127,10 @@ Cannot override the '{1}' attribute for {0}. A {0} with a specified '{1}' must not have attributes starting with '{7}' or an '{2}', '{3}', '{4}', '{5}', or '{6}' attribute. - Unexpected '{1}' expression result type '{2}' for {0}. '{1}' must be of type '{3}' if '{4}' is '{5}'. + Unexpected '{1}' expression result type '{2}' for {0}. '{1}' must be of type '{3}' or '{4}' that can be parsed as a '{3}' if '{5}' is '{6}'. + + + Unexpected expression result value '{1}' for {0}. '{1}' cannot be parsed as a '{2}'. '{1}' must not be null for {0} if '{2}' is '{3}'. diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs index 191fc2d914..6e443c5c5f 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs @@ -113,6 +113,142 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers Assert.Null(output.TagName); // Cleared } + [Theory] + [InlineData("bad")] + [InlineData("notbool")] + public void CheckBoxHandlesNonParsableStringsAsBoolsCorrectly( + string possibleBool) + { + // Arrange + const string content = "original content"; + const string tagName = "input"; + const string forAttributeName = "asp-for"; + + var expected = Resources.FormatInputTagHelper_InvalidStringResult( + forAttributeName, + possibleBool, + typeof(bool).FullName); + + var attributes = new TagHelperAttributeList + { + { "class", "form-control" }, + }; + + var context = new TagHelperContext( + allAttributes: new TagHelperAttributeList( + Enumerable.Empty()), + items: new Dictionary(), + uniqueId: "test"); + var output = new TagHelperOutput( + tagName, + attributes, + getChildContentAsync: (useCachedResult, encoder) => Task.FromResult(result: null)) + { + TagMode = TagMode.SelfClosing, + }; + output.Content.AppendHtml(content); + var htmlGenerator = new TestableHtmlGenerator(new EmptyModelMetadataProvider()); + var tagHelper = GetTagHelper(htmlGenerator, model: possibleBool, propertyName: nameof(Model.IsACar)); + + // Act and Assert + var ex = Assert.Throws(() => tagHelper.Process(context, output)); + Assert.Equal(expected, ex.Message); + } + + [Theory] + [InlineData(10)] + [InlineData(1337)] + public void CheckBoxHandlesInvalidDataTypesCorrectly( + int possibleBool) + { + // Arrange + const string content = "original content"; + const string tagName = "input"; + const string forAttributeName = "asp-for"; + + var expected = Resources.FormatInputTagHelper_InvalidExpressionResult( + "", + forAttributeName, + possibleBool.GetType().FullName, + typeof(bool).FullName, + typeof(string).FullName, + "type", + "checkbox"); + + var attributes = new TagHelperAttributeList + { + { "class", "form-control" }, + }; + + var context = new TagHelperContext( + allAttributes: new TagHelperAttributeList( + Enumerable.Empty()), + items: new Dictionary(), + uniqueId: "test"); + var output = new TagHelperOutput( + tagName, + attributes, + getChildContentAsync: (useCachedResult, encoder) => Task.FromResult(result: null)) + { + TagMode = TagMode.SelfClosing, + }; + output.Content.AppendHtml(content); + var htmlGenerator = new TestableHtmlGenerator(new EmptyModelMetadataProvider()); + var tagHelper = GetTagHelper(htmlGenerator, model: possibleBool, propertyName: nameof(Model.IsACar)); + + // Act and Assert + var ex = Assert.Throws(() => tagHelper.Process(context, output)); + Assert.Equal(expected, ex.Message); + } + + [Theory] + [InlineData("trUE")] + [InlineData("FAlse")] + public void CheckBoxHandlesParsableStringsAsBoolsCorrectly( + string possibleBool) + { + // Arrange + const string content = "original content"; + const string tagName = "input"; + const string isCheckedAttr = " checked=\"HtmlEncode[[checked]]\""; + var isChecked = (bool.Parse(possibleBool) ? isCheckedAttr : string.Empty); + var expectedContent = $"{content}"; + + + var attributes = new TagHelperAttributeList + { + { "class", "form-control" }, + }; + + var context = new TagHelperContext( + allAttributes: new TagHelperAttributeList( + Enumerable.Empty()), + items: new Dictionary(), + uniqueId: "test"); + var output = new TagHelperOutput( + tagName, + attributes, + getChildContentAsync: (useCachedResult, encoder) => Task.FromResult(result: null)) + { + TagMode = TagMode.SelfClosing, + }; + output.Content.AppendHtml(content); + var htmlGenerator = new TestableHtmlGenerator(new EmptyModelMetadataProvider()); + var tagHelper = GetTagHelper(htmlGenerator, model: possibleBool, propertyName: nameof(Model.IsACar)); + + // Act + tagHelper.Process(context, output); + + // Assert + Assert.Empty(output.Attributes); + Assert.Equal(expectedContent, HtmlContentUtilities.HtmlContentToString(output.Content)); + Assert.Equal(TagMode.SelfClosing, output.TagMode); + Assert.Null(output.TagName); + } + // Top-level container (List or Model instance), immediate container type (Model or NestModel), // model accessor, expression path / id, expected value. public static TheoryData TestDataSet