Fix x:Bind function bindings not updating when an argument becomes null - #11732
Open
Sergio Pedri (Sergio0694) wants to merge 3 commits into
Open
Fix x:Bind function bindings not updating when an argument becomes null#11732Sergio Pedri (Sergio0694) wants to merge 3 commits into
Sergio Pedri (Sergio0694) wants to merge 3 commits into
Conversation
A function binding is scheduled either as a child of the step that produces the
instance the method is invoked on - which is only updated while that instance is
non null - or as a dependent of one of its path arguments, which carries no such
guarantee. In the second case the generated Invoke_ method dereferenced the
instance path directly, so {x:Bind Model.Fn(ArgumentOutsideModel)} threw when
Model was null even though every argument resolved.
Retrieve the instance through the same TryGet_ helpers already used for path
arguments, immediately before the call so argument evaluation order is
unchanged, and invoke the method on the retrieved local so the path is walked
once. The retrieval is only emitted where an argument sits outside the instance
path, which is the only shape that can reach a null instance.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…s null
Update_<step> only scheduled the function bindings depending on a step while
that step's own value was non null, so a one way function binding stopped
updating on the very transition that set its argument to null:
<Grid Visibility="{x:Bind local:Helpers.VisibleIfNotNull(Detail), Mode=OneWay}" />
showed the element once Detail was set and then never collapsed it again, with
no binding failure and nothing in the debug output.
A null value is a legitimate argument rather than an unresolved path, and the
generated Invoke_ method already tells the two apart through TryGet_, which only
fails when an intermediate node of the argument path is null. So drop the null
check, matching what the C++/WinRT generator already emitted. FallbackValue
still applies when the path genuinely cannot be resolved, through the null check
that remains on the child steps.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Cover both the update that a null argument has to trigger and the instance retrieval that keeps it safe, in C# and Visual Basic, plus two controls that pin the shapes which must not change: path steps still skip their children when they are null, and a function whose arguments all sit under its instance still calls straight through the path. TestHelper.GenerateCodeBehind now parses the bind universes the way CompileXamlInternal does, since without it an x:Bind has no path steps and no binding code is generated at all. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Collaborator
Member
Author
Member
|
Copilot found a regression in your change. Min repro: <DataTemplate x:DataType="local:ObservableModel">
<Grid>
<local:Formatter x:Name="formatter" />
<TextBlock Text="{x:Bind formatter.Format(Child), Mode=OneWay}" />
</Grid>
</DataTemplate>Output when building: Full repro project (includes other scenarios that Copilot tested): PR11732-RegressionRepro.zip |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Fixes
Fixes #1904
PR Type
Description
A one-way
x:Bindfunction binding stops updating the moment its argument becomesnull. Setting the property to a value works, changing it to a different value works, but clearing it does nothing — the UI keeps whatever it was last given. There's no binding error and nothing in the debug output, so everything points at the view model being wrong when it isn't.The easiest way to hit it is a visibility helper:
The grid appears once
Detailis set, and then can never be hidden again — because hiding it depends on exactly the update that gets skipped. Anything else works the same way: astring?feeding aTextbinding keeps the last non-empty string instead of clearing.This is the same thing reported in #1904 back in 2020, there with a
bool?and a static converter method:I ran both repros from that issue through the compiler, and the generated code goes from skipping the call to always making it.
Current Behavior
The generated code treats a null argument as "this path didn't resolve, don't bother", so it never calls the function:
But null isn't an unresolved path here, it's a perfectly good argument. And the code that actually calls the function already knows the difference — it looks each argument up through a
TryGet_helper that only gives up when something in the middle of the path is null:So that check on the way in is the only thing in the way. C++/WinRT already didn't emit it — C#, VB and C++/CX did.
New Behavior
It's gone, so the function runs and its result is applied:
private void Update_Model_NullableDoublePropertyDP(global::System.Nullable<global::System.Double> obj, int phase) { - if (obj != null) - { - this.Update_Model_M_FunctionReturningNullableDouble_1740600808(phase); - } + this.Update_Model_M_FunctionReturningNullableDouble_1740600808(phase); if ((phase & ((1 << 0) | NOT_PHASED | DATA_CHANGED)) != 0) { // NullableTests.xaml line 39 if (!isobj5NullableDoubleDPDisabled) { XamlBindingSetters.Set_BindTestbedModel_NullablePropertiesButton_NullableDoubleDP(this.obj5, obj, null); } } }One thing that had to be fixed first
Removing that check on its own would have swapped a stale UI for a crash in one case, so the first commit closes that hole.
When a function is called on something —
{x:Bind Model.Format(...)}rather than a static helper — the generated code walked straight to it without checking it was there:That's fine as long as the only way in is through
Modelitself, which the compiler does check. But a binding can also be woken up by one of its arguments, and an argument doesn't have to live underModel.{x:Bind Model.Format(Title)}with a nullModelthrows today the momentTitlehas a value; with the rest of this change it would also throw when both are null, which is a very ordinary state for a page that hasn't loaded its data yet.So the instance now goes through the same lookup the arguments already use, and the call is made on what comes back:
private void Invoke_Model_M_FunctionOnModelOneStringArg_1668010001(int phase) { global::System.String p0; if (!TryGet_BindTestbedModel_DataModel_StaticStringProperty(out p0)) { return; } - global::System.String result = this.dataRoot.Model.FunctionOnModelOneStringArg(p0); + global::BindTestbedModel.DataModel instance; + if (!TryGet_Model(out instance) || instance == null) { return; } + global::System.String result = instance.FunctionOnModelOneStringArg(p0); ... }This is only emitted where an argument can reach the binding from outside the instance's own path — one binding across all four testbeds — so everything else generates exactly as before.
Customer Impact
User facing. Three things change for apps:
FallbackValueno longer kicks in when an argument is simply null. It still applies when the path genuinely can't be resolved — a null somewhere in the middle — which lines up with how a plain binding behaves, where a null value at the end gives youTargetNullValueand only a broken path gives youFallbackValue. This is the one behaviour someone could have been relying on, and it's also the workaround people reach for when they hit the bug in the first place.Regression Potential
The check that was removed only ever guarded function bindings, and only on the way in. Everything else keeps its null checks, including walking down a path, and there are tests pinning that. C++/WinRT has generated the new shape all along, so this brings the other three languages in line rather than inventing something.
How Has This Been Tested?
New codegen tests in
FunctionBindingCodegenTests.cscover C# and VB, plus two controls pinning the shapes that must not change. Confirmed against a locally built compiler, before and after:The checked-in expected-codegen masters are updated to match, and the compiler builds clean for both target frameworks.