-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Switch Expander.IsExpanded to a StyledProperty #9979
Switch Expander.IsExpanded to a StyledProperty #9979
Conversation
You can test this PR using the following package version. |
RaisePropertyChanged( | ||
IsExpandedProperty, | ||
oldValue: value, | ||
newValue: !value, | ||
BindingPriority.LocalValue, | ||
isEffectiveValue: true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's pretty weird that this can be done for a StyledProperty
, which is meant to automatically manage value storage and change events.
This method call is claiming that a) the new value has local priority and b) that it's an effective value change. But the actual property might not agree with this. What happens when IsExpanded
has a two-way binding within a template, for example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about it some more, this is a local solution to a general problem. The problem is that when two-way bindings transfer a new value in either direction, they assume that it was accepted by the recipient.
If the recipient object changes the value to something other than the one that was set, a property changed event is raised and the binding can update the sender object. But if the recipient object instead ignores the new value (as is the case here), then no event is raised and no action is taken.
Hopefully we can go with the local fix here, but this is something that should also be considered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think everyone agrees that this is non-ideal and kind-of 'hacky'. It was first discussed here: #9555 (comment).
This is why I was hoping coercion would handle this automatically for us but unfortunately it doesn't. Therefore, I had to add this back (you can remove it and test the control catalog to see the issues). I think WPF has issues here as well but for sure it would be better to improve coercion and actually handle the real property value that comes back after coercion itself.
For now, it must be done to ensure external code state is correct. (It is essentially just used to notify of a property changed event and force updates) Keep in mind the expander itself isn't the main 'source' of the IsExpanded property -- it is actually the checked state of a toggle button in the control template... so we have to be careful about this.
The problem is that when two-way bindings transfer a new value in either direction, they assume that it was accepted by the recipient.
Yes, that's the real issue. One I think exists in WPF as well.
the new value has local priority
That's something to think about some more I suppose. I copy pasted the code and when it was a direct-property LocalValue made sense. Now perhaps it doesn't. Since it isn't actually setting the value here though -- maybe it's harmless the value used here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is why I was hoping coercion would handle this automatically for us but unfortunately it doesn't.
Yeah I think we should probably make coercion handle this if it's not too difficult. What doesn't work with coercion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What doesn't work with coercion?
Here was a nice summary:
If the recipient object changes the value to something other than the one that was set, a property changed event is raised and the binding can update the sender object. But if the recipient object instead ignores the new value (as is the case here), then no event is raised and no action is taken.
Basically when a cancel happens in the coerce method the property value is reverted back to its original value. This means the value hasn't actually changed and no change notification happens -- seems to make sense. However, if two controls are two-way bound this actually causes a desync. Because the sender will still change its state.
My simple understanding (without looking at any control flow) is:
- Sender changes value (say true -> false)
- Receiver coerces value
- Coercion raises cancel events and external code cancels the change (value switch false -> true)
- The value 'true' is set to the control... but this is the same as it was before. Therefore no change notification and updates.
- The sender still has a false value instead of true because it never got a change notification because the value didn't actually change.
So an additional check is needed to see if coercion changes the value to a value other than what went into the coerce method. If so, and there is a two-way binding, it needs to notify the sender of a change even when the value set to the control is actually the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, and the ViewModel case is the issue I've seen in WPF before if I recall correctly. So this is an old problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still having a bit of trouble understanding. What are the sender and receiver in the case of Expander.IsExpanded
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@grokys In the example I gave:
- sender : This is the IsChecked property of the ToggleButton within the control template.
- receiver : The actual IsExpanded property of the Expander control itself
This is a general problem though with two-way binding if the "receiver" doesn't accept the sender's value for whatever reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, thanks for the clarification, that makes sense.
The problem with the RaisePropertyChanged
hack here is that this method is internal
so the hack will only work within Avalonia itself. We probably need to work out how to do this properly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Supporting cancellation in events is a fairly common practice from my experience and the API is mostly standardized as discussed above. It really should be supported in a UI framework IMO. Flyout is another example. If it currently requires an internal-use-only work-around for now that seems justifiable to me (especially a harmless one). The issue with the property system has existed for almost 20 years.
It's common in some cases to need to raise a property changed notification "hack" in apps as well. Especially due to the view model example above. It might also be possible to promote this idea from a "hack" into a "feature" by allowing a clean way to force a refresh.
Tests are failing because this: public void AddHandler<TEventArgs>(
RoutedEvent<TEventArgs> routedEvent,
EventHandler<TEventArgs>? handler,
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false) where TEventArgs : RoutedEventArgs Is not matching for the public void AddHandler(
RoutedEvent routedEvent,
Delegate handler,
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false) This almost seems like a compiler bug to me. If this really doesn't work there is no point in even having that method generic. |
I think we can probably remove the overload that accepts a |
Unfortunately, it's a deeper than that. The overload that accept a delegate is required to implement IInputElement. I suspect it exists there for a reason as well. I'm not sure the method matching precedence in the compiler but this really doesn't make sense. However, it should be possible to fix this by adding a third method that works only with CancelRoutedEventArgs. It's kind-of a waste but seems to be a necessary work-around. |
This hopefully fixes unit tests due to wrong overload being selected by the compiler.
Perhaps it is. I ran all tests locally on the previous commit in your branch, i.e. without |
All tests are failing in the PR (which you might not be able to see) because it won't compile -- it isn't an actual test failure:
Setting a breakpoint on the AddHandler methods and running the code confirmed that even on my local computer the wrong method is getting called. It's possible that the build server is set to treat these warnings as errors too which is why it isn't showing up locally (I didn't have a compile problem either). Unfortunately, even the new |
I explicitly added generic parameters to the Not sure why error CS8604 is appearing on the CI but not in local builds. Another thing to fix! |
@tomenscape Well, I feel dumb and should have double checked it wasnt something simple. Thanks a lot for taking a look! |
You can test this PR using the following package version. |
You can test this PR using the following package version. |
Do I understand it right, the only problem with this PR is RaisePropertyChanged hack? |
Yes
I've created a new issue: #10092. It will take a while before I could dig into the property system and attempt a fix. Otherwise, I'm probably not the best person to do it (I do have to learn the internals here better someday though). This really does seem harmless for now to me. |
You can test this PR using the following package version. |
What does the pull request do?
Switches the Expander.
IsExpanded
property to aStyledProperty
and adds a newCancelRoutedEventArgs
based onCancelEventArgs
.This is an update to PR #9555 based on discussion in #9944
What is the current behavior?
Expander.
IsExpanded
is aDirectProperty
and cannot be used with Styling.What is the updated/expected behavior with this PR?
IsExpanded
is now a fullStyledProperty
and usable in the styling system.CancelRoutedEventArgs
used for both theExpanding
andCollapsing
events based onCancelEventArgs
.How was the solution implemented (if it's not obvious)?
See code.
I had to go a different direction than what @tomenscape has done in #9944's working branch. It was necessary to add back some of the original code to:
Checklist
[ ] Added unit tests (if possible)?[ ] Consider submitting a PR to https://github.com/AvaloniaUI/Documentation with user documentationBreaking changes
Yes,
StyledProperty
instead ofDirectProperty
but functionality is largely only expanded for app developers.Obsoletions / Deprecations
None
Fixed issues
Relates to #9944