Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docfx/articles/interactions/event-trigger-behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,29 @@
</Button>
```

### Flyout lifecycle events

Behaviors can be attached directly to a `Flyout` to observe its `Opened` and `Closed` events. When the flyout is shown, the behavior and its actions inherit the placement target's data context, so command bindings resolve normally.

```xml
<Button Content="Open menu">
<Button.Flyout>
<Flyout>
<Interaction.Behaviors>
<EventTriggerBehavior EventName="Opened">
<InvokeCommandAction Command="{Binding FlyoutOpenedCommand}" />
</EventTriggerBehavior>
<EventTriggerBehavior EventName="Closed">
<InvokeCommandAction Command="{Binding FlyoutClosedCommand}" />
</EventTriggerBehavior>
</Interaction.Behaviors>

<TextBlock Text="Flyout content" />
</Flyout>
</Button.Flyout>
</Button>
```

## EventTrigger

`EventTrigger` is a deprecated class that functions similarly to `EventTriggerBehavior` but inherits from `Trigger`. It is recommended to use `EventTriggerBehavior` instead.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ static AddEventHandlerRegistry()
(o, h) => o.Click += h,
(o, h) => o.Click -= h));

Register(new FlyoutEventHandler());

Register(new FuncAddEventHandler<Control, CancelRoutedEventArgs>(
"ToolTipOpening",
ToolTip.AddToolTipOpeningHandler,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Wiesław Šoltés. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
using System;
using Avalonia.Controls.Primitives;

namespace Avalonia.Xaml.Interactivity;

internal sealed class FlyoutEventHandler : IAddEventHandler
{
public bool Matches(object source, string eventName)
{
return source is FlyoutBase &&
eventName is nameof(FlyoutBase.Opened) or nameof(FlyoutBase.Closed);
}

public IDisposable? AddHandler(
object source,
string eventName,
Action<object?, object> handler)
{
if (source is not FlyoutBase target)
{
return null;
}

EventHandler eventHandler = (sender, eventArgs) => handler(sender, eventArgs);

switch (eventName)
{
case nameof(FlyoutBase.Opened):
target.Opened += eventHandler;
return DisposableAction.Create(() => target.Opened -= eventHandler);

case nameof(FlyoutBase.Closed):
target.Closed += eventHandler;
return DisposableAction.Create(() => target.Closed -= eventHandler);

default:
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Diagnostics;
using System.Globalization;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Reactive;

namespace Avalonia.Xaml.Interactivity;
Expand Down Expand Up @@ -275,18 +276,22 @@ internal virtual void AttachBehaviorToLogicalTree()
parent = topLevel;
templatedParent = topLevel.TemplatedParent;
}

if (parent is null)
else if (AssociatedObject is FlyoutBase { Target: { } target })
{
parent = target;
templatedParent = target.TemplatedParent;
}
else if (AssociatedObject is StyledElement styledElement && styledElement.Parent is not null)
{
if (AssociatedObject is not StyledElement styledElement || styledElement.Parent is null)
{
return;
}

parent = styledElement;
templatedParent = styledElement.TemplatedParent;
}

if (parent is null)
{
return;
}

// Required for $parent binding in XAML
((ISetLogicalParent)this).SetParent(parent);

Expand Down Expand Up @@ -325,6 +330,46 @@ internal virtual void DetachBehaviorFromLogicalTree()
}));
}

if (associatedObject is FlyoutBase flyout)
{
IDisposable? targetDataContextSubscription = null;

void TargetChanged(Control? target)
{
targetDataContextSubscription?.Dispose();
targetDataContextSubscription = null;

if (Parent is not null || TemplatedParent is not null)
{
DetachBehaviorFromLogicalTree();
}

if (target is null)
{
SetCurrentValue(DataContextProperty, null);
return;
}

AttachBehaviorToLogicalTree();
targetDataContextSubscription = target
.GetObservable(DataContextProperty)
.Subscribe(new AnonymousObserver<object?>(x =>
{
SetCurrentValue(DataContextProperty, x);
}));
}

var targetSubscription = flyout
.GetObservable(FlyoutBase.TargetProperty)
.Subscribe(new AnonymousObserver<Control?>(TargetChanged));

return DisposableAction.Create(() =>
{
targetSubscription.Dispose();
targetDataContextSubscription?.Dispose();
});
}

return default;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ private void AttachActionsToLogicalTree(ActionCollection? actions)
}
else
{
if (AssociatedObject is not StyledElement styledElement || styledElement.Parent is null)
if (Parent is null)
{
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Avalonia.Xaml.Interactions.UnitTests.Core"
x:Class="Avalonia.Xaml.Interactions.UnitTests.Core.EventTriggerBehavior005"
x:DataType="local:FlyoutEventBindingSource">
<Window.DataContext>
<local:FlyoutEventBindingSource />
</Window.DataContext>

<Button x:Name="TargetButton" Content="Open flyout">
<Button.Flyout>
<Flyout>
<Interaction.Behaviors>
<EventTriggerBehavior EventName="Opened">
<InvokeCommandAction Command="{Binding OpenedCommand}"
PassEventArgsToCommand="True" />
</EventTriggerBehavior>
<EventTriggerBehavior EventName="Closed">
<InvokeCommandAction Command="{Binding ClosedCommand}"
PassEventArgsToCommand="True" />
</EventTriggerBehavior>
</Interaction.Behaviors>
<TextBlock Text="Flyout content" />
</Flyout>
</Button.Flyout>
</Button>
</Window>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Windows.Input;
using Avalonia.Controls;

namespace Avalonia.Xaml.Interactions.UnitTests.Core;

public partial class EventTriggerBehavior005 : Window
{
public EventTriggerBehavior005()
{
InitializeComponent();
}
}

public sealed class FlyoutEventBindingSource
{
public FlyoutEventBindingSource()
{
OpenedCommand = new Command(parameter =>
{
OpenedCount++;
OpenedParameter = parameter;
});
ClosedCommand = new Command(parameter =>
{
ClosedCount++;
ClosedParameter = parameter;
});
}

public ICommand OpenedCommand { get; }

public ICommand ClosedCommand { get; }

public int OpenedCount { get; private set; }

public int ClosedCount { get; private set; }

public object? OpenedParameter { get; private set; }

public object? ClosedParameter { get; private set; }
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;
using Avalonia.Controls;
using Avalonia.Headless;
using Avalonia.Headless.XUnit;
using Xunit;
Expand Down Expand Up @@ -62,4 +64,24 @@ public void EventTriggerBehavior_004()

Assert.Equal("Loaded Text", window.TargetTextBox.Text);
}

[AvaloniaFact]
public void EventTriggerBehavior_005_FlyoutEventsExecuteBoundCommands()
{
var window = new EventTriggerBehavior005();
var source = Assert.IsType<FlyoutEventBindingSource>(window.DataContext);
var flyout = Assert.IsType<Flyout>(window.TargetButton.Flyout);

window.Show();
flyout.ShowAt(window.TargetButton);

Assert.Equal(1, source.OpenedCount);
Assert.Same(EventArgs.Empty, source.OpenedParameter);
Assert.Equal(0, source.ClosedCount);

flyout.Hide();

Assert.Equal(1, source.ClosedCount);
Assert.Same(EventArgs.Empty, source.ClosedParameter);
}
}
Loading