diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c0fa8186..b38aea89 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -18,7 +18,7 @@ on:
env:
DOTNET_VERSION: ${{ '7.0.x' }}
- ENABLE_DIAGNOSTICS: false
+ ENABLE_DIAGNOSTICS: true
#COREHOST_TRACE: 1
COREHOST_TRACEFILE: corehosttrace.log
MULTI_TARGET_DIRECTORY: tooling/MultiTarget
@@ -68,6 +68,22 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
+ - name: Configure Pagefile
+ uses: al-cheb/configure-pagefile-action@v1.3
+ with:
+ minimum-size: 32GB
+ maximum-size: 32GB
+ disk-root: "C:"
+
+ - name: Enable User-Mode Dumps collecting
+ if: ${{ env.ENABLE_DIAGNOSTICS == 'true' || env.COREHOST_TRACE != '' }}
+ shell: powershell
+ run: |
+ New-Item '${{ github.workspace }}\CrashDumps' -Type Directory
+ Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps' -Name 'DumpFolder' -Type ExpandString -Value '${{ github.workspace }}\CrashDumps'
+ Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps' -Name 'DumpCount' -Type DWord -Value '10'
+ Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps' -Name 'DumpType' -Type DWord -Value '2'
+
- name: Install .NET SDK v${{ env.DOTNET_VERSION }}
uses: actions/setup-dotnet@v3
with:
@@ -106,8 +122,13 @@ jobs:
run: powershell -version 5.1 -command "./UseUnoWinUI.ps1 3" -ErrorAction Stop
if: ${{ matrix.platform == 'WinUI3' }}
+ - name: MSBuild (With diagnostics)
+ if: ${{ env.ENABLE_DIAGNOSTICS == 'true' }}
+ run: msbuild.exe CommunityToolkit.AllComponents.sln /restore /nowarn:MSB4011 -p:Configuration=Release -m /bl -v:diag
+
- name: MSBuild
- run: msbuild.exe CommunityToolkit.AllComponents.sln /restore /nowarn:MSB4011 -p:Configuration=Release -m -p:UseDotNetNativeToolchain=false
+ if: ${{ env.ENABLE_DIAGNOSTICS == 'false' }}
+ run: msbuild.exe CommunityToolkit.AllComponents.sln /restore /nowarn:MSB4011 -p:Configuration=Release -m
# Build All Packages
- name: pack experiments
@@ -125,7 +146,7 @@ jobs:
# Run tests
- name: Setup VSTest Path
- uses: darenm/Setup-VSTest@v1
+ uses: darenm/Setup-VSTest@v1.2
- name: Install Testspace Module
uses: testspace-com/setup-testspace@v1
@@ -152,6 +173,36 @@ jobs:
name: build-logs
path: ./**/*.*log
+ - name: Artifact - ILC Repro
+ uses: actions/upload-artifact@v3
+ if: ${{ (env.ENABLE_DIAGNOSTICS == 'true' || env.COREHOST_TRACE != '') && always() }}
+ with:
+ name: ilc-repro
+ path: ./*.zip
+
+ # https://github.com/dorny/paths-filter#custom-processing-of-changed-files
+ - name: Detect If any Dump Files
+ uses: dorny/paths-filter@v2.11.1
+ id: filter
+ with:
+ list-files: shell
+ filters: |
+ dump:
+ - added: '${{ github.workspace }}/CrashDumps/*.dmp'
+
+ - name: Artifact - WER crash dumps
+ uses: actions/upload-artifact@v3
+ if: ${{ steps.filter.outputs.dump == 'true' && (env.ENABLE_DIAGNOSTICS == 'true' || env.COREHOST_TRACE != '') && always() }}
+ with:
+ name: CrashDumps-${{ matrix.platform }}
+ path: '${{ github.workspace }}/CrashDumps'
+
+ - name: Analyze Dump
+ if: ${{ steps.filter.outputs.dump == 'true' && (env.ENABLE_DIAGNOSTICS == 'true' || env.COREHOST_TRACE != '') && always() }}
+ run: |
+ dotnet tool install --global dotnet-dump
+ dotnet-dump analyze ${{ steps.filter.outputs.dump_files }} -c "clrstack" -c "pe -lines" -c "exit"
+
wasm-linux:
runs-on: ubuntu-latest
diff --git a/components/Behaviors/samples/Assets/StackedNotificationsBehavior.png b/components/Behaviors/samples/Assets/StackedNotificationsBehavior.png
new file mode 100644
index 00000000..a90aed35
Binary files /dev/null and b/components/Behaviors/samples/Assets/StackedNotificationsBehavior.png differ
diff --git a/components/Behaviors/samples/Behaviors.Samples.csproj b/components/Behaviors/samples/Behaviors.Samples.csproj
index 2f1a83e6..9f3e1b2a 100644
--- a/components/Behaviors/samples/Behaviors.Samples.csproj
+++ b/components/Behaviors/samples/Behaviors.Samples.csproj
@@ -15,7 +15,7 @@
- Always
+ PreserveNewest
diff --git a/components/Behaviors/samples/StackedNotificationsBehavior.md b/components/Behaviors/samples/StackedNotificationsBehavior.md
new file mode 100644
index 00000000..22c6699e
--- /dev/null
+++ b/components/Behaviors/samples/StackedNotificationsBehavior.md
@@ -0,0 +1,77 @@
+---
+title: StackedNotificationsBehavior
+author: vgromfeld
+description: A behavior to add stacked notifications to a WinUI InfoBar control.
+keywords: StackedNotificationsBehavior, Control, Layout, InfoBar, Behavior
+dev_langs:
+ - csharp
+category: Xaml
+subcategory: Behaviors
+discussion-id: 0
+issue-id: 0
+icon: Assets/StackedNotificationsBehavior.png
+---
+
+The `StackedNotificationsBehavior` allows you to provide notifications within your app using an `InfoBar` control. This is a replacement for the prior `InAppNotification` control in the Toolkit.
+
+With the default settings, a notification will be displayed until it is dismissed by the user. Any subsequent notifications will be displayed
+in the order of being sent afterwards one-by-one.
+
+## Example
+
+Clicking on the button multiple times will queue up multiple messages to be displayed one after another.
+
+> [!Sample StackedNotificationsBehaviorCustomSample]
+
+## Notification options
+
+By default, the properties provided on the attached `InfoBar` will be used, like `ContentTemplate` or `IsIconVisible`.
+
+However, there are a number of options available on the `Notification` class to override these. When set, these will override any defaults
+or modified properties set on the parent `InfoBar` itself. They will be restored to the previously set value on the `InfoBar` after the message has been displayed.
+
+> [!WARNING]
+> Properties set on the `InfoBar` will be modified directly by the behavior with notification overrides, this means any bindings will
+> be broken by that change when it is overridden or restored by the notification. Therefore, it is best to only provide constants on the
+> parent `InfoBar` itself that will be consistent for all messages and set any dynamic options in the `Notification` options.
+
+When a `Duration` is provided, if the user has their pointer over the message, it will not be dismissed. It will instead reset the time before
+being dismissed once the pointer has left the active notification.
+
+## Migrating from InAppNotification
+
+If you previously used the `InAppNotification` component from the Windows Community Toolkit, like so:
+
+```xml
+
+```
+
+You can simply replace it with an `InfoBar` control and add the attached behavior:
+
+```xml
+
+
+
+
+
+```
+
+There are some changes to the `Show` method, however a simple text based one has been provided for backwards compatibility,
+otherwise it's best to construct your own `Notification` object for greater flexibility or set common properties on the
+parent `InfoBar` itself.
+
+> [!NOTE]
+> There is no `StackMode` property to control the behavior of the queue. Providing a stable queue of messages one after another
+> provides the best user experience as it reduces the risk when interacting with a notification for a new one to suddenly appear
+> and replace the one being displayed.
+
+The `ShowDismissButton` property should be mapped to the `InfoBar.IsClosable` property instead. Similar to any adjustments to position
+should be handled by the layout of the `InfoBar` control itself within the XAML layout.
+
+The `Closing` and `Closed` events can be mapped to those on the `InfoBar` as well.
+
+### Complete example
+
+This example shows sending simple text based notifications that will appear only for 2 seconds:
+
+> [!Sample StackedNotificationsBehaviorToolkitSample]
diff --git a/components/Behaviors/samples/StackedNotificationsBehaviorCustomSample.xaml b/components/Behaviors/samples/StackedNotificationsBehaviorCustomSample.xaml
new file mode 100644
index 00000000..1c57592f
--- /dev/null
+++ b/components/Behaviors/samples/StackedNotificationsBehaviorCustomSample.xaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Behaviors/samples/StackedNotificationsBehaviorCustomSample.xaml.cs b/components/Behaviors/samples/StackedNotificationsBehaviorCustomSample.xaml.cs
new file mode 100644
index 00000000..c3250e3f
--- /dev/null
+++ b/components/Behaviors/samples/StackedNotificationsBehaviorCustomSample.xaml.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI.Behaviors;
+
+namespace BehaviorsExperiment.Samples;
+
+[ToolkitSample(id: nameof(StackedNotificationsBehaviorCustomSample), "Stacked Notifications", description: $"A sample for showing how to create and use a {nameof(StackedNotificationsBehavior)} custom behavior.")]
+public sealed partial class StackedNotificationsBehaviorCustomSample : Page
+{
+ public StackedNotificationsBehaviorCustomSample()
+ {
+ this.InitializeComponent();
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ var notification = new Notification
+ {
+ Title = $"Notification {DateTimeOffset.Now}",
+ Message = GetRandomText(),
+ Severity = MUXC.InfoBarSeverity.Informational,
+ };
+
+ NotificationQueue.Show(notification);
+ }
+
+ private static int _current = 0;
+
+ private static string GetRandomText() => (_current++ % 4) switch
+ {
+ 1 => "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sollicitudin bibendum enim at tincidunt. Praesent egestas ipsum ligula, nec tincidunt lacus semper non.",
+ 2 => "Pellentesque in risus eget leo rhoncus ultricies nec id ante.",
+ 3 => "Sed quis nisi quis nunc condimentum varius id consectetur metus. Duis mauris sapien, commodo eget erat ac, efficitur iaculis magna. Morbi eu velit nec massa pharetra cursus.",
+ _ => "Fusce non quam egestas leo finibus interdum eu ac massa. Quisque nec justo leo. Aenean scelerisque placerat ultrices. Sed accumsan lorem at arcu commodo tristique.",
+ };
+}
diff --git a/components/Behaviors/samples/StackedNotificationsBehaviorToolkitSample.xaml b/components/Behaviors/samples/StackedNotificationsBehaviorToolkitSample.xaml
new file mode 100644
index 00000000..d1c5020c
--- /dev/null
+++ b/components/Behaviors/samples/StackedNotificationsBehaviorToolkitSample.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Behaviors/samples/StackedNotificationsBehaviorToolkitSample.xaml.cs b/components/Behaviors/samples/StackedNotificationsBehaviorToolkitSample.xaml.cs
new file mode 100644
index 00000000..9ad652de
--- /dev/null
+++ b/components/Behaviors/samples/StackedNotificationsBehaviorToolkitSample.xaml.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI.Behaviors;
+
+namespace BehaviorsExperiment.Samples;
+
+[ToolkitSample(id: nameof(StackedNotificationsBehaviorToolkitSample), "Stacked Notification Migration", description: $"A sample for showing how to create and use a {nameof(StackedNotificationsBehavior)} custom behavior upgrading from InAppNotification.")]
+public sealed partial class StackedNotificationsBehaviorToolkitSample : Page
+{
+ public StackedNotificationsBehaviorToolkitSample()
+ {
+ this.InitializeComponent();
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ // Show our notification for 2 seconds
+ NotificationQueue.Show(GetRandomText(), 2000);
+ }
+
+ private static int _current = 0;
+
+ private static string GetRandomText() => (_current++ % 4) switch
+ {
+ 1 => "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sollicitudin bibendum enim at tincidunt. Praesent egestas ipsum ligula, nec tincidunt lacus semper non.",
+ 2 => "Pellentesque in risus eget leo rhoncus ultricies nec id ante.",
+ 3 => "Sed quis nisi quis nunc condimentum varius id consectetur metus. Duis mauris sapien, commodo eget erat ac, efficitur iaculis magna. Morbi eu velit nec massa pharetra cursus.",
+ _ => "Fusce non quam egestas leo finibus interdum eu ac massa. Quisque nec justo leo. Aenean scelerisque placerat ultrices. Sed accumsan lorem at arcu commodo tristique.",
+ };
+}
diff --git a/components/Behaviors/src/Notification/Notification.cs b/components/Behaviors/src/Notification/Notification.cs
new file mode 100644
index 00000000..c9a30225
--- /dev/null
+++ b/components/Behaviors/src/Notification/Notification.cs
@@ -0,0 +1,138 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace CommunityToolkit.WinUI.Behaviors;
+
+///
+/// The content of a notification to display in .
+/// The and values will
+/// always be applied to the targeted .
+/// The , , , , , and values
+/// will be applied only if set, otherwise the parent values will be used, if available.
+///
+public class Notification
+{
+ private NotificationOverrides _overrides;
+ private MUXC.InfoBarSeverity? _severity;
+ private bool _isIconVisible = true; // Default for InfoBar
+ private MUXC.IconSource? _iconSource;
+ private object? _content;
+ private DataTemplate? _contentTemplate;
+ private ButtonBase? _actionButton;
+
+ ///
+ /// Gets or sets the notification title.
+ ///
+ public string? Title { get; set; }
+
+ ///
+ /// Gets or sets the notification message.
+ ///
+ public string? Message { get; set; }
+
+ ///
+ /// Gets or sets the duration of the notification.
+ /// Set to null for persistent notification (default).
+ ///
+ public TimeSpan? Duration { get; set; }
+
+ ///
+ /// Gets or sets the type of the to apply consistent status color, icon,
+ /// and assistive technology settings dependent on the criticality of the notification.
+ /// By default the parent property's value will be used.
+ ///
+ public MUXC.InfoBarSeverity? Severity
+ {
+ get => _severity;
+ set
+ {
+ _severity = value;
+ _overrides |= NotificationOverrides.Severity;
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the icon is visible or not.
+ /// True if the icon is visible; otherwise, false. The default is true.
+ ///
+ public bool IsIconVisible
+ {
+ get => _isIconVisible;
+ set
+ {
+ _isIconVisible = value;
+ _overrides |= NotificationOverrides.IconVisible;
+ }
+ }
+
+ ///
+ /// Gets or sets a value for an to use as the of the for this notification.
+ ///
+ public MUXC.IconSource? IconSource
+ {
+ get => _iconSource;
+ set
+ {
+ _iconSource = value;
+ _overrides |= NotificationOverrides.IconSource;
+ }
+ }
+
+ ///
+ /// Gets or sets the XAML Content that is displayed below the title and message in
+ /// the InfoBar.
+ ///
+ public object? Content
+ {
+ get => _content;
+ set
+ {
+ _content = value;
+ _overrides |= NotificationOverrides.Content;
+ }
+ }
+
+ ///
+ /// Gets or sets the data template for the .
+ ///
+ public DataTemplate? ContentTemplate
+ {
+ get => _contentTemplate;
+ set
+ {
+ _contentTemplate = value;
+ _overrides |= NotificationOverrides.ContentTemplate;
+ }
+ }
+
+ ///
+ /// Gets or sets the action button of the InfoBar.
+ ///
+ public ButtonBase? ActionButton
+ {
+ get => _actionButton;
+ set
+ {
+ _actionButton = value;
+ _overrides |= NotificationOverrides.ActionButton;
+ }
+ }
+
+ internal NotificationOverrides Overrides => _overrides;
+}
+
+///
+/// The overrides which should be set on the notification.
+///
+[Flags]
+internal enum NotificationOverrides
+{
+ None,
+ Severity,
+ IconVisible,
+ IconSource,
+ Content,
+ ContentTemplate,
+ ActionButton,
+}
diff --git a/components/Behaviors/src/Notification/StackedNotificationsBehavior.cs b/components/Behaviors/src/Notification/StackedNotificationsBehavior.cs
new file mode 100644
index 00000000..8b18a399
--- /dev/null
+++ b/components/Behaviors/src/Notification/StackedNotificationsBehavior.cs
@@ -0,0 +1,301 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#if !WINAPPSDK
+using Windows.System;
+using DQ = Windows.System.DispatcherQueue;
+#else
+using Microsoft.UI.Dispatching;
+using DQ = Microsoft.UI.Dispatching.DispatcherQueue;
+#endif
+
+namespace CommunityToolkit.WinUI.Behaviors;
+
+///
+/// A behavior to add the stacked notification support to .
+///
+public class StackedNotificationsBehavior : BehaviorBase
+{
+ private readonly LinkedList _stackedNotifications;
+ private readonly DispatcherQueueTimer _dismissTimer;
+ private Notification? _currentNotification;
+ private string? _initialTitle;
+ private MUXC.InfoBarSeverity? _initialSeverity;
+ private bool _initialIconVisible;
+ private MUXC.IconSource? _initialIconSource;
+ private object? _initialContent;
+ private DataTemplate? _initialContentTemplate;
+ private ButtonBase? _initialActionButton;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public StackedNotificationsBehavior()
+ {
+ _stackedNotifications = new LinkedList();
+
+ // TODO: On WinUI 3 we can use the local DispatcherQueue, so we need to abstract better for UWP
+ var dispatcherQueue = DQ.GetForCurrentThread();
+ _dismissTimer = dispatcherQueue.CreateTimer();
+ _dismissTimer.Tick += OnTimerTick;
+ }
+
+ //// Provided as a simple helper to migrate from older InAppNotification control.
+ ///
+ /// Show notification using text as the of the notification.
+ ///
+ /// string to display as the notification.
+ /// Optional, displayed duration of the notification in ms (less or equal 0 means infinite duration).
+ /// Optional, for the notification.
+ /// The constructed added to the queue.
+ public Notification Show(string message, int duration = 0, string? title = null)
+ {
+ Notification notification = new()
+ {
+ Title = title,
+ Message = message,
+ Duration = duration <= 0 ? null : TimeSpan.FromMilliseconds(duration)
+ };
+
+ return Show(notification);
+ }
+
+ //// Provided as a simple helper to migrate from older InAppNotification control.
+ ///
+ /// Show notification using object or UIElement as the of the notification. Note, it is
+ /// generally best to also specific a message and/or title.
+ ///
+ /// Content to display as the notification.
+ /// Optional, displayed duration of the notification in ms (less or equal 0 means infinite duration).
+ /// Optional, for the notification.
+ /// Optional, string to display as the notification.
+ /// The constructed added to the queue.
+ public Notification Show(object content, int duration = 0, string? title = null, string? message = null)
+ {
+ Notification notification = new()
+ {
+ Title = title,
+ Message = message,
+ Content = content,
+ Duration = duration <= 0 ? null : TimeSpan.FromMilliseconds(duration)
+ };
+
+ return Show(notification);
+ }
+
+ ///
+ /// Show .
+ ///
+ /// The notification to display.
+ /// The added to the queue.
+ public Notification Show(Notification notification)
+ {
+ if (notification is null)
+ {
+ throw new ArgumentNullException(nameof(notification));
+ }
+
+ _stackedNotifications.AddLast(notification);
+ ShowNext();
+
+ return notification;
+ }
+
+ ///
+ /// Remove the .
+ /// If the notification is displayed, it will be closed.
+ /// If the notification is still in the queue, it will be removed.
+ ///
+ /// The notification to remove.
+ public void Remove(Notification notification)
+ {
+ if (notification is null)
+ {
+ throw new ArgumentNullException(nameof(notification));
+ }
+
+ if (notification == _currentNotification)
+ {
+ // We close the notification. This will trigger the display of the next one.
+ // See OnInfoBarClosed.
+ AssociatedObject.IsOpen = false;
+ return;
+ }
+
+ _stackedNotifications.Remove(notification);
+ }
+
+ ///
+ /// Clears all notifications and closes any open notification.
+ ///
+ public void Clear()
+ {
+ _stackedNotifications.Clear();
+
+ AssociatedObject.IsOpen = false;
+ }
+
+ ///
+ protected override bool Initialize()
+ {
+ AssociatedObject.Closed += OnInfoBarClosed;
+ AssociatedObject.PointerEntered += OnPointerEntered;
+ AssociatedObject.PointerExited += OnPointedExited;
+ return true;
+ }
+
+ ///
+ protected override bool Uninitialize()
+ {
+ StopTimer();
+
+ AssociatedObject.Closed -= OnInfoBarClosed;
+ AssociatedObject.PointerEntered -= OnPointerEntered;
+ AssociatedObject.PointerExited -= OnPointedExited;
+ return true;
+ }
+
+ private void OnInfoBarClosed(MUXC.InfoBar sender, MUXC.InfoBarClosedEventArgs args)
+ {
+ // We display the next notification.
+ ShowNext();
+ }
+
+ private void ShowNext()
+ {
+ if (AssociatedObject.IsOpen)
+ {
+ // One notification is already displayed. We wait for it to close
+ return;
+ }
+
+ StopTimer();
+ AssociatedObject.IsOpen = false;
+ RestoreOverridenProperties();
+
+ if (_stackedNotifications.Count == 0)
+ {
+ _currentNotification = null;
+ return;
+ }
+
+ var notificationToDisplay = _stackedNotifications!.First!.Value;
+ _stackedNotifications.RemoveFirst();
+
+ _currentNotification = notificationToDisplay;
+ SetNotification(notificationToDisplay);
+ AssociatedObject.IsOpen = true;
+
+ StartTimer(notificationToDisplay.Duration);
+ }
+
+ private void SetNotification(Notification notification)
+ {
+ if (notification.Title != null)
+ {
+ _initialTitle = AssociatedObject.Title;
+ AssociatedObject.Title = notification.Title;
+ }
+
+ AssociatedObject.Message = notification.Message ?? string.Empty;
+
+ if (notification.Overrides.HasFlag(NotificationOverrides.Severity))
+ {
+ _initialSeverity = AssociatedObject.Severity;
+ AssociatedObject.Severity = notification.Severity!.Value;
+ }
+
+ if (notification.Overrides.HasFlag(NotificationOverrides.IconVisible))
+ {
+ _initialIconVisible = AssociatedObject.IsIconVisible;
+ AssociatedObject.IsIconVisible = notification.IsIconVisible;
+ }
+
+ if (notification.Overrides.HasFlag(NotificationOverrides.IconSource))
+ {
+ _initialIconSource = AssociatedObject.IconSource;
+ AssociatedObject.IconSource = notification.IconSource!;
+ }
+
+ if (notification.Overrides.HasFlag(NotificationOverrides.Content))
+ {
+ _initialContent = AssociatedObject.Content;
+ AssociatedObject.Content = notification.Content!;
+ }
+
+ if (notification.Overrides.HasFlag(NotificationOverrides.ContentTemplate))
+ {
+ _initialContentTemplate = AssociatedObject.ContentTemplate;
+ AssociatedObject.ContentTemplate = notification.ContentTemplate!;
+ }
+
+ if (notification.Overrides.HasFlag(NotificationOverrides.ActionButton))
+ {
+ _initialActionButton = AssociatedObject.ActionButton;
+ AssociatedObject.ActionButton = notification.ActionButton!;
+ }
+ }
+
+ private void RestoreOverridenProperties()
+ {
+ if (_currentNotification is null)
+ {
+ return;
+ }
+
+ if (_currentNotification.Title != null)
+ {
+ AssociatedObject.Title = _initialTitle ?? string.Empty;
+ }
+
+ if (_currentNotification.Overrides.HasFlag(NotificationOverrides.Severity))
+ {
+ AssociatedObject.Severity = _initialSeverity!.Value;
+ }
+
+ if (_currentNotification.Overrides.HasFlag(NotificationOverrides.IconVisible))
+ {
+ AssociatedObject.IsIconVisible = _initialIconVisible;
+ }
+
+ if (_currentNotification.Overrides.HasFlag(NotificationOverrides.IconSource))
+ {
+ AssociatedObject.IconSource = _initialIconSource!;
+ }
+
+ if (_currentNotification.Overrides.HasFlag(NotificationOverrides.Content))
+ {
+ AssociatedObject.Content = _initialContent!;
+ }
+
+ if (_currentNotification.Overrides.HasFlag(NotificationOverrides.ContentTemplate))
+ {
+ AssociatedObject.ContentTemplate = _initialContentTemplate!;
+ }
+
+ if (_currentNotification.Overrides.HasFlag(NotificationOverrides.ActionButton))
+ {
+ AssociatedObject.ActionButton = _initialActionButton!;
+ }
+ }
+
+ private void StartTimer(TimeSpan? duration)
+ {
+ if (duration is null)
+ {
+ return;
+ }
+
+ _dismissTimer.Interval = duration.Value;
+ _dismissTimer.Start();
+ }
+
+ private void StopTimer() => _dismissTimer.Stop();
+
+ private void OnTimerTick(DispatcherQueueTimer sender, object args) => AssociatedObject.IsOpen = false;
+
+ private void OnPointedExited(object sender, PointerRoutedEventArgs e) => StartTimer(_currentNotification?.Duration);
+
+ private void OnPointerEntered(object sender, PointerRoutedEventArgs e) => StopTimer();
+}
diff --git a/components/Behaviors/tests/Behaviors.Tests.projitems b/components/Behaviors/tests/Behaviors.Tests.projitems
index 75ff7d8c..c21c6464 100644
--- a/components/Behaviors/tests/Behaviors.Tests.projitems
+++ b/components/Behaviors/tests/Behaviors.Tests.projitems
@@ -13,11 +13,19 @@
ExampleBehaviorsTestPage.xaml
+
+ StackedNotificationsBehaviorsTestPage.xaml
+
+ DesignerMSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
\ No newline at end of file
diff --git a/components/Behaviors/tests/StackedNotificationsBehaviorTestClass.cs b/components/Behaviors/tests/StackedNotificationsBehaviorTestClass.cs
new file mode 100644
index 00000000..80d43fc4
--- /dev/null
+++ b/components/Behaviors/tests/StackedNotificationsBehaviorTestClass.cs
@@ -0,0 +1,189 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using BehaviorsExperiment.Tests;
+using CommunityToolkit.Tests;
+using CommunityToolkit.Tooling.TestGen;
+using CommunityToolkit.WinUI.Behaviors;
+using Microsoft.Xaml.Interactivity;
+
+namespace StackedNotificationsBehaviorExperiment.Tests;
+
+[TestClass]
+public partial class StackedNotificationsBehaviorTestClass : VisualUITestBase
+{
+ [UIThreadTestMethod]
+ public async Task NotificationBehaviorSingleTest(StackedNotificationsBehaviorsTestPage page)
+ {
+ var infobar = page.FindDescendant();
+ var queue = Interaction.GetBehaviors(infobar)[0] as StackedNotificationsBehavior;
+
+ Assert.IsNotNull(infobar, "Could not find infobar control.");
+ Assert.IsNotNull(queue, "Could not find notification behavior.");
+
+ Notification notification = new()
+ {
+ Title = "Test Title",
+ Message = "Test Message",
+ Severity = MUXC.InfoBarSeverity.Error,
+ };
+
+ queue.Show(notification);
+
+ // Wait for UI update;
+ await EnqueueAsync(() => { });
+
+ Assert.IsTrue(infobar.IsOpen, "InfoBar didn't open.");
+ Assert.AreEqual("Test Title", infobar.Title, "Title not equal to expected value");
+ Assert.AreEqual("Test Message", infobar.Message, "Message not equal to expected value");
+ Assert.AreEqual(MUXC.InfoBarSeverity.Error, infobar.Severity, "Severity not equal to expected value");
+ }
+
+ [UIThreadTestMethod]
+ public async Task NotificationBehaviorOverrideTest(StackedNotificationsBehaviorsTestPage page)
+ {
+ var infobar = page.FindDescendant();
+ var queue = Interaction.GetBehaviors(infobar)[0] as StackedNotificationsBehavior;
+
+ Assert.IsNotNull(infobar, "Could not find infobar control.");
+ Assert.IsNotNull(queue, "Could not find notification behavior.");
+
+ infobar.Title = "Prior Title";
+ infobar.Content = "Prior Content";
+ infobar.Severity = MUXC.InfoBarSeverity.Error;
+
+ Notification notification = new()
+ {
+ Title = "Test Title",
+ Content = "Some Content",
+ };
+
+ queue.Show(notification);
+
+ // Wait for UI update;
+ await EnqueueAsync(() => { });
+
+ Assert.IsTrue(infobar.IsOpen, "InfoBar didn't open.");
+ Assert.AreEqual("Test Title", infobar.Title, "Title not equal to expected value");
+ Assert.AreEqual("Some Content", infobar.Content, "Content not equal to expected value");
+ Assert.AreEqual(MUXC.InfoBarSeverity.Error, infobar.Severity, "Severity not equal to set parent value");
+
+ infobar.IsOpen = false;
+
+ // Wait for UI update;
+ await EnqueueAsync(() => { });
+
+ Assert.AreEqual("Prior Title", infobar.Title, "InfoBar title not reset");
+ Assert.AreEqual("Prior Content", infobar.Content, "InfoBar content not reset");
+ }
+
+ [UIThreadTestMethod]
+ public async Task NotificationBehaviorDurationTest(StackedNotificationsBehaviorsTestPage page)
+ {
+ var infobar = page.FindDescendant();
+ var queue = Interaction.GetBehaviors(infobar)[0] as StackedNotificationsBehavior;
+
+ Assert.IsNotNull(infobar, "Could not find infobar control.");
+ Assert.IsNotNull(queue, "Could not find notification behavior.");
+
+ Notification notification = new()
+ {
+ Message = "Test Message",
+ Duration = TimeSpan.FromMilliseconds(1000),
+ };
+
+ queue.Show(notification);
+
+ // Wait for UI update;
+ await EnqueueAsync(() => { });
+
+ Assert.IsTrue(infobar.IsOpen, "InfoBar didn't open.");
+ Assert.AreEqual(string.Empty, infobar.Title, "Expected no Title value");
+ Assert.AreEqual("Test Message", infobar.Message, "Message not equal to expected value");
+ Assert.AreEqual(MUXC.InfoBarSeverity.Informational, infobar.Severity, "Severity not equal to default value");
+
+ // Wait for little bit more than Duration
+ await Task.Delay(1100);
+
+ Assert.IsFalse(infobar.IsOpen, "InfoBar didn't close after.");
+ }
+
+ [UIThreadTestMethod]
+ public async Task NotificationBehaviorQueueBehindTest(StackedNotificationsBehaviorsTestPage page)
+ {
+ var infobar = page.FindDescendant();
+ var queue = Interaction.GetBehaviors(infobar)[0] as StackedNotificationsBehavior;
+
+ Assert.IsNotNull(infobar, "Could not find infobar control.");
+ Assert.IsNotNull(queue, "Could not find notification behavior.");
+
+ Notification notification1 = new()
+ {
+ Message = "Test Message",
+ Duration = TimeSpan.FromMilliseconds(1000),
+ };
+ Notification notification2 = new()
+ {
+ Message = "Test Message 2",
+ Duration = TimeSpan.FromMilliseconds(1000),
+ };
+
+ queue.Show(notification1);
+ // Queue next message behind first
+ queue.Show(notification2);
+
+ // Wait for UI update;
+ await EnqueueAsync(() => { });
+
+ Assert.IsTrue(infobar.IsOpen, "InfoBar didn't open.");
+ Assert.AreEqual("Test Message", infobar.Message, "First Message not equal to expected value.");
+
+ // Wait for little bit more than Duration
+ await Task.Delay(1100);
+
+ Assert.IsTrue(infobar.IsOpen, "InfoBar didn't remain open.");
+ Assert.AreEqual("Test Message 2", infobar.Message, "Message didn't change to 2nd message value.");
+
+ await Task.Delay(1000);
+
+ Assert.IsFalse(infobar.IsOpen, "InfoBar didn't close after all messages displayed.");
+ }
+
+
+ [UIThreadTestMethod]
+ public async Task NotificationBehaviorCancelCloseTest(StackedNotificationsBehaviorsTestPage page)
+ {
+ var infobar = page.FindDescendant();
+ var queue = Interaction.GetBehaviors(infobar)[0] as StackedNotificationsBehavior;
+
+ Assert.IsNotNull(infobar, "Could not find infobar control.");
+ Assert.IsNotNull(queue, "Could not find notification behavior.");
+
+ bool attemptedClose = false;
+ infobar.Closing += (sender, args) =>
+ {
+ attemptedClose = true;
+ args.Cancel = true;
+ };
+
+ Notification notification = new()
+ {
+ Message = "Test Message",
+ Duration = TimeSpan.FromMilliseconds(500),
+ };
+
+ queue.Show(notification);
+
+ // Wait for UI update;
+ await EnqueueAsync(() => { });
+
+ Assert.IsTrue(infobar.IsOpen, "InfoBar didn't open.");
+ Assert.AreEqual("Test Message", infobar.Message, "Message not equal to expected value");
+
+ await Task.Delay(600);
+
+ Assert.IsTrue(attemptedClose, "InfoBar never attempted to close.");
+ Assert.IsTrue(infobar.IsOpen, "InfoBar didn't remain open.");
+ }
+}
diff --git a/components/Behaviors/tests/StackedNotificationsBehaviorsTestPage.xaml b/components/Behaviors/tests/StackedNotificationsBehaviorsTestPage.xaml
new file mode 100644
index 00000000..069dae69
--- /dev/null
+++ b/components/Behaviors/tests/StackedNotificationsBehaviorsTestPage.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/components/Behaviors/tests/StackedNotificationsBehaviorsTestPage.xaml.cs b/components/Behaviors/tests/StackedNotificationsBehaviorsTestPage.xaml.cs
new file mode 100644
index 00000000..63a81dc7
--- /dev/null
+++ b/components/Behaviors/tests/StackedNotificationsBehaviorsTestPage.xaml.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace BehaviorsExperiment.Tests;
+
+///
+/// An empty page that can be used on its own or navigated to within a Frame.
+///
+public sealed partial class StackedNotificationsBehaviorsTestPage : Page
+{
+ public StackedNotificationsBehaviorsTestPage()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/Extensions/tests/Element/FrameworkElementExtensionsTests.RelativeAncestor.cs b/components/Extensions/tests/Element/FrameworkElementExtensionsTests.RelativeAncestor.cs
index 3a2eeca6..fab4af3d 100644
--- a/components/Extensions/tests/Element/FrameworkElementExtensionsTests.RelativeAncestor.cs
+++ b/components/Extensions/tests/Element/FrameworkElementExtensionsTests.RelativeAncestor.cs
@@ -87,6 +87,7 @@ public async Task FrameworkElementExtension_RelativeAncestor_FreeParent(Framewor
[TestCategory("FrameworkElementExtension")]
[UIThreadTestMethod]
+ [Ignore]
public async Task FrameworkElementExtension_RelativeAncestor_FreePageNavigation()
{
TaskCompletionSource taskCompletionSource = new();