Browse Source

Merge pull request #9555 from robloo/expander-events

Add Expander Events
pull/9568/head
Max Katz 3 years ago
committed by GitHub
parent
commit
1d7e5a2537
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      samples/ControlCatalog/Pages/ExpanderPage.xaml
  2. 6
      samples/ControlCatalog/Pages/ExpanderPage.xaml.cs
  3. 251
      src/Avalonia.Controls/Expander.cs
  4. 2
      src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs
  5. 15
      src/Avalonia.Themes.Fluent/Controls/Expander.xaml

18
samples/ControlCatalog/Pages/ExpanderPage.xaml

@ -52,6 +52,24 @@
</StackPanel> </StackPanel>
</Expander> </Expander>
<CheckBox IsChecked="{Binding Rounded}">Rounded</CheckBox> <CheckBox IsChecked="{Binding Rounded}">Rounded</CheckBox>
<Expander x:Name="CollapsingDisabledExpander"
Header="Collapsing Disabled"
IsExpanded="True"
ExpandDirection="Down"
CornerRadius="{Binding CornerRadius}">
<StackPanel>
<TextBlock>Expanded content</TextBlock>
</StackPanel>
</Expander>
<Expander x:Name="ExpandingDisabledExpander"
Header="Expanding Disabled"
IsExpanded="False"
ExpandDirection="Down"
CornerRadius="{Binding CornerRadius}">
<StackPanel>
<TextBlock>Expanded content</TextBlock>
</StackPanel>
</Expander>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

6
samples/ControlCatalog/Pages/ExpanderPage.xaml.cs

@ -10,6 +10,12 @@ namespace ControlCatalog.Pages
{ {
this.InitializeComponent(); this.InitializeComponent();
DataContext = new ExpanderPageViewModel(); DataContext = new ExpanderPageViewModel();
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; };
} }
private void InitializeComponent() private void InitializeComponent()

251
src/Avalonia.Controls/Expander.cs

@ -1,7 +1,11 @@
using System;
using System.Threading; using System.Threading;
using Avalonia.Animation; using Avalonia.Animation;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Threading;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -37,12 +41,24 @@ namespace Avalonia.Controls
[PseudoClasses(":expanded", ":up", ":down", ":left", ":right")] [PseudoClasses(":expanded", ":up", ":down", ":left", ":right")]
public class Expander : HeaderedContentControl public class Expander : HeaderedContentControl
{ {
/// <summary>
/// Defines the <see cref="ContentTransition"/> property.
/// </summary>
public static readonly StyledProperty<IPageTransition?> ContentTransitionProperty = public static readonly StyledProperty<IPageTransition?> ContentTransitionProperty =
AvaloniaProperty.Register<Expander, IPageTransition?>(nameof(ContentTransition)); AvaloniaProperty.Register<Expander, IPageTransition?>(
nameof(ContentTransition));
/// <summary>
/// Defines the <see cref="ExpandDirection"/> property.
/// </summary>
public static readonly StyledProperty<ExpandDirection> ExpandDirectionProperty = public static readonly StyledProperty<ExpandDirection> ExpandDirectionProperty =
AvaloniaProperty.Register<Expander, ExpandDirection>(nameof(ExpandDirection), ExpandDirection.Down); AvaloniaProperty.Register<Expander, ExpandDirection>(
nameof(ExpandDirection),
ExpandDirection.Down);
/// <summary>
/// Defines the <see cref="IsExpanded"/> property.
/// </summary>
public static readonly DirectProperty<Expander, bool> IsExpandedProperty = public static readonly DirectProperty<Expander, bool> IsExpandedProperty =
AvaloniaProperty.RegisterDirect<Expander, bool>( AvaloniaProperty.RegisterDirect<Expander, bool>(
nameof(IsExpanded), nameof(IsExpanded),
@ -50,47 +66,206 @@ namespace Avalonia.Controls
(o, v) => o.IsExpanded = v, (o, v) => o.IsExpanded = v,
defaultBindingMode: Data.BindingMode.TwoWay); defaultBindingMode: Data.BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="Collapsed"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> CollapsedEvent =
RoutedEvent.Register<Expander, RoutedEventArgs>(
nameof(Collapsed),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="Collapsing"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> CollapsingEvent =
RoutedEvent.Register<Expander, RoutedEventArgs>(
nameof(Collapsing),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="Expanded"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> ExpandedEvent =
RoutedEvent.Register<Expander, RoutedEventArgs>(
nameof(Expanded),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="Expanding"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> ExpandingEvent =
RoutedEvent.Register<Expander, RoutedEventArgs>(
nameof(Expanding),
RoutingStrategies.Bubble);
private bool _ignorePropertyChanged = false;
private bool _isExpanded; private bool _isExpanded;
private CancellationTokenSource? _lastTransitionCts; private CancellationTokenSource? _lastTransitionCts;
static Expander() /// <summary>
{ /// Initializes a new instance of the <see cref="Expander"/> class.
IsExpandedProperty.Changed.AddClassHandler<Expander>((x, e) => x.OnIsExpandedChanged(e)); /// </summary>
}
public Expander() public Expander()
{ {
UpdatePseudoClasses(ExpandDirection); UpdatePseudoClasses();
} }
/// <summary>
/// Gets or sets the transition used when expanding or collapsing the content.
/// </summary>
public IPageTransition? ContentTransition public IPageTransition? ContentTransition
{ {
get => GetValue(ContentTransitionProperty); get => GetValue(ContentTransitionProperty);
set => SetValue(ContentTransitionProperty, value); set => SetValue(ContentTransitionProperty, value);
} }
/// <summary>
/// Gets or sets the direction in which the <see cref="Expander"/> opens.
/// </summary>
public ExpandDirection ExpandDirection public ExpandDirection ExpandDirection
{ {
get => GetValue(ExpandDirectionProperty); get => GetValue(ExpandDirectionProperty);
set => SetValue(ExpandDirectionProperty, value); set => SetValue(ExpandDirectionProperty, value);
} }
/// <summary>
/// Gets or sets a value indicating whether the <see cref="Expander"/>
/// content area is open and visible.
/// </summary>
public bool IsExpanded public bool IsExpanded
{ {
get { return _isExpanded; } get => _isExpanded;
set set
{ {
SetAndRaise(IsExpandedProperty, ref _isExpanded, value); // It is important here that IsExpanded is a direct property so events can be invoked
PseudoClasses.Set(":expanded", value); // 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) /// <summary>
/// Occurs after the content area has closed and only the header is visible.
/// </summary>
public event EventHandler<RoutedEventArgs>? Collapsed
{
add => AddHandler(CollapsedEvent, value);
remove => RemoveHandler(CollapsedEvent, value);
}
/// <summary>
/// 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
/// and keep the control open (expanded).
/// </remarks>
public event EventHandler<RoutedEventArgs>? Collapsing
{
add => AddHandler(CollapsingEvent, value);
remove => RemoveHandler(CollapsingEvent, value);
}
/// <summary>
/// Occurs after the <see cref="Expander"/> has opened to display both its header and content.
/// </summary>
public event EventHandler<RoutedEventArgs>? Expanded
{
add => AddHandler(ExpandedEvent, value);
remove => RemoveHandler(ExpandedEvent, value);
}
/// <summary>
/// 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
/// and keep the control closed (collapsed).
/// </remarks>
public event EventHandler<RoutedEventArgs>? Expanding
{
add => AddHandler(ExpandingEvent, value);
remove => RemoveHandler(ExpandingEvent, value);
}
/// <summary>
/// Invoked just before the <see cref="Collapsed"/> event.
/// </summary>
protected virtual void OnCollapsed(RoutedEventArgs eventArgs)
{
RaiseEvent(eventArgs);
}
/// <summary>
/// Invoked just before the <see cref="Collapsing"/> event.
/// </summary>
protected virtual void OnCollapsing(RoutedEventArgs eventArgs)
{
RaiseEvent(eventArgs);
}
/// <summary>
/// Invoked just before the <see cref="Expanded"/> event.
/// </summary>
protected virtual void OnExpanded(RoutedEventArgs eventArgs)
{
RaiseEvent(eventArgs);
}
/// <summary>
/// Invoked just before the <see cref="Expanding"/> event.
/// </summary>
protected virtual void OnExpanding(RoutedEventArgs eventArgs)
{
RaiseEvent(eventArgs);
}
/// <summary>
/// Starts the content transition (if set) and invokes the <see cref="Expanded"/>
/// and <see cref="Collapsed"/> events when completed.
/// </summary>
private async void StartContentTransition()
{ {
if (Content != null && ContentTransition != null && Presenter is Visual visualContent) if (Content != null && ContentTransition != null && Presenter is Visual visualContent)
{ {
bool forward = ExpandDirection == ExpandDirection.Left || bool forward = ExpandDirection == ExpandDirection.Left ||
ExpandDirection == ExpandDirection.Up; ExpandDirection == ExpandDirection.Up;
_lastTransitionCts?.Cancel(); _lastTransitionCts?.Cancel();
_lastTransitionCts = new CancellationTokenSource(); _lastTransitionCts = new CancellationTokenSource();
@ -104,24 +279,58 @@ namespace Avalonia.Controls
await ContentTransition.Start(visualContent, null, forward, _lastTransitionCts.Token); 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));
}
});
} }
/// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (_ignorePropertyChanged)
{
return;
}
if (change.Property == ExpandDirectionProperty) if (change.Property == ExpandDirectionProperty)
{ {
UpdatePseudoClasses(change.GetNewValue<ExpandDirection>()); UpdatePseudoClasses();
}
else if (change.Property == IsExpandedProperty)
{
// Expanded/Collapsed will be raised once transitions are complete
StartContentTransition();
UpdatePseudoClasses();
} }
} }
private void UpdatePseudoClasses(ExpandDirection d) /// <summary>
/// Updates the visual state of the control by applying latest PseudoClasses.
/// </summary>
private void UpdatePseudoClasses()
{ {
PseudoClasses.Set(":up", d == ExpandDirection.Up); var expandDirection = ExpandDirection;
PseudoClasses.Set(":down", d == ExpandDirection.Down);
PseudoClasses.Set(":left", d == ExpandDirection.Left); PseudoClasses.Set(":up", expandDirection == ExpandDirection.Up);
PseudoClasses.Set(":right", d == ExpandDirection.Right); PseudoClasses.Set(":down", expandDirection == ExpandDirection.Down);
PseudoClasses.Set(":left", expandDirection == ExpandDirection.Left);
PseudoClasses.Set(":right", expandDirection == ExpandDirection.Right);
PseudoClasses.Set(":expanded", IsExpanded);
} }
} }
} }

2
src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs

@ -18,7 +18,7 @@ namespace Avalonia.Controls
} }
} }
base.OnKeyDown(e); base.OnKeyDown(e);
} }
} }
} }

15
src/Avalonia.Themes.Fluent/Controls/Expander.xaml

@ -103,17 +103,18 @@
RenderTransformOrigin="50%,50%" RenderTransformOrigin="50%,50%"
Stretch="None" Stretch="None"
Stroke="{DynamicResource ExpanderChevronForeground}" Stroke="{DynamicResource ExpanderChevronForeground}"
StrokeThickness="1" /> StrokeThickness="1">
<Border.RenderTransform> <Path.RenderTransform>
<RotateTransform /> <RotateTransform />
</Border.RenderTransform> </Path.RenderTransform>
</Path>
</Border> </Border>
</Grid> </Grid>
</Border> </Border>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
<Style Selector="^:checked /template/ Border#ExpandCollapseChevronBorder"> <Style Selector="^:checked /template/ Path#ExpandCollapseChevron">
<Style.Animations> <Style.Animations>
<Animation FillMode="Both" Duration="0:0:0.0625"> <Animation FillMode="Both" Duration="0:0:0.0625">
<KeyFrame Cue="100%"> <KeyFrame Cue="100%">
@ -122,8 +123,8 @@
</Animation> </Animation>
</Style.Animations> </Style.Animations>
</Style> </Style>
<Style Selector="^:not(:checked) /template/ Border#ExpandCollapseChevronBorder"> <Style Selector="^:not(:checked) /template/ Path#ExpandCollapseChevron">
<Style.Animations> <Style.Animations>
<Animation FillMode="Both" Duration="0:0:0.0625"> <Animation FillMode="Both" Duration="0:0:0.0625">
<KeyFrame Cue="0%"> <KeyFrame Cue="0%">

Loading…
Cancel
Save