Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

[Feature] String should be an acceptable value for a checkbox if it can be parsed as a boolean#5845

Merged
dougbu merged 9 commits intoaspnet:devfrom
Elfocrash:dev
Mar 8, 2017
Merged

[Feature] String should be an acceptable value for a checkbox if it can be parsed as a boolean#5845
dougbu merged 9 commits intoaspnet:devfrom
Elfocrash:dev

Conversation

@Elfocrash
Copy link
Copy Markdown
Contributor

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.

@dnfclas
Copy link
Copy Markdown

dnfclas commented Feb 22, 2017

Hi @Elfocrash, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution!
You've already signed the contribution license agreement. Thanks!
We will now validate the agreement and then real humans will evaluate your PR.

TTYL, DNFBOT;

@Eilon
Copy link
Copy Markdown
Contributor

Eilon commented Feb 28, 2017

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.

@Elfocrash
Copy link
Copy Markdown
Contributor Author

Hello @Eilon.
I totally understand what you mean. My point is, it is not uncommon that some generic configuration values are saved as string are retrieved as string on a key-value/string-string pattern.
When you want to load them up and alter them you would have to parse true or false string values as bools in order for the input tag helper to render it as a checkbox but then for the asp-for value you have to use the parsed parameter, making keeping track of the original property impossible.
The way i work around it is i create a second hidden input that has the string value and with js i change box the visible and the hidden in order for my changes to be saved.

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.

@Eilon
Copy link
Copy Markdown
Contributor

Eilon commented Mar 4, 2017

@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).

@Elfocrash
Copy link
Copy Markdown
Contributor Author

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.

@Eilon
Copy link
Copy Markdown
Contributor

Eilon commented Mar 5, 2017

@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.

@Elfocrash
Copy link
Copy Markdown
Contributor Author

@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.

@Eilon
Copy link
Copy Markdown
Contributor

Eilon commented Mar 7, 2017

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 trUE should work, and using the string bad would be ignored.)

if (modelExplorer.ModelType == typeof(string))
{
bool potentialBool;
isValidBool = bool.TryParse(modelExplorer.Model.ToString(), out potentialBool);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can Model be null here?

@Eilon
Copy link
Copy Markdown
Contributor

Eilon commented Mar 7, 2017

@dougbu, assigning to you for model binding / HTML rendering expertise.

try
{
// Act
tagHelper.Process(context, output);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, i tend to use mstest were you normally do ExpectedException 😄. Just a sec

[InlineData("trUE")]
[InlineData("FAlse")]
public void CheckBoxHandlesParsableStringsAsBoolsCorrectly(
string possibleBool)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indent this 😄

@Eilon
Copy link
Copy Markdown
Contributor

Eilon commented Mar 7, 2017

@Elfocrash looking very good, thanks for submitting the tests! I think just some small changes are needed.

@Elfocrash
Copy link
Copy Markdown
Contributor Author

Elfocrash commented Mar 7, 2017

@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";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Eilon
Copy link
Copy Markdown
Contributor

Eilon commented Mar 8, 2017

@Elfocrash I think it's really close!

Copy link
Copy Markdown
Contributor

@dougbu dougbu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for removing this restriction. PR is close.

isValidBool = bool.TryParse(modelExplorer.Model?.ToString() ?? string.Empty, out potentialBool);
}

if (typeof(bool) != modelExplorer.ModelType && !isValidBool)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor Author

@doubgu will do, thanks

@Elfocrash
Copy link
Copy Markdown
Contributor Author

Elfocrash commented Mar 8, 2017

@Eilon @dougbu All changes are now in place.

Copy link
Copy Markdown
Contributor

@Eilon Eilon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

}
else if (modelExplorer.ModelType != typeof(bool))
{
throw new InvalidOperationException(Resources.FormatInputTagHelper_InvalidExpressionResult(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new record for number of string.Format substitutions, perhaps?? 😄

Copy link
Copy Markdown
Contributor

@dougbu dougbu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smaller touch-ups left.

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]]\" />";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@dougbu dougbu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit: LGTM

I have a small question on the resources for @Eilon (offline) but expect to get this in soon.

@dougbu dougbu merged commit 015dafc into aspnet:dev Mar 8, 2017
@dougbu
Copy link
Copy Markdown
Contributor

dougbu commented Mar 8, 2017

015dafc

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants