Skip to content

Commit

Permalink
Merge pull request #9979 from robloo/expander-isexpanded-styled-prop
Browse files Browse the repository at this point in the history
Switch Expander.IsExpanded to a StyledProperty
  • Loading branch information
maxkatz6 authored Jan 26, 2023
2 parents 12fd3b8 + ddd67de commit 2072b19
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 60 deletions.
4 changes: 2 additions & 2 deletions samples/ControlCatalog/Pages/ExpanderPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ public ExpanderPage()
var CollapsingDisabledExpander = this.Get<Expander>("CollapsingDisabledExpander");
var ExpandingDisabledExpander = this.Get<Expander>("ExpandingDisabledExpander");

CollapsingDisabledExpander.Collapsing += (s, e) => { e.Handled = true; };
ExpandingDisabledExpander.Expanding += (s, e) => { e.Handled = true; };
CollapsingDisabledExpander.Collapsing += (s, e) => { e.Cancel = true; };
ExpandingDisabledExpander.Expanding += (s, e) => { e.Cancel = true; };
}

private void InitializeComponent()
Expand Down
39 changes: 39 additions & 0 deletions src/Avalonia.Base/Interactivity/CancelRoutedEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace Avalonia.Interactivity
{
/// <summary>
/// Provides state information and data specific to a cancelable routed event.
/// </summary>
public class CancelRoutedEventArgs : RoutedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="CancelRoutedEventArgs"/> class.
/// </summary>
public CancelRoutedEventArgs()
{
}

/// <summary>
/// Initializes a new instance of the <see cref="CancelRoutedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
public CancelRoutedEventArgs(RoutedEvent? routedEvent)
: base(routedEvent)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="CancelRoutedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
/// <param name="source">The source object that raised the routed event.</param>
public CancelRoutedEventArgs(RoutedEvent? routedEvent, object? source)
: base(routedEvent, source)
{
}

/// <summary>
/// Gets or sets a value indicating whether the routed event should be canceled.
/// </summary>
public bool Cancel { get; set; } = false;
}
}
130 changes: 72 additions & 58 deletions src/Avalonia.Controls/Expander.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,11 @@ public class Expander : HeaderedContentControl
/// <summary>
/// Defines the <see cref="IsExpanded"/> property.
/// </summary>
public static readonly DirectProperty<Expander, bool> IsExpandedProperty =
AvaloniaProperty.RegisterDirect<Expander, bool>(
public static readonly StyledProperty<bool> IsExpandedProperty =
AvaloniaProperty.Register<Expander, bool>(
nameof(IsExpanded),
o => o.IsExpanded,
(o, v) => o.IsExpanded = v,
defaultBindingMode: Data.BindingMode.TwoWay);
defaultBindingMode: BindingMode.TwoWay,
coerce: CoerceIsExpanded);

/// <summary>
/// Defines the <see cref="Collapsed"/> event.
Expand All @@ -77,8 +76,8 @@ public class Expander : HeaderedContentControl
/// <summary>
/// Defines the <see cref="Collapsing"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> CollapsingEvent =
RoutedEvent.Register<Expander, RoutedEventArgs>(
public static readonly RoutedEvent<CancelRoutedEventArgs> CollapsingEvent =
RoutedEvent.Register<Expander, CancelRoutedEventArgs>(
nameof(Collapsing),
RoutingStrategies.Bubble);

Expand All @@ -93,13 +92,12 @@ public class Expander : HeaderedContentControl
/// <summary>
/// Defines the <see cref="Expanding"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> ExpandingEvent =
RoutedEvent.Register<Expander, RoutedEventArgs>(
public static readonly RoutedEvent<CancelRoutedEventArgs> ExpandingEvent =
RoutedEvent.Register<Expander, CancelRoutedEventArgs>(
nameof(Expanding),
RoutingStrategies.Bubble);

private bool _ignorePropertyChanged = false;
private bool _isExpanded;
private CancellationTokenSource? _lastTransitionCts;

/// <summary>
Expand Down Expand Up @@ -134,50 +132,8 @@ public ExpandDirection ExpandDirection
/// </summary>
public bool IsExpanded
{
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);
}
}
}
get => GetValue(IsExpandedProperty);
set => SetValue(IsExpandedProperty, value);
}

/// <summary>
Expand All @@ -193,10 +149,10 @@ public event EventHandler<RoutedEventArgs>? Collapsed
/// Occurs as the content area is closing.
/// </summary>
/// <remarks>
/// The event args <see cref="RoutedEventArgs.Handled"/> property may be set to true to cancel the event
/// The event args <see cref="CancelRoutedEventArgs.Cancel"/> property may be set to true to cancel the event
/// and keep the control open (expanded).
/// </remarks>
public event EventHandler<RoutedEventArgs>? Collapsing
public event EventHandler<CancelRoutedEventArgs>? Collapsing
{
add => AddHandler(CollapsingEvent, value);
remove => RemoveHandler(CollapsingEvent, value);
Expand All @@ -215,10 +171,10 @@ public event EventHandler<RoutedEventArgs>? Expanded
/// Occurs as the content area is opening.
/// </summary>
/// <remarks>
/// The event args <see cref="RoutedEventArgs.Handled"/> property may be set to true to cancel the event
/// The event args <see cref="CancelRoutedEventArgs.Cancel"/> property may be set to true to cancel the event
/// and keep the control closed (collapsed).
/// </remarks>
public event EventHandler<RoutedEventArgs>? Expanding
public event EventHandler<CancelRoutedEventArgs>? Expanding
{
add => AddHandler(ExpandingEvent, value);
remove => RemoveHandler(ExpandingEvent, value);
Expand Down Expand Up @@ -332,5 +288,63 @@ private void UpdatePseudoClasses()

PseudoClasses.Set(":expanded", IsExpanded);
}

/// <summary>
/// Called when the <see cref="IsExpanded"/> property has to be coerced.
/// </summary>
/// <param name="value">The value to coerce.</param>
protected virtual bool OnCoerceIsExpanded(bool value)
{
CancelRoutedEventArgs eventArgs;

if (value)
{
eventArgs = new CancelRoutedEventArgs(ExpandingEvent, this);
OnExpanding(eventArgs);
}
else
{
eventArgs = new CancelRoutedEventArgs(CollapsingEvent, this);
OnCollapsing(eventArgs);
}

if (eventArgs.Cancel)
{
// If the event was externally 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 and is also used for animations.
_ignorePropertyChanged = true;

RaisePropertyChanged(
IsExpandedProperty,
oldValue: value,
newValue: !value,
BindingPriority.LocalValue,
isEffectiveValue: true);

_ignorePropertyChanged = false;

return !value;
}

return value;
}

/// <summary>
/// Coerces/validates the <see cref="IsExpanded"/> property value.
/// </summary>
/// <param name="instance">The <see cref="Expander"/> instance.</param>
/// <param name="value">The value to coerce.</param>
/// <returns>The coerced/validated value.</returns>
private static bool CoerceIsExpanded(AvaloniaObject instance, bool value)
{
if (instance is Expander expander)
{
return expander.OnCoerceIsExpanded(value);
}

return value;
}
}
}

0 comments on commit 2072b19

Please sign in to comment.