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

Clear ReferenceNode ParamNames before serializing each expression string to avoid collisions #4183

Conversation

arcadiogarcia
Copy link
Member

Fixes

PR Type

What kind of change does this PR introduce?

  • Bugfix

This is a fix that prevents name collisions when nodes/subtrees are reused to build more than one expression string. It is not completely clear to me if reusing nodes/subtrees to build multiple expression strings is a explicitly supported scenario, but the docs don't warn against it and it is imo desirable to:

  • Avoid having to rebuild the same tree of nodes over and over when animating collections of elements
  • Avoid having to rebuild the same tree of nodes for animating properties that share an "animation subtree"
  • Stay in the realm of ReferenceNodes in our animation classes, rather than pass them the whole InteractionTracker/PropertySet (giving them "write access") just so they can call GetReference() as many times as they need.

As far as I can tell, the underlying issue has already existed independently of whether we use the "InteractionTracker_1", guids or alphabetical identifiers, but it is now easier to hit since my previous PR #4112 makes the ids sequential.

What is the current behavior?

Currently, if you try to reuse a ReferenceNode across many animations, we only set the ParamName of each node once and we reuse the old name on future animations. Since ParamNames are only guaranteed to be unique for each call to EnsureReferenceInfo, this can result in two nodes having the same ParamName, e.g.

            //Create tracker and properties
            var visual = ElementCompositionPreview.GetElementVisual(this);
            var compositor = visual.Compositor;

            var tracker = InteractionTracker.Create(compositor);

            var properties = compositor.CreatePropertySet();
            const string SomeProperty = nameof(SomeProperty);
            properties.InsertBoolean(SomeProperty, false);

            var propertiesReference = properties.GetReference();
            var trackerReference = tracker.GetReference();

            //Create an expression tree that contains one of the references - it will be named A
            var opacityAnimation = ExpressionFunctions.Conditional(propertiesReference.GetBooleanProperty(SomeProperty), 1, 0);
            visual.StartAnimation("Opacity", opacityAnimation);
            //Resolves to ((A.SomeProperty) ? 1 : (0))

            //Create an expression tree that contains both references - one is already named A, the other will be named A
            var positionAnimation = ExpressionFunctions.Conditional(propertiesReference.GetBooleanProperty(SomeProperty), trackerReference.Position.X, 0);
            visual.StartAnimation("Offset.X", positionAnimation);
            // Resolves to ((A.SomeProperty) ? (A.Position.X) : (0))

throws

System.ArgumentException
  HResult=0x80070057
  Message=The parameter is incorrect.

The specified property was not found or cannot be referenced in an expression.
Context: SomeProperty
Expression: ((A.SomeProperty) ? (A.Position.X) : (0))
...

This means that in practice it is dangerous to reuse references nodes as collisions might happen in a way that is hard to predict, depending on the tree of each animation and the order in which they start. E.g. in the previous example, the code runs fine if we swap the order of the StartAnimation calls.

What is the new behavior?

ClearReferenceInfo is always called before ToExpressionString, wiping the ParamName and _objRefList in each node in the tree. This has a small cost since it involves traversing the tree, but guarantees that we don't reuse stale data from previous calls.

An alternative solution would be to clear the data after/at the end of each ToExpressionString call, to guarantee that the nodes are always in a clean state.

PR Checklist

Please check if your PR fulfills the following requirements:

  • Tested code with current supported SDKs
  • Pull Request has been submitted to the documentation repository instructions. Link:
  • Sample in sample app has been added / updated (for bug fixes / features)
  • New major technical changes in the toolkit have or will be added to the Wiki e.g. build changes, source generators, testing infrastructure, sample creation changes, etc...
  • Tests for the changes have been added (for bug fixes / features) (if applicable)
  • Header has been added to all new source files (run build/UpdateHeaders.bat)
  • Contains NO breaking changes

Other information

@ghost
Copy link

ghost commented Aug 18, 2021

Thanks arcadiogarcia for opening a Pull Request! The reviewers will test the PR and highlight if there is any conflict or changes required. If the PR is approved we will proceed to merge the pull request 🙌

@ghost ghost requested review from azchohfi and Rosuavio August 18, 2021 00:57
@michael-hawker michael-hawker added this to the 7.1 milestone Aug 18, 2021
Copy link
Member

@Sergio0694 Sergio0694 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! I guess there's a tiny bit of overhead due to the recursive reset for each call now, but (1) each call is very very short, (2) trees are generally very small anyway given that they're manually built for animations and (3) they're never created in hot-paths anyway, as one would just create an expression animation once and forget about it.

Thanks for the fix! 🙌

@michael-hawker
Copy link
Member

michael-hawker commented Sep 9, 2021

@arcadiogarcia I'm not sure what happened here. I merged your other PR #4189 and then resolved a conflict there, but the other half of c4f6009 seems to be missing now that uses the new function???

I think you had your two branches based off one another or something and had a remove commit in the other one, so now that's part of our history from the other merge and causing it to be missing in this one...

Can you reset your fork's main, rebase your branch on top of it, but then only take the relevant commits to your ClearReference to fix the PR?

Copy link
Member

@michael-hawker michael-hawker left a comment

Choose a reason for hiding this comment

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

Invalid history because of history of #4189

@ghost
Copy link

ghost commented Sep 9, 2021

This PR has been marked as "needs attention 👋" and awaiting a response from the team.

@arcadiogarcia
Copy link
Member Author

I just force pushed a new history for this branch that has a new commit with the same changes, should be good to go.

@michael-hawker michael-hawker merged commit 07687af into CommunityToolkit:main Sep 14, 2021
@michael-hawker
Copy link
Member

Thanks @arcadiogarcia! Will double-check everything looks good post-merge and we should be good.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants