Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

For two-way bindings, enforce consistency between .NET model and DOM by patching old tree. Fixes #8204 #11438

Merged
merged 4 commits into from
Jun 24, 2019

Conversation

SteveSandersonMS
Copy link
Member

This is the rather nontrivial solution to a multitude of related async-binding and diff-inconsistency issues. As of this change, two-way bindings have a stricter behavior whereby they:

  • Find out from the JS code what the latest state in the DOM is, and apply that to the old render tree
  • Consequently, produce a diff output that may be smaller (no need to tell the client to perform changes it has already performed, which fixes the laggy typing thing), or may be larger (maybe we need to tell the client to revert its change, which fixes the "moving checkboxes" case we had reported by a customer)

In most ways this is purely advantageous. Various cases where the behavior was bizarre or broken before are now fixed (e.g., binding a checkbox to a property whose value is fixed as true or false before didn't reflect that in the UI - now the checkbox will forcibly revert itself to "checked" or "unchecked" as appropriate"). Also it's much safer to revert invalid edits to number fields, because now the user can see exactly how their input is being interpreted - previously they had no way to know if their edit was being ignored because it was considered invalid.

However the extra strictness will make developers go through extra hoops in a few cases. The most notable one is textboxes bound to non-nullable int/double/etc that use bind-value:event="oninput". These will no longer allow unparseable values such as being empty or just containing -, so developers will have to either:

  • Not use bind-value:event="oninput" for non-nullable numbers, since you don't want to prevent the invalid intermediate states. Just use the default change event which is "change".
  • Or, use a nullable version of your number, or even bind to a string so you can do whatever you want with the invalid intermediate states
  • Or, use <InputNumber> which already handles this nicely. In the future we might extend that to support bind-value:event="oninput", and it will do what you expect because behind the scenes it's binding to a string and handling parsing separately so it can record validation errors.

@SteveSandersonMS
Copy link
Member Author

@dougbu This PR contains some of the commits from dotnet-maestro, because I'm waiting for #11307. As long as the build for this PR passes, I think it should be fine to merge those commits as part of this PR. Or if #11307 gets merged first, it will make no difference after being rebased.

@SteveSandersonMS SteveSandersonMS added the area-blazor Includes: Blazor, Razor Components label Jun 21, 2019
@SteveSandersonMS SteveSandersonMS added this to the 3.0.0-preview7 milestone Jun 21, 2019
@SteveSandersonMS SteveSandersonMS force-pushed the stevesa/fix-tree-mutations-during-events branch 2 times, most recently from 5547c3c to 57ff22d Compare June 21, 2019 14:02
@SteveSandersonMS
Copy link
Member Author

@aspnet-hello
Copy link

This comment was made automatically. If there is a problem contact aspnetcore-build@microsoft.com.

I've triaged the above build. I've created/commented on the following issue(s)
https://github.com/aspnet/AspNetCore-Internal/issues/2707

@SteveSandersonMS SteveSandersonMS force-pushed the stevesa/fix-tree-mutations-during-events branch from 57ff22d to fbffba2 Compare June 21, 2019 14:50
@dougbu
Copy link
Member

dougbu commented Jun 22, 2019

@SteveSandersonMS I see you force pushed the branch and no dependency updates remain. Do you still need anything from me here?

@SteveSandersonMS
Copy link
Member Author

@dougbu No, nothing else needed. Unless you’re part of the BRT or whatever the current equivalent to that is, because there is still https://github.com/aspnet/AspNetCore-Internal/issues/2707

@SteveSandersonMS SteveSandersonMS force-pushed the stevesa/fix-tree-mutations-during-events branch from fbffba2 to 6d88845 Compare June 24, 2019 09:10
{
// We only allow the client to supply string or bool currently, since those are the only kinds of
// values we output on attributes that go to the client
if (!(newFieldValue is string || newFieldValue is bool))
Copy link
Member

Choose a reason for hiding this comment

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

In which case can this not be either an int or bool, wouldn't that signal that something went terribly wrong? (Like could a number get in here somehow?)

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point.

I'm not aware of any valid way that the value here might be something other than string/bool. If it was, we could just throw. Maybe that would be clearer.

Copy link
Member

Choose a reason for hiding this comment

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

Looking at your tests, it's fine if this simply returns as the value comes from the client and is untrusted

Copy link
Contributor

Choose a reason for hiding this comment

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

This might help a bit - #11517

/// <returns>
/// A <see cref="Task"/> which will complete once all asynchronous processing related to the event
/// has completed.
/// </returns>
public virtual Task DispatchEventAsync(int eventHandlerId, UIEventArgs eventArgs)
public virtual Task DispatchEventAsync(int eventHandlerId, EventFieldInfo fieldInfo, UIEventArgs eventArgs)
Copy link
Member

Choose a reason for hiding this comment

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

Is the expectation that this chain grows bigger than 1 when there are rapidly firing events?

Copy link
Member Author

Choose a reason for hiding this comment

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

The chain of replaced event handler IDs? If so, yes, it can grow beyond 1.

Assert.NotEqual(0, attributeFrame.AttributeEventHandlerId);
Assert.NotEqual(eventHandlerId, attributeFrame.AttributeEventHandlerId);
});
}
Copy link
Member

Choose a reason for hiding this comment

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

Would this for example be a case where there are multiple events fired with the old event handler (like typing fast or moving the cursor quick) and the render batches have not been produced yet?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, exactly!

Copy link
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

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

Great set of changes.

I didn't get to follow all the details because its very low-level and likely requires some debugging to understand it fully, but the changes look solid and the test coverage is appropriate.

Great job!

@SteveSandersonMS SteveSandersonMS force-pushed the stevesa/fix-tree-mutations-during-events branch from 6d88845 to 1335f41 Compare June 24, 2019 19:07
{
public EventFieldInfo() { }
public int ComponentId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public object FieldValue { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
Copy link
Contributor

Choose a reason for hiding this comment

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

Filed #11517 in case we have interest in doing the work here.

// to process invalid input but avoid dirtying the state of the component if can't be converted. Imagine if
// we assigned default(T) on failure - this would result in trouncing the user's typed in value.
// We only invoke the setter if the conversion didn't throw, or if the newly-entered value is empty.
// If the user entered some non-empty value we couldn't parse, we leave the state of the .NET field
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

{
// We only allow the client to supply string or bool currently, since those are the only kinds of
// values we output on attributes that go to the client
if (!(newFieldValue is string || newFieldValue is bool))
Copy link
Contributor

Choose a reason for hiding this comment

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

This might help a bit - #11517

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants