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

Fix TouchBehavior for CollectionView #1815

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

pictos
Copy link
Member

@pictos pictos commented Apr 14, 2024

Description of Change

Remove the IsSet check on ICommunityToolkitBehavior, this check will fail for any control that recycle views, since the behavior on that particular view will be already set, causing it to not update. Removing this check will make sure we can update the BindingContext.

Linked Issues

PR Checklist

  • Has a linked Issue, and the Issue has been approved(bug) or Championed (feature/proposal)
  • Has tests (if omitted, state reason in description)
  • Has samples (if omitted, state reason in description)
  • Rebased on top of main at time of PR
  • Changes adhere to coding standard
  • Documentation created or updated: https://github.com/MicrosoftDocs/CommunityToolkit/pulls

Additional information

@pictos pictos requested a review from a team April 14, 2024 23:10
@@ -21,7 +21,12 @@ internal bool TrySetBindingContextToAttachedViewBindingContext()
throw new InvalidOperationException($"{nameof(ICommunityToolkitBehavior<TView>)} can only be used for a {nameof(Behavior)}");
}

if (behavior.IsSet(BindableObject.BindingContextProperty) || View is null)
Copy link
Contributor

Choose a reason for hiding this comment

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

What will happen in scenarios where a developer explicitly sets their own BindingContext for the behavior? I think this could would overwrite it with the BindingContext from the View?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Agreed. I added behavior.IsSet when working on the TouchBehavior PR for two reasons here:

  1. To ensure we don't override a manually set BindingContext
  2. If a Behavior is reused and attached to a second View, it will
    maintain the BindingContext of the first View it is attached to

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps the safer change is to remove the binding altogether and subscribe to the BindingContext changed event on the View and assign to the Behavior?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not being able to re-assing the BindingContext will make all Behaviors un-usable on recycling/virtualization scenarios.

What do you think the correct approach would be?

Copy link
Contributor

Choose a reason for hiding this comment

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

I have changed the OnAttachedTo method in BaseBehavior to look as follows:

protected override void OnAttachedTo(TView bindable)
{
	base.OnAttachedTo(bindable);

	bindable.PropertyChanging += (sender, args) =>
	{
		var currentViewBindingContext = ((TView)sender).BindingContext;
			
		if (args.PropertyName == nameof(BindingContext) && 
		    ReferenceEquals(this.BindingContext, currentViewBindingContext))
		{
			this.BindingContext = null;
		}
	};
		
	bindable.PropertyChanged += (sender, args) =>
	{
		if (args.PropertyName == nameof(BindingContext) &&
		    this.BindingContext is null)
		{
			this.BindingContext = ((TView?)sender)?.BindingContext;				
		}
    };
}

It might not be the prettiest but I believe it should work. What does everyone think? Any ideas on how to make it nicer?

Copy link

Choose a reason for hiding this comment

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

I have reverted the change and added in two examples from @tranb3r example repo. Perhaps @tranb3r you can test this sample and let us know how it compares to your expectation. The code works as I expect on iOS/macOS in our sample app

These are indeed the same cases as in my repro. Not working on android, unless using 8.0.0.

Copy link
Contributor

Choose a reason for hiding this comment

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

@tranb3r thank you for confirming.

@tranb3r and @pictos the changes that I have just pushed appear to solve the issue on Android based on my testing.

Copy link

Choose a reason for hiding this comment

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

Sorry to ask again, but I'm still wondering if this BindingContext auto-set feature (introduced in 8.0.1) is really required?
It seems to me that 8.0.0 was fine. If the goal was only to simplify xaml code, I'm not sure it's worth it, considering the difficulty you have to figure out how to fix this regression.
I'm probably missing something, but could you please explain which bug did you try to fix in 8.0.1? Or at least post a code sample for the original issue?

Copy link
Contributor

Choose a reason for hiding this comment

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

I recall there was a lot of developers reporting an issue that their bindings didn't work. @brminnick Is this correct?

Copy link

Choose a reason for hiding this comment

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

I suppose their bindings didn't work because they were not setting the BindingContext manually.

bijington and others added 6 commits June 4, 2024 21:08
…ommunityToolkit/Maui into pj/fix-binding-context-for-collections
…gContext and we are inside a CollectionView then we can assume that we are recycling views and need to assign the BindingContext again.
break;
}

if (parent is CollectionView)
Copy link
Member Author

Choose a reason for hiding this comment

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

I believe for testing this is enough for now... But collectionView isn't the only control that may have virtualization

Copy link
Contributor

Choose a reason for hiding this comment

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

Good point!

Copy link
Contributor

Choose a reason for hiding this comment

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

I've been looking for a suitable interface or base class but I don't think I have found one yet. I am thinking ItemsView and possibly IItemsView. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

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

still not enough, because devs can apply virtualization to any kind of control or layout... I'm still didn't get the reason to be so conservative on allowing changing the BindingContext. In other words, we shouldn't prevent the BindingContext to be mutable. You may have cases where the dev reuses the control across the page and wants the control's BindingContext to change when that happens

Copy link
Contributor

Choose a reason for hiding this comment

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

I see that we want to automatically inherit the BindingContext from the View it is attached to only if the BindingContext is not already set. It appears there are issues around checking whether the BindingContext is already set especially when the behavior is used inside something like the CollectionView and the behavior is attached to recycled views.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, but the thing is that after the first set, that value will always be true, and that's causing the issue. Because of that I removed the IsSet check, as mentioned on the PR description.

I think this happens because when setting the new value, they don't use the RemoveBinding method, just BindinContext = someValue, causing the IsSet to never be false again

Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't confirm the instances but I did discover that OnAttachedTo was being called before OnDetachedFrom when the recycling was happening. Which may explain why IsSet is always true after the first time.

@brminnick brminnick added the needs discussion Discuss it on the next Monthly standup label Jun 6, 2024
@dotnet-policy-service dotnet-policy-service bot added stale The author has not responded in over 30 days help wanted This proposal has been approved and is ready to be implemented labels Jul 7, 2024
@brminnick brminnick removed the needs discussion Discuss it on the next Monthly standup label Jul 11, 2024
@BusterIT
Copy link

Just simply push this solution. It is more performant than always set the binding context new:

internal bool TrySetBindingContextToAttachedViewBindingContext()
{
	if (this is not Behavior behavior)
	{
		throw new InvalidOperationException($"{nameof(ICommunityToolkitBehavior<TView>)} can only be used for a {nameof(Behavior)}");
	}

	if (View is null)
	{
		return false;
	}

	if (!behavior.IsSet(BindableObject.BindingContextProperty))
	{
		behavior.SetBinding(BindableObject.BindingContextProperty, new Binding
		{
			Source = View,
			Path = BindableObject.BindingContextProperty.PropertyName
		});
		return true;
	}

	if (!Equals(behavior.BindingContext, View.BindingContext))
	{
		behavior.BindingContext = View.BindingContext;
		return true;
	}

	return false;
}

@sstobinski
Copy link

Hello, what about the review? Isn't the merge already possible?

@DavidV1603
Copy link

Hello, this is really important for my app. Would it be possible to merge this or is there a problem left?

@bijington
Copy link
Contributor

@BusterIT I don't think that solutions works for all use cases. It won't work if you have a behavior attached to a view inside a CollectionView and it's bound to something outside of that view. It will work on the first pass but not if a view is recycled. Unless I am missing something

@bijington
Copy link
Contributor

For the others asking, I don't recall the state of this PR. I can try to take a look at it soon but have any of you tested that this solves your problem?

@BusterIT
Copy link

@BusterIT I don't think that solutions works for all use cases. It won't work if you have a behavior attached to a view inside a CollectionView and it's bound to something outside of that view. It will work on the first pass but not if a view is recycled. Unless I am missing something

While the concerns about handling behaviors in recycled views within a CollectionView are valid, the modifications I introduced aim to ensure the Behavior's BindingContext is initially synchronized with the View's BindingContext more reliably than before. The original code did not address changes to the BindingContext post-initialization, which is a limitation that remains consistent even after my changes.

To address the concerns about recycling:

  1. The check if (!Equals(behavior.BindingContext, View.BindingContext)) ensures that if the View's BindingContext changes, which can happen in a recycled view, it updates the Behavior's BindingContext correspondingly. This means my solution should handle the initial cases more robustly.
  2. The scenario where the binding might need to refresh dynamically due to external bindings outside of the view's direct context still needs consideration. To robustly handle the recycling scenarios, further enhancements might be required, such as hooking into more lifecycle events of the View or using additional observables to monitor changes in the BindingContext actively.

In summary, the modifications do not introduce new limitations but rather provide a foundation on which more robust handling of dynamic contexts and view recycling can be built.

@bijington
Copy link
Contributor

@BusterIT thanks for the detailed response. If you look back through the review comments you will see that the use case that I am referring to was a blocking reason why this PR didn't get merged therefore it is something I think we need to consider. I believe that we can find some common ground between your proposed changes and the desired use cases we need to support 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted This proposal has been approved and is ready to be implemented stale The author has not responded in over 30 days
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[BUG][Regression][8.0.1] TouchBevavior binding not working in a CollectionView [BUG] TouchBehavior crash
7 participants