[Feature] String should be an acceptable value for a checkbox if it can be parsed as a boolean#5845
[Feature] String should be an acceptable value for a checkbox if it can be parsed as a boolean#5845dougbu merged 9 commits intoaspnet:devfrom Elfocrash:dev
Conversation
|
Hi @Elfocrash, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution! TTYL, DNFBOT; |
|
Hi @Elfocrash can you share more details about this PR? I believe the reason that MVC requires Boolean-looking values is that MVC's model binding feature only works with those values. In fact, it has to do extra stuff such as rendering a second "hidden checkbox" (really a hidden input field) to work with the whole system. I'm concerned that with this change, it'll be easy for developers to render content that won't "round trip" when it's posted back to the server. |
|
Hello @Eilon. With this change, you don't need to mess with creating extra inputs and keeping track of many things in order to have model binding working. Let me know if something is unclear and i will try to explain it better. |
|
@Elfocrash thanks, I think I understand the scenario. The concern I have is that the most common case with checkboxes is with Boolean values, and that this change would make it easy for developers to create unexpected behavior (because of the incompatibility with model binding). |
|
I see. Well if it doesn't make sense to do, then i will solve this locally with a custom tag helper. Thanks for considering. |
|
@Elfocrash I'll discuss with some others on the team before we make any final decision. We definitely appreciate your work and we want to make sure we balance compatibility and flexibility. |
|
@Eilon Great to hear. Locking something like a checkbox on a very specific datatype feels as a restriction, especially with the level of flexibility that C# and .NET offers as a whole. |
|
Ok I talked to @rynowak and @danroth27 and they convinced me this is a good change 😄 I had actually misunderstood part of this change, so they helped me understand it better. @Elfocrash do you think you could add a unit test to cover this new scenario, both for positive and negative cases? (E.g. using the string |
| if (modelExplorer.ModelType == typeof(string)) | ||
| { | ||
| bool potentialBool; | ||
| isValidBool = bool.TryParse(modelExplorer.Model.ToString(), out potentialBool); |
|
@dougbu, assigning to you for model binding / HTML rendering expertise. |
…ool logic for the checkbox
| try | ||
| { | ||
| // Act | ||
| tagHelper.Process(context, output); |
There was a problem hiding this comment.
I think there's some kind of Assert.Throws(...) helper, no? Might want to scan through some other tests to see the pattern. It can make the code a bit cleaner.
There was a problem hiding this comment.
Good point, i tend to use mstest were you normally do ExpectedException 😄. Just a sec
| [InlineData("trUE")] | ||
| [InlineData("FAlse")] | ||
| public void CheckBoxHandlesParsableStringsAsBoolsCorrectly( | ||
| string possibleBool) |
|
@Elfocrash looking very good, thanks for submitting the tests! I think just some small changes are needed. |
|
@Eilon Did all the changes. Let me know if there is something more that needs to be done, thanks. |
| string possibleBool) | ||
| { | ||
| // Arrange | ||
| var originalContent = "original content"; |
There was a problem hiding this comment.
BTW if these aren't really variables, and they're just constants, you could do const string ... instead. That way it's clearer that they're never modified.
There was a problem hiding this comment.
@Eilon we rarely use const in our test code beyond a few test classes for confirming our handling of consts and occasional values used in multiple assertions and shared (complicated) setup methods. Thoughts?
There was a problem hiding this comment.
These values really are constants, and they really aren't variables, so I can't think of even one good reason to avoid using a const.
| var ex = Record.Exception(() => tagHelper.Process(context, output)); | ||
|
|
||
| // Assert | ||
| Assert.Null(ex); |
There was a problem hiding this comment.
So this asserts that there was no exception, but where does it check that the right thing got rendered? We could presumably do something similar to how other checkbox tag helpers work? (I'm hoping we have some similar tests for this!)
There was a problem hiding this comment.
We intentionally have very few "does not throw" tests and definitely don't use Record.Exception() to confirm the call records nothing. So, please set up an expectation similar to https://github.com/Elfocrash/Mvc/blob/6edb9c9fce2b5d78ed3d533df71abd18495f8d5b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs#L86-L88 and assert that's what comes out.
|
@Elfocrash I think it's really close! |
dougbu
left a comment
There was a problem hiding this comment.
Thanks for removing this restriction. PR is close.
| isValidBool = bool.TryParse(modelExplorer.Model?.ToString() ?? string.Empty, out potentialBool); | ||
| } | ||
|
|
||
| if (typeof(bool) != modelExplorer.ModelType && !isValidBool) |
There was a problem hiding this comment.
This makes sense given the history of the code but will be a bit easier to read as
var isValidBool = false;
if (modelExplorer.ModelType == typeof(bool))
{
isValidBool = true;
}
else if (modelExplorer.ModelType == typeof(string))
{
// what you've got
}
if (!isValidBool)
// et cetera|
|
||
| if (typeof(bool) != modelExplorer.ModelType && !isValidBool) | ||
| { | ||
| throw new InvalidOperationException(Resources.FormatInputTagHelper_InvalidExpressionResult( |
There was a problem hiding this comment.
Please update the Resources.resx file and reword this. Resource currently says only that the model must be of type bool.
Better yet, update the resource to state the model must of type bool or string and add a new one for the string that can't be parsed as a bool e.g.
if (modelExplorer.ModelType == typeof(string))
{
if (modelExplorer.Model != null)
{
// throw with new resource if parsing fails
}
}
else if (modelExplorer.ModelType != typeof(bool))
{
// throw with updated resource, passing one more parameter
}| string possibleBool) | ||
| { | ||
| // Arrange | ||
| var originalContent = "original content"; |
There was a problem hiding this comment.
@Eilon we rarely use const in our test code beyond a few test classes for confirming our handling of consts and occasional values used in multiple assertions and shared (complicated) setup methods. Thoughts?
| var ex = Record.Exception(() => tagHelper.Process(context, output)); | ||
|
|
||
| // Assert | ||
| Assert.Null(ex); |
There was a problem hiding this comment.
We intentionally have very few "does not throw" tests and definitely don't use Record.Exception() to confirm the call records nothing. So, please set up an expectation similar to https://github.com/Elfocrash/Mvc/blob/6edb9c9fce2b5d78ed3d533df71abd18495f8d5b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/InputTagHelperTest.cs#L86-L88 and assert that's what comes out.
|
@doubgu will do, thanks |
… exception and added content expectation and reorganized the code.
| } | ||
| else if (modelExplorer.ModelType != typeof(bool)) | ||
| { | ||
| throw new InvalidOperationException(Resources.FormatInputTagHelper_InvalidExpressionResult( |
There was a problem hiding this comment.
A new record for number of string.Format substitutions, perhaps?? 😄
| var expectedContent = $"{content}<input{isChecked} class=\"HtmlEncode[[form-control]]\" " + | ||
| "id=\"HtmlEncode[[IsACar]]\" name=\"HtmlEncode[[IsACar]]\" type=\"HtmlEncode[[checkbox]]\" " + | ||
| "value=\"HtmlEncode[[true]]\" /><input name=\"HtmlEncode[[IsACar]]\" type=\"HtmlEncode[[hidden]]\" " + | ||
| "value=\"HtmlEncode[[false]]\" />"; |
There was a problem hiding this comment.
Just indent 4 spaces from the previous line.
| <value>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}'.</value> | ||
| </data> | ||
| <data name="InputTagHelper_InvalidStringResult" xml:space="preserve"> | ||
| <value>Unexpected '{1}' expression result type '{2}' for {0}. '{1}' is of type '{3}' for {5} but cannot be parsed as a '{4}'.</value> |
There was a problem hiding this comment.
The result type is fine. This should be more like Unexpected expression result value '{1}' for {0}. '{1}' cannot be parsed as a '{2}'.
| "type", | ||
| "checkbox")); | ||
| } | ||
|
|
There was a problem hiding this comment.
I wasn't clear enough in my earlier comments. This block and the one above can be combined. No need for the isValidBool variable. Then you're only checking the types once.
If the asp-for value is string with the values "true" or "false" the code is able to process that as valid boolean and generate a proper checkbox around that.
However the typeof check in the GenerateCheckBox method prevents that.
Added a check that allows the value to be processed and create a checkbox if the value is a valid bool string.