diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index bc86558ab3..0cde765f76 100644
--- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
@@ -6,6 +6,7 @@ using Avalonia.Input;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Utilities;
using Avalonia.VisualTree;
+using System.Linq;
namespace Avalonia.Controls.Presenters
{
@@ -19,44 +20,34 @@ namespace Avalonia.Controls.Presenters
///
/// Defines the property.
///
- public static readonly DirectProperty CanHorizontallyScrollProperty =
- AvaloniaProperty.RegisterDirect(
- nameof(CanHorizontallyScroll),
- o => o.CanHorizontallyScroll,
- (o, v) => o.CanHorizontallyScroll = v);
+ public static readonly StyledProperty CanHorizontallyScrollProperty =
+ AvaloniaProperty.Register(nameof(CanHorizontallyScroll));
///
/// Defines the property.
///
- public static readonly DirectProperty CanVerticallyScrollProperty =
- AvaloniaProperty.RegisterDirect(
- nameof(CanVerticallyScroll),
- o => o.CanVerticallyScroll,
- (o, v) => o.CanVerticallyScroll = v);
+ public static readonly StyledProperty CanVerticallyScrollProperty =
+ AvaloniaProperty.Register(nameof(CanVerticallyScroll));
///
/// Defines the property.
///
public static readonly DirectProperty ExtentProperty =
ScrollViewer.ExtentProperty.AddOwner(
- o => o.Extent,
- (o, v) => o.Extent = v);
+ o => o.Extent);
///
/// Defines the property.
///
- public static readonly DirectProperty OffsetProperty =
- ScrollViewer.OffsetProperty.AddOwner(
- o => o.Offset,
- (o, v) => o.Offset = v);
+ public static readonly StyledProperty OffsetProperty =
+ ScrollViewer.OffsetProperty.AddOwner(new(coerce: ScrollViewer.CoerceOffset));
///
/// Defines the property.
///
public static readonly DirectProperty ViewportProperty =
ScrollViewer.ViewportProperty.AddOwner(
- o => o.Viewport,
- (o, v) => o.Viewport = v);
+ o => o.Viewport);
///
/// Defines the property.
@@ -88,11 +79,8 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty IsScrollChainingEnabledProperty =
ScrollViewer.IsScrollChainingEnabledProperty.AddOwner();
- private bool _canHorizontallyScroll;
- private bool _canVerticallyScroll;
private bool _arranging;
private Size _extent;
- private Vector _offset;
private IDisposable? _logicalScrollSubscription;
private Size _viewport;
private Dictionary? _activeLogicalGestureScrolls;
@@ -109,6 +97,7 @@ namespace Avalonia.Controls.Presenters
private double _verticalSnapPoint;
private double _verticalSnapPointOffset;
private double _horizontalSnapPointOffset;
+ private CompositeDisposable? _ownerSubscriptions;
///
/// Initializes static members of the class.
@@ -116,7 +105,6 @@ namespace Avalonia.Controls.Presenters
static ScrollContentPresenter()
{
ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true);
- ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e));
}
///
@@ -137,8 +125,8 @@ namespace Avalonia.Controls.Presenters
///
public bool CanHorizontallyScroll
{
- get { return _canHorizontallyScroll; }
- set { SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); }
+ get => GetValue(CanHorizontallyScrollProperty);
+ set => SetValue(CanHorizontallyScrollProperty, value);
}
///
@@ -146,8 +134,8 @@ namespace Avalonia.Controls.Presenters
///
public bool CanVerticallyScroll
{
- get { return _canVerticallyScroll; }
- set { SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); }
+ get => GetValue(CanVerticallyScrollProperty);
+ set => SetValue(CanVerticallyScrollProperty, value);
}
///
@@ -164,8 +152,8 @@ namespace Avalonia.Controls.Presenters
///
public Vector Offset
{
- get { return _offset; }
- set { SetAndRaise(OffsetProperty, ref _offset, ScrollViewer.CoerceOffset(Extent, Viewport, value)); }
+ get => GetValue(OffsetProperty);
+ set => SetValue(OffsetProperty, value);
}
///
@@ -295,12 +283,64 @@ namespace Avalonia.Controls.Presenters
if (result)
{
- Offset = offset;
+ SetCurrentValue(OffsetProperty, offset);
}
return result;
}
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+ AttachToScrollViewer();
+ }
+
+ ///
+ /// Locates the first ancestor and binds to it. Properties which have been set through other means are not bound.
+ ///
+ ///
+ /// This method is automatically called when the control is attached to a visual tree.
+ ///
+ protected internal virtual void AttachToScrollViewer()
+ {
+ _ownerSubscriptions?.Dispose();
+
+ var owner = this.FindAncestorOfType();
+
+ if (owner == null)
+ {
+ return;
+ }
+
+ var subscriptionDisposables = new IDisposable?[]
+ {
+ IfUnset(CanHorizontallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.HorizontalScrollBarVisibilityProperty).Select(NotDisabled), Data.BindingPriority.Template)),
+ IfUnset(CanVerticallyScrollProperty, p => Bind(p, owner.GetObservable(ScrollViewer.VerticalScrollBarVisibilityProperty).Select(NotDisabled), Data.BindingPriority.Template)),
+ IfUnset(OffsetProperty, p => new CompositeDisposable(
+ Bind(p, owner.GetBindingObservable(ScrollViewer.OffsetProperty), Data.BindingPriority.Template),
+ this.GetObservable(OffsetProperty).Subscribe(v => owner.SetCurrentValue(OffsetProperty, v)))),
+ IfUnset(IsScrollChainingEnabledProperty, p => Bind(p, owner.GetBindingObservable(ScrollViewer.IsScrollChainingEnabledProperty), Data.BindingPriority.Template)),
+ IfUnset(ContentProperty, p => Bind(p, owner.GetBindingObservable(ContentProperty), Data.BindingPriority.Template)),
+
+ // read-only properties on ScrollViewer with internal setters:
+ this.GetObservable(ExtentProperty).Subscribe(v => owner.Extent = v),
+ this.GetObservable(ViewportProperty).Subscribe(v => owner.Viewport = v)
+ }.Where(d => d != null).Cast().ToArray();
+
+ _ownerSubscriptions = new CompositeDisposable(subscriptionDisposables);
+
+ static bool NotDisabled(ScrollBarVisibility v) => v != ScrollBarVisibility.Disabled;
+
+ IDisposable? IfUnset(T property, Func func) where T : AvaloniaProperty => GetValueStore().IsSet(property) ? null : func(property);
+ }
+
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ _ownerSubscriptions?.Dispose();
+ base.OnDetachedFromVisualTree(e);
+ }
+
///
void IScrollAnchorProvider.RegisterAnchorCandidate(Control element)
{
@@ -410,7 +450,7 @@ namespace Avalonia.Controls.Presenters
try
{
_arranging = true;
- Offset = newOffset;
+ SetCurrentValue(OffsetProperty, newOffset);
}
finally
{
@@ -427,7 +467,6 @@ namespace Avalonia.Controls.Presenters
Viewport = finalSize;
Extent = Child!.Bounds.Size.Inflate(Child.Margin);
- Offset = ScrollViewer.CoerceOffset(Extent, finalSize, Offset);
_isAnchorElementDirty = true;
return finalSize;
@@ -516,7 +555,7 @@ namespace Avalonia.Controls.Presenters
}
bool offsetChanged = newOffset != Offset;
- Offset = newOffset;
+ SetCurrentValue(OffsetProperty, newOffset);
e.Handled = !IsScrollChainingEnabled || offsetChanged;
@@ -529,7 +568,7 @@ namespace Avalonia.Controls.Presenters
_activeLogicalGestureScrolls?.Remove(e.Id);
_scrollGestureSnapPoints?.Remove(e.Id);
- Offset = SnapOffset(Offset);
+ SetCurrentValue(OffsetProperty, SnapOffset(Offset));
}
private void OnScrollGestureInertiaStartingEnded(object? sender, ScrollGestureInertiaStartingEventArgs e)
@@ -623,7 +662,7 @@ namespace Avalonia.Controls.Presenters
Vector newOffset = SnapOffset(new Vector(x, y));
bool offsetChanged = newOffset != Offset;
- Offset = newOffset;
+ SetCurrentValue(OffsetProperty, newOffset);
e.Handled = !IsScrollChainingEnabled || offsetChanged;
}
@@ -651,6 +690,10 @@ namespace Avalonia.Controls.Presenters
UpdateSnapPoints();
}
+ else if (change.Property == ChildProperty)
+ {
+ ChildChanged(change);
+ }
else if (change.Property == HorizontalSnapPointsAlignmentProperty ||
change.Property == VerticalSnapPointsAlignmentProperty)
{
@@ -677,7 +720,7 @@ namespace Avalonia.Controls.Presenters
if (e.OldValue != null)
{
- Offset = default;
+ SetCurrentValue(OffsetProperty, default);
}
}
@@ -719,14 +762,14 @@ namespace Avalonia.Controls.Presenters
if (logicalScroll != scrollable.IsLogicalScrollEnabled)
{
UpdateScrollableSubscription(Child);
- Offset = default;
+ SetCurrentValue(OffsetProperty, default);
InvalidateMeasure();
}
else if (scrollable.IsLogicalScrollEnabled)
{
Viewport = scrollable.Viewport;
Extent = scrollable.Extent;
- Offset = scrollable.Offset;
+ SetCurrentValue(OffsetProperty, scrollable.Offset);
}
}
diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs
index 38d848d69b..fd9de47236 100644
--- a/src/Avalonia.Controls/Primitives/RangeBase.cs
+++ b/src/Avalonia.Controls/Primitives/RangeBase.cs
@@ -12,30 +12,22 @@ namespace Avalonia.Controls.Primitives
///
/// Defines the property.
///
- public static readonly DirectProperty MinimumProperty =
- AvaloniaProperty.RegisterDirect(
- nameof(Minimum),
- o => o.Minimum,
- (o, v) => o.Minimum = v);
+ public static readonly StyledProperty MinimumProperty =
+ AvaloniaProperty.Register(nameof(Minimum), coerce: CoerceMinimum);
///
/// Defines the property.
///
- public static readonly DirectProperty MaximumProperty =
- AvaloniaProperty.RegisterDirect(
- nameof(Maximum),
- o => o.Maximum,
- (o, v) => o.Maximum = v);
+ public static readonly StyledProperty MaximumProperty =
+ AvaloniaProperty.Register(nameof(Maximum), 100, coerce: CoerceMaximum);
///
/// Defines the property.
///
- public static readonly DirectProperty ValueProperty =
- AvaloniaProperty.RegisterDirect(
- nameof(Value),
- o => o.Value,
- (o, v) => o.Value = v,
- defaultBindingMode: BindingMode.TwoWay);
+ public static readonly StyledProperty ValueProperty =
+ AvaloniaProperty.Register(nameof(Value),
+ defaultBindingMode: BindingMode.TwoWay,
+ coerce: CoerceValue);
///
/// Defines the property.
@@ -49,44 +41,26 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty LargeChangeProperty =
AvaloniaProperty.Register(nameof(LargeChange), 10);
- private double _minimum;
- private double _maximum = 100.0;
- private double _value;
-
///
- /// Initializes a new instance of the class.
+ /// Gets or sets the minimum value.
///
- public RangeBase()
+ public double Minimum
{
+ get => GetValue(MinimumProperty);
+ set => SetValue(MinimumProperty, value);
}
- ///
- /// Gets or sets the minimum value.
- ///
- public double Minimum
+ private static double CoerceMinimum(AvaloniaObject sender, double value)
{
- get
- {
- return _minimum;
- }
+ return ValidateDouble(value) ? value : sender.GetValue(MinimumProperty);
+ }
- set
+ private void OnMinimumChanged()
+ {
+ if (IsInitialized)
{
- if (!ValidateDouble(value))
- {
- return;
- }
-
- if (IsInitialized)
- {
- SetAndRaise(MinimumProperty, ref _minimum, value);
- Maximum = ValidateMaximum(Maximum);
- Value = ValidateValue(Value);
- }
- else
- {
- SetAndRaise(MinimumProperty, ref _minimum, value);
- }
+ CoerceValue(MaximumProperty);
+ CoerceValue(ValueProperty);
}
}
@@ -95,28 +69,22 @@ namespace Avalonia.Controls.Primitives
///
public double Maximum
{
- get
- {
- return _maximum;
- }
+ get => GetValue(MaximumProperty);
+ set => SetValue(MaximumProperty, value);
+ }
+
+ private static double CoerceMaximum(AvaloniaObject sender, double value)
+ {
+ return ValidateDouble(value)
+ ? Math.Max(value, sender.GetValue(MinimumProperty))
+ : sender.GetValue(MaximumProperty);
+ }
- set
+ private void OnMaximumChanged()
+ {
+ if (IsInitialized)
{
- if (!ValidateDouble(value))
- {
- return;
- }
-
- if (IsInitialized)
- {
- value = ValidateMaximum(value);
- SetAndRaise(MaximumProperty, ref _maximum, value);
- Value = ValidateValue(Value);
- }
- else
- {
- SetAndRaise(MaximumProperty, ref _maximum, value);
- }
+ CoerceValue(ValueProperty);
}
}
@@ -125,28 +93,15 @@ namespace Avalonia.Controls.Primitives
///
public double Value
{
- get
- {
- return _value;
- }
+ get => GetValue(ValueProperty);
+ set => SetValue(ValueProperty, value);
+ }
- set
- {
- if (!ValidateDouble(value))
- {
- return;
- }
-
- if (IsInitialized)
- {
- value = ValidateValue(value);
- SetAndRaise(ValueProperty, ref _value, value);
- }
- else
- {
- SetAndRaise(ValueProperty, ref _value, value);
- }
- }
+ private static double CoerceValue(AvaloniaObject sender, double value)
+ {
+ return ValidateDouble(value)
+ ? MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(MaximumProperty))
+ : sender.GetValue(ValueProperty);
}
public double SmallChange
@@ -165,37 +120,31 @@ namespace Avalonia.Controls.Primitives
{
base.OnInitialized();
- Maximum = ValidateMaximum(Maximum);
- Value = ValidateValue(Value);
+ CoerceValue(MaximumProperty);
+ CoerceValue(ValueProperty);
}
- ///
- /// Checks if the double value is not infinity nor NaN.
- ///
- /// The value.
- private static bool ValidateDouble(double value)
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
- return !double.IsInfinity(value) && !double.IsNaN(value);
- }
+ base.OnPropertyChanged(change);
- ///
- /// Validates/coerces the property.
- ///
- /// The value.
- /// The coerced value.
- private double ValidateMaximum(double value)
- {
- return Math.Max(value, Minimum);
+ if (change.Property == MinimumProperty)
+ {
+ OnMinimumChanged();
+ }
+ else if (change.Property == MaximumProperty)
+ {
+ OnMaximumChanged();
+ }
}
///
- /// Validates/coerces the property.
+ /// Checks if the double value is not infinity nor NaN.
///
/// The value.
- /// The coerced value.
- private double ValidateValue(double value)
+ private static bool ValidateDouble(double value)
{
- return MathUtilities.Clamp(value, Minimum, Maximum);
+ return !double.IsInfinity(value) && !double.IsNaN(value);
}
}
}
diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs
index b4a8408901..df69baae0a 100644
--- a/src/Avalonia.Controls/Primitives/ScrollBar.cs
+++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs
@@ -6,6 +6,9 @@ using Avalonia.Layout;
using Avalonia.Threading;
using Avalonia.Controls.Metadata;
using Avalonia.Automation.Peers;
+using Avalonia.VisualTree;
+using Avalonia.Reactive;
+using System.Linq;
namespace Avalonia.Controls.Primitives
{
@@ -80,6 +83,7 @@ namespace Avalonia.Controls.Primitives
private Button? _pageDownButton;
private DispatcherTimer? _timer;
private bool _isExpanded;
+ private CompositeDisposable? _ownerSubscriptions;
///
/// Initializes static members of the class.
@@ -88,6 +92,8 @@ namespace Avalonia.Controls.Primitives
{
Thumb.DragDeltaEvent.AddClassHandler((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble);
Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragComplete(e), RoutingStrategies.Bubble);
+
+ FocusableProperty.OverrideMetadata(new(false));
}
///
@@ -178,7 +184,60 @@ namespace Avalonia.Controls.Primitives
_ => throw new InvalidOperationException("Invalid value for ScrollBar.Visibility.")
};
- SetValue(IsVisibleProperty, isVisible);
+ SetCurrentValue(IsVisibleProperty, isVisible);
+ }
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+ AttachToScrollViewer();
+ }
+
+ ///
+ /// Locates the first ancestor and binds to its properties. Properties which have been set through other means are not bound.
+ ///
+ ///
+ /// This method is automatically called when the control is attached to a visual tree.
+ ///
+ protected internal virtual void AttachToScrollViewer()
+ {
+ _ownerSubscriptions?.Dispose();
+
+ var owner = this.FindAncestorOfType();
+
+ if (owner == null)
+ {
+ return;
+ }
+
+ var visibilitySource = Orientation == Orientation.Horizontal ? ScrollViewer.HorizontalScrollBarVisibilityProperty : ScrollViewer.VerticalScrollBarVisibilityProperty;
+
+ var subscriptionDisposables = new IDisposable?[]
+ {
+ IfUnset(MaximumProperty, p => Bind(p, owner.GetObservable(ScrollViewer.ScrollBarMaximumProperty).Select(ExtractOrdinate), BindingPriority.Template)),
+ IfUnset(ValueProperty, p => new CompositeDisposable(
+ Bind(p, owner.GetObservable(ScrollViewer.OffsetProperty).Select(ExtractOrdinate), BindingPriority.Template),
+ this.GetObservable(ValueProperty).Subscribe(v => SetScrollViewerOffset(owner, v)))),
+ IfUnset(ViewportSizeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.ViewportProperty).Select(ExtractOrdinate), BindingPriority.Template)),
+ IfUnset(VisibilityProperty, p => Bind(p, owner.GetObservable(visibilitySource), BindingPriority.Template)),
+ IfUnset(AllowAutoHideProperty, p => Bind(p, owner.GetObservable(ScrollViewer.AllowAutoHideProperty), BindingPriority.Template)),
+ IfUnset(LargeChangeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.LargeChangeProperty).Select(ExtractOrdinate), BindingPriority.Template)),
+ IfUnset(SmallChangeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.SmallChangeProperty).Select(ExtractOrdinate), BindingPriority.Template))
+ }.Where(d => d != null).Cast().ToArray();
+
+ _ownerSubscriptions = new CompositeDisposable(subscriptionDisposables);
+
+ IDisposable? IfUnset(T property, Func func) where T : AvaloniaProperty => GetValueStore().IsSet(property) ? null : func(property);
+ }
+
+ private double ExtractOrdinate(Vector v) => Orientation == Orientation.Horizontal ? v.X : v.Y;
+ private double ExtractOrdinate(Size v) => Orientation == Orientation.Horizontal ? v.Width : v.Height;
+ private void SetScrollViewerOffset(ScrollViewer viewer, double value) => viewer.SetCurrentValue(ScrollViewer.OffsetProperty, Orientation == Orientation.Horizontal ? viewer.Offset.WithX(value) : viewer.Offset.WithY(value));
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ _ownerSubscriptions?.Dispose();
+ base.OnDetachedFromVisualTree(e);
}
protected override void OnKeyDown(KeyEventArgs e)
@@ -202,6 +261,7 @@ namespace Avalonia.Controls.Primitives
if (change.Property == OrientationProperty)
{
UpdatePseudoClasses(change.GetNewValue());
+ AttachToScrollViewer(); // there's no way to manually refresh bindings, so reapply them
}
else if (change.Property == AllowAutoHideProperty)
{
@@ -373,25 +433,25 @@ namespace Avalonia.Controls.Primitives
private void SmallDecrement()
{
- Value = Math.Max(Value - SmallChange, Minimum);
+ SetCurrentValue(ValueProperty, Math.Max(Value - SmallChange, Minimum));
OnScroll(ScrollEventType.SmallDecrement);
}
private void SmallIncrement()
{
- Value = Math.Min(Value + SmallChange, Maximum);
+ SetCurrentValue(ValueProperty, Math.Min(Value + SmallChange, Maximum));
OnScroll(ScrollEventType.SmallIncrement);
}
private void LargeDecrement()
{
- Value = Math.Max(Value - LargeChange, Minimum);
+ SetCurrentValue(ValueProperty, Math.Max(Value - LargeChange, Minimum));
OnScroll(ScrollEventType.LargeDecrement);
}
private void LargeIncrement()
{
- Value = Math.Min(Value + LargeChange, Maximum);
+ SetCurrentValue(ValueProperty, Math.Min(Value + LargeChange, Maximum));
OnScroll(ScrollEventType.LargeIncrement);
}
diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs
index 9e8d1478fa..fe4912a33c 100644
--- a/src/Avalonia.Controls/Primitives/Track.cs
+++ b/src/Avalonia.Controls/Primitives/Track.cs
@@ -15,14 +15,14 @@ namespace Avalonia.Controls.Primitives
[PseudoClasses(":vertical", ":horizontal")]
public class Track : Control
{
- public static readonly DirectProperty