diff --git a/src/Wpf.Ui/Controls/ContentDialog/ContentDialog.cs b/src/Wpf.Ui/Controls/ContentDialog/ContentDialog.cs
index eab8314a7..fa79e785e 100644
--- a/src/Wpf.Ui/Controls/ContentDialog/ContentDialog.cs
+++ b/src/Wpf.Ui/Controls/ContentDialog/ContentDialog.cs
@@ -4,6 +4,7 @@
// All Rights Reserved.
using System.Windows.Controls;
+using System.Windows.Media.Animation;
using Wpf.Ui.Input;
// ReSharper disable once CheckNamespace
@@ -210,6 +211,14 @@ public class ContentDialog : ContentControl
new PropertyMetadata(null)
);
+ /// Identifies the dependency property.
+ public static readonly DependencyProperty AnimationDurationProperty = DependencyProperty.Register(
+ nameof(AnimationDuration),
+ typeof(TimeSpan),
+ typeof(ContentDialog),
+ new PropertyMetadata(TimeSpan.FromMilliseconds(250))
+ );
+
/// Identifies the routed event.
public static readonly RoutedEvent OpenedEvent = EventManager.RegisterRoutedEvent(
nameof(Opened),
@@ -422,6 +431,15 @@ public bool IsFooterVisible
set => SetValue(IsFooterVisibleProperty, value);
}
+ ///
+ /// Gets or sets the duration of the open/close animation.
+ ///
+ public TimeSpan AnimationDuration
+ {
+ get => (TimeSpan)GetValue(AnimationDurationProperty);
+ set => SetValue(AnimationDurationProperty, value);
+ }
+
///
/// Gets command triggered after clicking the button in the template.
///
@@ -520,7 +538,7 @@ public ContentDialog(ContentPresenter? dialogHost)
protected TaskCompletionSource? Tcs { get; set; }
///
- /// Shows the dialog
+ /// Shows the dialog with animation
///
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"WpfAnalyzers.DependencyProperty",
@@ -545,6 +563,12 @@ public async Task ShowAsync(CancellationToken cancellationT
try
{
DialogHost.Content = this;
+
+ Visibility = Visibility.Hidden;
+
+ // Play opening animation
+ await PlayOpenAnimationAsync();
+
result = await Tcs.Task;
return result;
@@ -572,8 +596,187 @@ public virtual void Hide(ContentDialogResult result = ContentDialogResult.None)
if (!closingEventArgs.Cancel)
{
- _ = Tcs?.TrySetResult(result);
+ Dispatcher.BeginInvoke(
+ async () =>
+ {
+ await PlayCloseAnimationAsync();
+ _ = Tcs?.TrySetResult(result);
+ },
+ System.Windows.Threading.DispatcherPriority.Background
+ );
+ }
+ }
+
+ ///
+ /// Plays the opening animation
+ ///
+ protected virtual async Task PlayOpenAnimationAsync()
+ {
+ // Wait for visual tree to be ready
+ await Dispatcher.InvokeAsync(() => { }, System.Windows.Threading.DispatcherPriority.Loaded);
+
+ if (VisualChildrenCount == 0)
+ {
+ return;
}
+
+ var rootElement = GetVisualChild(0) as FrameworkElement;
+ if (rootElement == null)
+ {
+ return;
+ }
+
+ // Find the dialog content (usually a Border or Grid inside the root)
+ FrameworkElement? dialogContent = FindDialogContent(rootElement);
+ if (dialogContent == null)
+ {
+ return;
+ }
+
+ var tcs = new TaskCompletionSource();
+
+ // Set initial state for background (fade only)
+ SetCurrentValue(VisibilityProperty, Visibility.Visible);
+ rootElement.Opacity = 0;
+
+ // Set initial state for dialog content (fade + scale)
+ dialogContent.Opacity = 0;
+ dialogContent.RenderTransform = new ScaleTransform(0.9, 0.9);
+ dialogContent.RenderTransformOrigin = new Point(0.5, 0.5);
+
+ var storyboard = new Storyboard();
+
+ // Background fade in animation
+ var bgFadeAnimation = new DoubleAnimation
+ {
+ From = 0,
+ To = 1,
+ Duration = new Duration(AnimationDuration),
+ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
+ };
+ Storyboard.SetTarget(bgFadeAnimation, rootElement);
+ Storyboard.SetTargetProperty(bgFadeAnimation, new PropertyPath(OpacityProperty));
+ storyboard.Children.Add(bgFadeAnimation);
+
+ // Dialog content fade in animation
+ var contentFadeAnimation = new DoubleAnimation
+ {
+ From = 0,
+ To = 1,
+ Duration = new Duration(AnimationDuration),
+ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
+ };
+ Storyboard.SetTarget(contentFadeAnimation, dialogContent);
+ Storyboard.SetTargetProperty(contentFadeAnimation, new PropertyPath(OpacityProperty));
+ storyboard.Children.Add(contentFadeAnimation);
+
+ // Scale animation for dialog content only
+ var scaleXAnimation = new DoubleAnimation
+ {
+ From = 0.9,
+ To = 1.0,
+ Duration = new Duration(AnimationDuration),
+ EasingFunction = new BackEase { EasingMode = EasingMode.EaseOut, Amplitude = 0.3 }
+ };
+ Storyboard.SetTarget(scaleXAnimation, dialogContent);
+ Storyboard.SetTargetProperty(scaleXAnimation, new PropertyPath("RenderTransform.ScaleX"));
+ storyboard.Children.Add(scaleXAnimation);
+
+ var scaleYAnimation = new DoubleAnimation
+ {
+ From = 0.9,
+ To = 1.0,
+ Duration = new Duration(AnimationDuration),
+ EasingFunction = new BackEase { EasingMode = EasingMode.EaseOut, Amplitude = 0.3 }
+ };
+ Storyboard.SetTarget(scaleYAnimation, dialogContent);
+ Storyboard.SetTargetProperty(scaleYAnimation, new PropertyPath("RenderTransform.ScaleY"));
+ storyboard.Children.Add(scaleYAnimation);
+
+ storyboard.Completed += (s, e) => tcs.SetResult(true);
+ storyboard.Begin();
+
+ _ = await tcs.Task;
+ }
+
+ ///
+ /// Plays the closing animation
+ ///
+ protected virtual async Task PlayCloseAnimationAsync()
+ {
+ if (VisualChildrenCount == 0)
+ {
+ return;
+ }
+
+ var rootElement = GetVisualChild(0) as FrameworkElement;
+ if (rootElement == null)
+ {
+ return;
+ }
+
+ // Find the dialog content
+ FrameworkElement? dialogContent = FindDialogContent(rootElement);
+ if (dialogContent == null)
+ {
+ return;
+ }
+
+ var tcs = new TaskCompletionSource();
+
+ var storyboard = new Storyboard();
+
+ // Background fade out animation
+ var bgFadeAnimation = new DoubleAnimation
+ {
+ From = 1,
+ To = 0,
+ Duration = new Duration(AnimationDuration),
+ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseIn }
+ };
+ Storyboard.SetTarget(bgFadeAnimation, rootElement);
+ Storyboard.SetTargetProperty(bgFadeAnimation, new PropertyPath(OpacityProperty));
+ storyboard.Children.Add(bgFadeAnimation);
+
+ // Dialog content fade out animation
+ var contentFadeAnimation = new DoubleAnimation
+ {
+ From = 1,
+ To = 0,
+ Duration = new Duration(AnimationDuration),
+ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseIn }
+ };
+ Storyboard.SetTarget(contentFadeAnimation, dialogContent);
+ Storyboard.SetTargetProperty(contentFadeAnimation, new PropertyPath(OpacityProperty));
+ storyboard.Children.Add(contentFadeAnimation);
+
+ // Scale animation for dialog content only
+ var scaleXAnimation = new DoubleAnimation
+ {
+ From = 1.0,
+ To = 0.9,
+ Duration = new Duration(AnimationDuration),
+ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseIn }
+ };
+ Storyboard.SetTarget(scaleXAnimation, dialogContent);
+ Storyboard.SetTargetProperty(scaleXAnimation, new PropertyPath("RenderTransform.ScaleX"));
+ storyboard.Children.Add(scaleXAnimation);
+
+ var scaleYAnimation = new DoubleAnimation
+ {
+ From = 1.0,
+ To = 0.9,
+ Duration = new Duration(AnimationDuration),
+ EasingFunction = new CubicEase { EasingMode = EasingMode.EaseIn }
+ };
+ Storyboard.SetTarget(scaleYAnimation, dialogContent);
+ Storyboard.SetTargetProperty(scaleYAnimation, new PropertyPath("RenderTransform.ScaleY"));
+ storyboard.Children.Add(scaleYAnimation);
+
+ storyboard.Completed += (s, e) => tcs.SetResult(true);
+ storyboard.Begin();
+
+ _ = await tcs.Task;
}
///
@@ -696,4 +899,49 @@ private void ResizeHeight(UIElement element)
/*Debug.WriteLine($"DEBUG | {GetType()} | WARNING | DialogWidth > DialogMaxWidth after resizing height!");*/
}
}
-}
+
+ ///
+ /// Finds the dialog content element (the actual dialog box, not the background overlay)
+ ///
+ private FrameworkElement? FindDialogContent(FrameworkElement rootElement)
+ {
+ // Try to find element with specific name or type
+ // Common names: "DialogSpace", "PART_DialogSpace", "ContentDialog", etc.
+ if (rootElement is Panel panel)
+ {
+ foreach (UIElement child in panel.Children)
+ {
+ if (child is FrameworkElement fe)
+ {
+ // Look for the dialog container (usually has a name containing "Dialog" or "Content")
+ if (fe.Name?.Contains("Dialog") == true ||
+ fe.Name?.Contains("Content") == true ||
+ fe.Name?.Contains("PART") == true)
+ {
+ return fe;
+ }
+
+ // If it's a Border, ContentControl, or similar container, it's likely the dialog
+ if (fe is Border || fe is ContentControl)
+ {
+ return fe;
+ }
+ }
+ }
+
+ // If no specific element found, return the last child (usually the dialog is on top)
+ if (panel.Children.Count > 0 && panel.Children[panel.Children.Count - 1] is FrameworkElement lastChild)
+ {
+ return lastChild;
+ }
+ }
+
+ // Fallback: if root is Grid, try to get the child at index 1 (background usually at 0)
+ if (rootElement is Grid grid && grid.Children.Count > 1)
+ {
+ return grid.Children[1] as FrameworkElement;
+ }
+
+ return null;
+ }
+}
\ No newline at end of file