Closed robloo closed 1 year ago
DirectProperty is still useful in some cases (read-only properties and lists) and must not go away
direct properties should be reserved only for "data"... which includes content, read-only collections
Enumerable type properties should also be able to be styled. There's been some discussion on this in the past for Grid but that lost traction a while ago (#4397 & #4464). Doing so would also allow moving towards tomenscape's idea, which I don't necessarily disagree with, but I think there's still some use cases for having writable DirectProperties.
This means ALL properties such as
Expander.IsExpanded
andSplitPane.IsPaneOpen
(which are UI state information) would be converted to direct.
(I think you meant "converted to styled")
And to add another property: TextBlock.Text
needs to be styleable.
Also FWIW w/ SplitView, the only reason it's Direct is because I was following Expander's IsExpanded as a model and this was still my early days using Avalonia.
Enumerable type properties should also be able to be styled.
Enumerables and lists/collections are almost a separate topic. It goes all the way back to WPF and SelectedItems
which doesn't support binding at all (without some tricky work-arounds). Avalonia as I understand it solves this problem by passing around IEnumerable and allowing get/set of the property itself. I'm not sure that is a good solution but at least it supports binding...
(I think you meant "converted to styled")
Thanks, fixed.
And to add another property: TextBlock.Text needs to be styleable.
Yea, I'll add it and link to some of those older discussions.
Also FWIW w/ SplitView, the only reason it's Direct is because I was following Expander's IsExpanded as a model and this was still my early days using Avalonia.
Ah, I didn't realize you did that control. If that's the case I can do a PR eventually to switch that one over. It doesn't seem too confrontational.
Separately: Changing Expander.IsExpanded is problematic now because I added cancellation which requires a direct property to intercept before the property is set. We would need an OnPropertyChanging
callback with cancellation to get around this now.
Thanks for making this issue @robloo. I was looking at the new ValueStore
last night and I agree that there doesn't seem to be much of a performance difference between setting a StyledProperty
with local priority and setting a DirectProperty
. It seems that the only remaining benefit of DirectProperty
is providing support for read-only properties.
You have described the issue well, so I don't need to replicate most of what I said. I'll just add these details about the use case at my company:
We are using
ControlTheme
everywhere, and those themes often contains activatable styles. Our use case is actually low-level controls (e.g.Button
) which appear within the templates of high-level controls, and the selectors are something like^[Foo=bar] /template/ #SomeButton
.I understand the problems that come with setting direct properties from this context, but since we never wanted to remove these values, only to switch between multiple different options, that has never been a problem for us.
I would implement this change by first removing DirectProperty.Setter
and AvaloniaObject.SetDirectValueUnchecked
, and then fixing all the compile errors that arise. The fix for each error would be switching to StyledProperty
or removing the setter and making the property read-only. AvaloniaObject.SetAndRaise
(which is a protected method) would remain for use within private/protected C# property setters.
Separately: Changing Expander.IsExpanded is problematic now because I added cancellation which requires a direct property to intercept before the property is set. We would need an
OnPropertyChanging
callback with cancellation to get around this now.
Once the property is changed to StyledProperty
, a coercion method could be executed before the value is written.
I actually mostly agree with this. The recent changes to the value store should at least alleviate some of the previous performance problems, and if performance is still a problem with reading in some cases then the value can be cached in a field.
I think the only remaining problems with styled properties are:
Direct properties should be obviously kept for read-only properties, and read/write direct properties should be kept around for use by client code.
Separately: Changing Expander.IsExpanded is problematic now because I added cancellation which requires a direct property to intercept before the property is set. We would need an OnPropertyChanging callback with cancellation to get around this now.
Yep, as @tomenscape mentioned I think coercion should be able to solve this.
I'm going to make a start on these changes now, since they will prevent us from upgrading to the next preview otherwise.
I can't assign the issue to myself though.
@tomenscape
I'll handle at least Expander by this weekend at the latest. If you need me to do others let me know (was already thinking about TextBlock). I was going to look at doing this on a control-by-control basis to start because I think several controls are doing things in the property setter so it's going to require some thought to change them and review. Will know more once the code is more broadly looked at though.
Once the property is changed to StyledProperty, a coercion method could be executed before the value is written.
Yes, good idea. I spent too much time with UWP/WinUI and often forget to consider coercion like I should.
@grokys
As mentioned, styling collections is a problem we inherited from WPF
I'm going to break this out as a new issue later. This doesn't have to be fully solved to close this issue I think. Conceptually, I've been thinking about it for some time too. The property system needs to itself be aware of and synchronize changes within a type. Even in WPF its really only designed for single values instead of multi-values per property. That line of thought needs a lot of work though and probably a new interface and special, new collection types. Even higher level conceptually, it's going from one dimension to two in the property system.
Styled properties don't currently support data validation. This could be added.
Didn't know that myself. Will definitely have to add that in the future.
I've pushed a branch. It's untested as it doesn't compile yet, but I've made a lot of progress. Remaining types to refactor in the Controls assembly are ItemsControl
(and inheritors), Expander
, and SplitView
.
@robloo I've now completed the first pass of this change. The control gallery compiles and runs, but renders incorrectly. There are also many failing tests which I'm working through next, and I hope that once everything is green the gallery will work again.
I refactored SplitView
, and then when I looked at Expander
realised that they are both solving the same problem. So I went ahead brought the two into line with each other. Both now raise a RoutedEvent
with an explicit Cancel
property. Handling the event is no longer required.
Some classes with weird behaviour that I had to make substantial internal changes to:
SelectingItemsControl
ScrollViewer
(and related types)TextBox
I'll write more about these when the time to open a PR comes.
Had an issue with my phone. Replacing last message...
It's great you are getting so much done here. However, I'm a bit concerned after looking briefly at the changes that this is going deeper than expected in some areas. Generally, I would be uncomfortable making such a large PR changing all this functionality. It's easy to make even a little mistake here and it's even easier to get lost in a giant PR review. I know the core maintainers do this though... and if they can review it I guess it's no issue. However, for me I would be splitting this out into separate PRs especially for controls with deeper changes.
I have some specific comments concerning Expander since I was still planning on working on that... I just don't always have the time during the day or even week to jump into things that come up unexpected. I did commit to this weekend though but you didn't give me a chance.
IsCancelled
or something.It's entirely possible this level of debate is needed for each control. So again, I really advice against doing this all at once.
I'm definitely in favour of slicing this up into multiple PRs. Once everything has settled down I'll start extracting clean commits for different areas of the solution. With sensible management we can then open and complete them one by one without breaking master in between. Most changes are quite boring and can come back at the same time, so there shouldn't be too many separate PRs.
I don't mind what the ultimate design is for cancelling Expander events, I just did something that works and was relatively quick to implement. Feel free to change it later. :)
@tomenscape Ok, perfect, sounds like we are on the same page. Thanks for doing all this, really is great work!
I've hit a bump in this project: there is no equivalent to WPF's DependencyObject.SetCurrentValue
method. This is necessary when a control is internally setting its own StyledProperty
values, because if the property has had its value set by another component then you need to know which BindingPriority
to set your value with. Too low a priority and your new value doesn't become effective, but too high a priority and it overrides any value coming from the binding/style/etc. that wants to set a value too.
A secondary issue is that there are two read-only binding priorities: Unset
and Inherited
. You can't set a value with either of these priorities.
To complicate matters further, bindings don't modify the target property until they change its value. So if you place a binding on a property, and the binding produces the property's default value, the current BindingPriority
remains Unset
even though there is a binding is attached and active.
None of this is an issue for DirectProperty
because that has no concept of priorities.
Here's what WPF does: SetCurrentValue
allows you to provide a new value for any ValueSource
(i.e. BindingPriority
). This includes default/unset! Here is some WPF code that demonstrates this:
var oldSource = DependencyPropertyHelper.GetValueSource(this, DataContextProperty);
SetCurrentValue(DataContextProperty, new object());
var newSource = DependencyPropertyHelper.GetValueSource(this, DataContextProperty);
Debugger output:
oldSource
{System.Windows.ValueSource}
BaseValueSource: Default
IsAnimated: false
IsCoerced: false
IsCurrent: false
IsExpression: false
newSource
{System.Windows.ValueSource}
BaseValueSource: Default
IsAnimated: false
IsCoerced: true
IsCurrent: true
IsExpression: false
As you can see, our value source is still default, but the actual value has changed. It's a weird situation, but since the value source is normally private to the binding system it's at least contained within the framework.
I'm not sure what the best way to solve this in Avalonia is. We could replicate what WPF does, but it seems pretty ugly to me. And because Avalonia, unlike WPF, preserves existing bindings when setting a value, I don't think we need to have another code path in the value store.
Another potential solution is to:
SetCurrentValue
method which reads the current BindingPriority
and writes a new value with it.Style
and Inherited
in the enum. If SetCurrentValue
finds that the current priority is Inherited
or Unset
, then it sets the value with this "Current" priority.Any thoughts?
@tomenscape Can you estimate how widespread this problem is? How many controls are affected? My initial thought is just to skip those controls for 11.0. This change doesn't have to be all or nothing -- it can be just what is currently possible. I highly doubt the changes you are requested will make it in for the next release (even if the spec was decided). Could be wrong though and it's not for me to say -- that's just my impression.
Firstly, a correction: Avalonia does detach bindings when SetValue
is called, but only for StyledProperty
.
This means that the lack of SetCurrentValue
affects every control that sets its own StyledProperty
values. On master there are few if any which do this, because DirectProperty
is used for these cases instead. Perhaps for this very reason.
The only problematic case I've noticed on master is where a two-way TemplateBinding
sets a value on the source object. The source property could easily be a StyledProperty
with a binding of its own.
However, the number of problematic cases will leap upwards when DirectProperty
becomes read-only. I noticed ScrollViewer
breaking on my branch, which is due to it having several sub-components which synchronise values between one another, yet also setting the scroll offset internally (e.g. when dragging the scroll bar or scrolling the mouse wheel). There will be more cases like that, but I haven't looked. Each one will be a serious bug.
Because this is a blocker for this issue, and because this issue in turn blocks my company's transition to Avalonia, I have actually already spent some time implementing SetCurrentValue
. I want to write some more tests, then I'll submit a PR for review. It wasn't as complicated as I feared.
BTW, I will be occupied by other tasks between Tuesday and Friday and not able to work on this full time.
However, the number of problematic cases will leap upwards when DirectProperty becomes read-only
I wouldn't make DirectProerty read-only anytime soon. It is used in apps and we don't want to break any existing functionality there. I thought you were disabling that functionally only to identify areas that need to change in Avalonia. We certainly don't want to restrict direct property like this until this change in direction is well-tested and well-established. This is something @grokys also stated:
and read/write direct properties should be kept around for use by client code.
So I think we need to narrow the scope a bit -- which won't affect Avalonia or your companies transition I think.
However, the number of problematic cases will leap upwards when DirectProperty becomes read-only. I noticed ScrollViewer breaking on my branch
Yea, that's certainly more problematic and the kind of issues I was expected. My first thought was perhaps we need to ignore these places for now... however:
I have actually already spent some time implementing SetCurrentValue
That's great!
Those are fair points regarding outright removing DirectProperty
setters and their supporting methods. Their continued presence won't hurt anyone.
But I would like to at least deprecate them with ObsoleteAttribute
. We are here because DirectProperty
setter functionality has already been restricted, and IMO writeable properties in new code should always use StyledProperty
instead.
Syncing state between TextBox and TextPresenter is already a nightmare. Converting shared properties to StyledProperty makes this even harder. I don't see any reason to apply this conversion on TextBox/TextPresenter. Changing TextBlock isn't that of an issue.
I don't see any reason to apply this conversion on TextBox/TextPresenter. Changing TextBlock isn't that of an issue.
TextBlock.Text
is the main one that needs to be switched to a styled property anyway: #7982
@tomenscape Just an update on this. I'll have time to get back to this in about two weeks. I'm hoping that is enough time before an RC but could be wrong. Before then, I'll complete the other two controls already started.
Hopefully I can then take up several more controls and we can work to merge in your branch piece by piece. I dont think there are any blocking PRs now.
I think I still need to add data validation support to styled properties. Will try to start that this week.
Spotted data validation yesterday while preparing the first refactor branch. That branch contains all of the easy refactorings, and I'll open a PR for it as soon as the data validation PR above is complete.
I've added a tracking list to the original issue description. Apologies for the length but this will help us move this issue to closure.
I will try to keep this up-to-date. I still have to go through the controls a bit more and find the ones that don't need to be updated (no direct properties).
The first PR is back! The next control I am going to extract from my mega branch is TextBlock
. I see that TextBox
has been marked as out of scope, but I actually converted it back when this issue was first opened. As predicted it was a bit of a pain, but the port was successful and the control now works very nicely and is fully styleable/animatable.
Maybe it's too big a change to accept at this stage? I think it can wait, since TextBox
's nature as a user input control means it is far less likely to have styled values. But there is a catch: TextBox.TextProperty
is the result of a call to TextBlock.TextProperty.AddOwner
. This means that if we don't convert both to styled properties at the same time, we need to break that link and have two different Text properties floating around, each incompatible with the other. That could become confusing.
I see that TextBox has been marked as out of scope, but I actually converted it back when this issue was first opened. As predicted it was a bit of a pain, but the port was successful and the control now works very nicely and is fully styleable/animatable.
I think due to the complexity of the control there was not only worry about difficult changing these properties -- but issues with synchronization and secondary bugs. There still may be issues with SetCurrentValue() as well.
This means that if we don't convert both to styled properties at the same time, we need to break that link and have two different Text properties floating around, each incompatible with the other. That could become confusing.
That's a good point though. If you are confident with the changes it does sound best to do both. Hope there aren't a lot of issues. @Gillibald probably needs to give the final go-ahead.
For TextBlock.Text, don't forget about the original issue: https://github.com/AvaloniaUI/Avalonia/issues/7982
Next up: RangeBase
and derived types.
@tomenscape I was planning to give RangeBase an attempt this weekend. As usual you beat me to it :) It seems like a good idea to target base types next. So I'll find another and mention it before I start. Hoping to do 1 or 2 this weekend.
I'm going to do Track
and then AutoCompleteBox
next. SelectingItemsControl
probably is another good base type to target after that but it's going to take a bit more thought.
We're getting very close to 11.0-rc where breaking changes will be halted - what is the status of these changes? Looks like there are still a few controls to convert - are they difficult? Are they necessary?
SelectingItemsControl
and descendants are important. There has been a lot of activity in ItemsControl
recently though. Have things settled now?
SelectingItemsControl
and descendants are important.
Agreed, do you already have changes to these controls partially completed, or should I handle this?
There has been a lot of activity in
ItemsControl
recently though. Have things settled now?
Yep, I don't think that there are any more incoming changes which will affect the properties of ItemsControl
.
I started work on them a long time ago, but those changes are going to be incompatible with master now. Starting again is the best bet, so go ahead!
I'll do Track and ProgressBar by this weekend (not sure how much is required for these). Otherwise will find something else that needs it.
Does everyone agree that ItemsRepeater and DataGrid should be skipped for v11?
Does everyone agree that ItemsRepeater and DataGrid should be skipped for v11?
Fine with me!
I've run into a hitch when converting SelectingItemsControl
's SelectedIndex
and SelectedItem
to be styled properties.
At the moment, they're simply projections of the SelectionModel
's SelectedIndex
and SelectedItem
properties, and these properties only raise change notifications after both properties are updated and in sync.
If we switch to styled properties then when change notifications for SelectedIndex
and SelectedItem
are raised, they will not be in sync - one will necessarily change before the other.
Now, WPF also has this problem, e.g. in WPF:
<ListBox Name="lb" ItemsSource="{Binding}"/>
var items = new List<string> { "Item 0", "Item 1", "Item 2" };
DataContext = items;
var itemChanged = DependencyPropertyDescriptor.FromProperty(Selector.SelectedItemProperty, typeof(ListBox));
itemChanged.AddValueChanged(lb, (s, e) =>
{
System.Diagnostics.Debug.WriteLine($"Selected item changed: {lb.SelectedItem} {lb.SelectedIndex}");
});
var indexChanged = DependencyPropertyDescriptor.FromProperty(Selector.SelectedIndexProperty, typeof(ListBox));
indexChanged.AddValueChanged(lb, (s, e) =>
{
System.Diagnostics.Debug.WriteLine($"Selected index changed: {lb.SelectedItem} {lb.SelectedIndex}");
});
You see that when the properties change, first SelectedIndex
changes and SelectedItem
is out of sync, then the notification fires for SelectedItem
:
Selected index changed: 0
Selected item changed: Item 0 0
Selected index changed: Item 0 1
Selected item changed: Item 1 1
Selected index changed: Item 1 2
Selected item changed: Item 2 2
Are we OK with changing our SelectingItemsControl
s to have this behavior? In WPF it's less of a problem because it's harder to listen for individual property changed events, but in our own codebase I've already found a place where this breaks things (was easy enough to fix but may be confusing and hard to debug).
That's tricky and another special case the property system can't handle. I'm almost wondering if we shouldn't pick one to be styled and let the other be direct. Then it seems changes and events could be synronized. Not sure how I would like that design myself though.
Usually, I would just say when in doubt follow WPF when a better idea isn't yet found.
Maybe there could be a new property type (some kind of ProxyProperty
), which would just act as a proxy to a state property?
Example: there would be a SelectionState
property (styled), and then proxy properties like SelectedIndex
and SelectedItem
, which would reflect the state, with getters state => state.Index
, state => state.Item
, and setters (state, index) => state.WithIndex(index)
and (state, item) => state.WithItem(item)
.
@jp2masa Proxy properties have come up a few times before as you probably know. Slightly different usage (usually to alias an existing property) but the functionality is similar.
My guess is it is too late in the 11.0 development process to introduce such a new property type.
Proxy properties have come up a few times before as you probably know.
You're right, it sounds familiar now that you point it out, I probably forgot it.
My guess is it is too late in the 11.0 development process to introduce such a new property type.
Yes, I guess it's too late as well. I was just suggesting it because I hate inconsistency and it was the best solution I could think about, even though there are probably better solutions.
Yes, I guess it's too late as well. I was just suggesting it because I hate inconsistency and it was the best solution I could think about, even though there are probably better solutions.
Definitely agree, I think I'll file another issue for that sometime as well. There are two property-system shortcoming that have come up in the past month or two (weren't solved in WPF either).
Another option is a "property update batch" object which defers notifications for property changes made during its lifetime until disposed. This can be initiated from a control's OnPropertyChanged
, since it is executed first.
Track
and ProgressBar
were reviewed and didn't require any changes. ProgressBar
was the most interesting case and it had several direct properties in TemplateSettings. However, these are considered out-of-scope as they are basically calculated and used in animations.
Next up is TreeView
. There are only two properties to consider SelectedItem
and SelectedItems
. Did we resolve what to do about SelectedItems? Styled properties with lists are not really supported (I still have to make that other issues). So it seems to me only SelectedItem
should change. I want to confirm that everyone is on the same page for this though:
Another option is a "property update batch" object which defers notifications for property changes made during its lifetime until disposed. This can be initiated from a control's OnPropertyChanged, since it is executed first.
To me that sounds like the only solution that might be doable in the 11.0 timeframe.
Did we resolve what to do about SelectedItems
We didn't, no. I have a WIP which converts SelectedItem
and SelectedIndex
in SelectingItemsControl
to styled properties, but everything breaks :( Selection is a real PITA because there are so many competing sources of selection state, and all of them need to be in sync. We've had quite a few breakages in selection recently due to this. At this point I'm tempted to just say that selection properties need to stay as direct properties, at least until 12.0.
Another option is a "property update batch" object
We actually have this in 0.10.x but I removed it because it was
PropertyChanged
handler triggered from the first batch update"PropertyChanged
notification raised on ending the batch update"I'd not be totally opposed to adding batch updates back in, but I'd like to think carefully about whether it's really needed. Maybe some kind of simpler mechanism could be introduced (thinking out loud: an API for setting multiple properties at once?)
ScrollBar
, ScrollContentPresenter
and MenuBase
marked as done as the only remaining direct properties are read-only properties.
Did we resolve what to do about SelectedItems
We didn't, no. I have a WIP which converts SelectedItem and SelectedIndex in SelectingItemsControl to styled properties, but everything breaks :( Selection is a real PITA because there are so many competing sources of selection state, and all of them need to be in sync. We've had quite a few breakages in selection recently due to this. At this point I'm tempted to just say that selection properties need to stay as direct properties, at least until 12.0.
I'm tempted to say you're right. In use-cases I can think of I also don't see a reason one would actually want to set the SelectedItem or SelectedItems in styles. This just isn't really done. @tomenscape Can you think of some reasons you need this on your end? Otherwise, I think @grokys is right and we should drop it for now.
I also finally got around to creating a separate issue for lists in the property system: https://github.com/AvaloniaUI/Avalonia/issues/11285.
(thinking out loud: an API for setting multiple properties at once?)
That seems promising!
Our use case is described in #11220. The SelectedItem
binding is usually made in the body of a UserControl
, but it could conceivably be set via a style too.
It's not a high priority though, since an attached styled property can be used as a proxy.
an attached styled property can be used as a proxy.
Yeah I'd recommend this approach if selection really needs to be styleable. Just make sure you stick to a single selection property (per ItemsControl
) and you shouldn't run into problems.
@grokys @tomenscape
ListBox, MenuFlyout, SelectingItemsConteol and TreeView are all that are left unchecked on the list.
Looks like MenuFlyout is done so will mark that complete.
TreeView, ListBox and SelectingItemsControl still have direct properties for SelectedItem and SelectedItems. It's not clear to me what should change with these. Due to the discussion above it sounds like:
With all of this in mind (and without doing a last DirectProperty search to final check) I think this issue can be closed.
Any objections or concerns?
ListBox
, SelectingItemsConteol
and TreeView
were noted as partially complete in the list above with the comment:
Has SelectedItem, SelectedIndex or SelectedItems properties which were not fully converted due to the complexity discussed in this issue: https://github.com/AvaloniaUI/Avalonia/issues/9944#issuecomment-1538012823. This may be separately addressed in a future version of Avalonia.
As far as I know this completes review of the relevant control properties.
I'm closing this issue as complete since most properties were converted and no more will be done for v11. Great job everyone!
Is your feature request related to a problem? Please describe.
This one is going to cause some controversy. However, I believe it's time to significantly narrow the use-case of
DirectProperty
(ies) which are unable to be set by styles in most cases.Some additional points for discussion:
With the significant optimization done to the property value store over the past few years, I don't expect a significant performance impact moving most properties over. I could be wrong though.
Describe the solution you'd like
Almost all current
DirectProperty
occurrences should be converted toStyledProperty
so they can be used fully within styles. My original comment was:@tomenscape had some good points and narrowed the scope again:
This means ALL direct properties such as
TextBlock.Text
,Expander.IsExpanded
andSplitPane.IsPaneOpen
(which are UI state information) would be converted to styled properties.Describe alternatives you've considered
There are two alternatives:
DirectProperty
as if it was aStyledProperty
. No idea how to do this.Additional context
Some recent discussions include:
@tomenscape Feel free to duplicate your past comments here. It will put everything in one spot for the future.
Current status is tracked below. Note that animations and certain drawing primitives are currently out-of-scope. There is a strong chance of performance regressions in these areas and the use-cases of changing such properties in styles is not yet clear. Therefore, the focus is primarily controls:
IsPressed
is a read-only property so remains unchangedSelectedItem
,SelectedIndex
orSelectedItems
properties which were not fully converted due to the complexity discussed in this issue: https://github.com/AvaloniaUI/Avalonia/issues/9944#issuecomment-1538012823. This may be separately addressed in a future version of Avalonia.Percentage
is correct as a direct property. This is a read-only calculated value.RangeBase
IsExpanded
that is likely already correctSelectedItem
,SelectedIndex
orSelectedItems
properties which were not fully converted due to the complexity discussed in this issue: https://github.com/AvaloniaUI/Avalonia/issues/9944#issuecomment-1538012823. This may be separately addressed in a future version of Avalonia.SelectedItem
,SelectedIndex
orSelectedItems
properties which were not fully converted due to the complexity discussed in this issue: https://github.com/AvaloniaUI/Avalonia/issues/9944#issuecomment-1538012823. This may be separately addressed in a future version of Avalonia.