diff --git a/samples/ControlCatalog/Pages/ExpanderPage.xaml b/samples/ControlCatalog/Pages/ExpanderPage.xaml
index f0a80fd7ab..b5a2e6cdd0 100644
--- a/samples/ControlCatalog/Pages/ExpanderPage.xaml
+++ b/samples/ControlCatalog/Pages/ExpanderPage.xaml
@@ -52,6 +52,24 @@
Rounded
+
+
+ Expanded content
+
+
+
+
+ Expanded content
+
+
diff --git a/samples/ControlCatalog/Pages/ExpanderPage.xaml.cs b/samples/ControlCatalog/Pages/ExpanderPage.xaml.cs
index e8a080899a..98a494e533 100644
--- a/samples/ControlCatalog/Pages/ExpanderPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ExpanderPage.xaml.cs
@@ -10,6 +10,12 @@ namespace ControlCatalog.Pages
{
this.InitializeComponent();
DataContext = new ExpanderPageViewModel();
+
+ var CollapsingDisabledExpander = this.Get("CollapsingDisabledExpander");
+ var ExpandingDisabledExpander = this.Get("ExpandingDisabledExpander");
+
+ CollapsingDisabledExpander.Collapsing += (s, e) => { e.Handled = true; };
+ ExpandingDisabledExpander.Expanding += (s, e) => { e.Handled = true; };
}
private void InitializeComponent()
diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs
index 3ba99d8a67..65227a826a 100644
--- a/src/Avalonia.Controls/Expander.cs
+++ b/src/Avalonia.Controls/Expander.cs
@@ -1,7 +1,11 @@
+using System;
using System.Threading;
using Avalonia.Animation;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
namespace Avalonia.Controls
{
@@ -37,12 +41,24 @@ namespace Avalonia.Controls
[PseudoClasses(":expanded", ":up", ":down", ":left", ":right")]
public class Expander : HeaderedContentControl
{
+ ///
+ /// Defines the property.
+ ///
public static readonly StyledProperty ContentTransitionProperty =
- AvaloniaProperty.Register(nameof(ContentTransition));
+ AvaloniaProperty.Register(
+ nameof(ContentTransition));
+ ///
+ /// Defines the property.
+ ///
public static readonly StyledProperty ExpandDirectionProperty =
- AvaloniaProperty.Register(nameof(ExpandDirection), ExpandDirection.Down);
+ AvaloniaProperty.Register(
+ nameof(ExpandDirection),
+ ExpandDirection.Down);
+ ///
+ /// Defines the property.
+ ///
public static readonly DirectProperty IsExpandedProperty =
AvaloniaProperty.RegisterDirect(
nameof(IsExpanded),
@@ -50,47 +66,206 @@ namespace Avalonia.Controls
(o, v) => o.IsExpanded = v,
defaultBindingMode: Data.BindingMode.TwoWay);
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent CollapsedEvent =
+ RoutedEvent.Register(
+ nameof(Collapsed),
+ RoutingStrategies.Bubble);
+
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent CollapsingEvent =
+ RoutedEvent.Register(
+ nameof(Collapsing),
+ RoutingStrategies.Bubble);
+
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent ExpandedEvent =
+ RoutedEvent.Register(
+ nameof(Expanded),
+ RoutingStrategies.Bubble);
+
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent ExpandingEvent =
+ RoutedEvent.Register(
+ nameof(Expanding),
+ RoutingStrategies.Bubble);
+
+ private bool _ignorePropertyChanged = false;
private bool _isExpanded;
private CancellationTokenSource? _lastTransitionCts;
- static Expander()
- {
- IsExpandedProperty.Changed.AddClassHandler((x, e) => x.OnIsExpandedChanged(e));
- }
-
+ ///
+ /// Initializes a new instance of the class.
+ ///
public Expander()
{
- UpdatePseudoClasses(ExpandDirection);
+ UpdatePseudoClasses();
}
+ ///
+ /// Gets or sets the transition used when expanding or collapsing the content.
+ ///
public IPageTransition? ContentTransition
{
get => GetValue(ContentTransitionProperty);
set => SetValue(ContentTransitionProperty, value);
}
+ ///
+ /// Gets or sets the direction in which the opens.
+ ///
public ExpandDirection ExpandDirection
{
get => GetValue(ExpandDirectionProperty);
set => SetValue(ExpandDirectionProperty, value);
}
+ ///
+ /// Gets or sets a value indicating whether the
+ /// content area is open and visible.
+ ///
public bool IsExpanded
{
- get { return _isExpanded; }
- set
- {
- SetAndRaise(IsExpandedProperty, ref _isExpanded, value);
- PseudoClasses.Set(":expanded", value);
+ get => _isExpanded;
+ set
+ {
+ // It is important here that IsExpanded is a direct property so events can be invoked
+ // BEFORE the property system gets notified of updated values. This is because events
+ // may be canceled by external code.
+ if (_isExpanded != value)
+ {
+ RoutedEventArgs eventArgs;
+
+ if (value)
+ {
+ eventArgs = new RoutedEventArgs(ExpandingEvent, this);
+ OnExpanding(eventArgs);
+ }
+ else
+ {
+ eventArgs = new RoutedEventArgs(CollapsingEvent, this);
+ OnCollapsing(eventArgs);
+ }
+
+ if (eventArgs.Handled)
+ {
+ // If the event was externally handled (canceled) we must still notify the value has changed.
+ // This property changed notification will update any external code observing this property that itself may have set the new value.
+ // We are essentially reverted any external state change along with ignoring the IsExpanded property set.
+ // Remember IsExpanded is usually controlled by a ToggleButton in the control theme.
+ _ignorePropertyChanged = true;
+
+ RaisePropertyChanged(
+ IsExpandedProperty,
+ oldValue: value,
+ newValue: _isExpanded,
+ BindingPriority.LocalValue,
+ isEffectiveValue: true);
+
+ _ignorePropertyChanged = false;
+ }
+ else
+ {
+ SetAndRaise(IsExpandedProperty, ref _isExpanded, value);
+ }
+ }
}
}
- protected virtual async void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e)
+ ///
+ /// Occurs after the content area has closed and only the header is visible.
+ ///
+ public event EventHandler? Collapsed
+ {
+ add => AddHandler(CollapsedEvent, value);
+ remove => RemoveHandler(CollapsedEvent, value);
+ }
+
+ ///
+ /// Occurs as the content area is closing.
+ ///
+ ///
+ /// The event args property may be set to true to cancel the event
+ /// and keep the control open (expanded).
+ ///
+ public event EventHandler? Collapsing
+ {
+ add => AddHandler(CollapsingEvent, value);
+ remove => RemoveHandler(CollapsingEvent, value);
+ }
+
+ ///
+ /// Occurs after the has opened to display both its header and content.
+ ///
+ public event EventHandler? Expanded
+ {
+ add => AddHandler(ExpandedEvent, value);
+ remove => RemoveHandler(ExpandedEvent, value);
+ }
+
+ ///
+ /// Occurs as the content area is opening.
+ ///
+ ///
+ /// The event args property may be set to true to cancel the event
+ /// and keep the control closed (collapsed).
+ ///
+ public event EventHandler? Expanding
+ {
+ add => AddHandler(ExpandingEvent, value);
+ remove => RemoveHandler(ExpandingEvent, value);
+ }
+
+ ///
+ /// Invoked just before the event.
+ ///
+ protected virtual void OnCollapsed(RoutedEventArgs eventArgs)
+ {
+ RaiseEvent(eventArgs);
+ }
+
+ ///
+ /// Invoked just before the event.
+ ///
+ protected virtual void OnCollapsing(RoutedEventArgs eventArgs)
+ {
+ RaiseEvent(eventArgs);
+ }
+
+ ///
+ /// Invoked just before the event.
+ ///
+ protected virtual void OnExpanded(RoutedEventArgs eventArgs)
+ {
+ RaiseEvent(eventArgs);
+ }
+
+ ///
+ /// Invoked just before the event.
+ ///
+ protected virtual void OnExpanding(RoutedEventArgs eventArgs)
+ {
+ RaiseEvent(eventArgs);
+ }
+
+ ///
+ /// Starts the content transition (if set) and invokes the
+ /// and events when completed.
+ ///
+ private async void StartContentTransition()
{
if (Content != null && ContentTransition != null && Presenter is Visual visualContent)
{
bool forward = ExpandDirection == ExpandDirection.Left ||
- ExpandDirection == ExpandDirection.Up;
+ ExpandDirection == ExpandDirection.Up;
_lastTransitionCts?.Cancel();
_lastTransitionCts = new CancellationTokenSource();
@@ -104,24 +279,58 @@ namespace Avalonia.Controls
await ContentTransition.Start(visualContent, null, forward, _lastTransitionCts.Token);
}
}
+
+ // Expanded/Collapsed events are invoked asynchronously to ensure other events,
+ // such as Click, have time to complete first.
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (IsExpanded)
+ {
+ OnExpanded(new RoutedEventArgs(ExpandedEvent, this));
+ }
+ else
+ {
+ OnCollapsed(new RoutedEventArgs(CollapsedEvent, this));
+ }
+ });
}
+ ///
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
+ if (_ignorePropertyChanged)
+ {
+ return;
+ }
+
if (change.Property == ExpandDirectionProperty)
{
- UpdatePseudoClasses(change.GetNewValue());
+ UpdatePseudoClasses();
+ }
+ else if (change.Property == IsExpandedProperty)
+ {
+ // Expanded/Collapsed will be raised once transitions are complete
+ StartContentTransition();
+
+ UpdatePseudoClasses();
}
}
- private void UpdatePseudoClasses(ExpandDirection d)
+ ///
+ /// Updates the visual state of the control by applying latest PseudoClasses.
+ ///
+ private void UpdatePseudoClasses()
{
- PseudoClasses.Set(":up", d == ExpandDirection.Up);
- PseudoClasses.Set(":down", d == ExpandDirection.Down);
- PseudoClasses.Set(":left", d == ExpandDirection.Left);
- PseudoClasses.Set(":right", d == ExpandDirection.Right);
+ var expandDirection = ExpandDirection;
+
+ PseudoClasses.Set(":up", expandDirection == ExpandDirection.Up);
+ PseudoClasses.Set(":down", expandDirection == ExpandDirection.Down);
+ PseudoClasses.Set(":left", expandDirection == ExpandDirection.Left);
+ PseudoClasses.Set(":right", expandDirection == ExpandDirection.Right);
+
+ PseudoClasses.Set(":expanded", IsExpanded);
}
}
}
diff --git a/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs b/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs
index 0f257224dd..8590974462 100644
--- a/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs
+++ b/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs
@@ -18,7 +18,7 @@ namespace Avalonia.Controls
}
}
- base.OnKeyDown(e);
+ base.OnKeyDown(e);
}
}
}
diff --git a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
index 3cf819c4b8..37e9b71185 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
@@ -103,17 +103,18 @@
RenderTransformOrigin="50%,50%"
Stretch="None"
Stroke="{DynamicResource ExpanderChevronForeground}"
- StrokeThickness="1" />
-
-
-
+ StrokeThickness="1">
+
+
+
+
-
-
-