Add Static API for MessageBox

This commit is contained in:
chinboy
2025-12-23 14:34:58 +08:00
parent 08c2bf4f1f
commit 8aaadfec4c
11 changed files with 332 additions and 115 deletions
@@ -1,6 +1,5 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace AtomUI.Desktop.Controls;
@@ -36,7 +35,7 @@ public partial class Dialog
return result;
}
public static Task<object?>? ShowDialogAsync<TView, TViewModel>(TViewModel? dataContext, DialogOptions? options = null, TopLevel? topLevel = null)
public static async Task ShowDialogAsync<TView, TViewModel>(TViewModel? dataContext, DialogOptions? options = null, TopLevel? topLevel = null)
where TView : Control, new()
{
var dialogManager = FindDialogManager(topLevel);
@@ -47,10 +46,10 @@ public partial class Dialog
}
dialog.Closed += (_, _) => dialogManager.Children.Remove(dialog);
dialogManager.Children.Add(dialog);
return dialog.OpenAsync();
await Dispatcher.UIThread.InvokeAsync(async () => await dialog.OpenAsync());
}
public static Task<object?>? ShowDialogAsync(Control content, object? dataContext = null, DialogOptions? options = null, TopLevel? topLevel = null)
public static async Task ShowDialogAsync(Control content, object? dataContext = null, DialogOptions? options = null, TopLevel? topLevel = null)
{
var dialogManager = FindDialogManager(topLevel);
var dialog = CreateDialog(content, dataContext, options);
@@ -60,7 +59,7 @@ public partial class Dialog
}
dialog.Closed += (_, _) => dialogManager.Children.Remove(dialog);
dialogManager.Children.Add(dialog);
return dialog.OpenAsync();
await Dispatcher.UIThread.InvokeAsync(async () => await dialog.OpenAsync());
}
private static Dialog CreateDialog(Control content, object? dataContext, DialogOptions? options)
@@ -99,7 +98,7 @@ public partial class Dialog
private static GlobalDialogManager FindDialogManager(TopLevel? topLevel)
{
var toplevel = topLevel ?? GetMainWindow();
var toplevel = topLevel ?? Window.GetMainWindow();
var dialogManager = toplevel.FindDescendantOfType<GlobalDialogManager>();
if (dialogManager == null)
{
@@ -107,14 +106,4 @@ public partial class Dialog
}
return dialogManager;
}
private static Window? GetMainWindow()
{
var lifetime = Application.Current?.ApplicationLifetime;
if (lifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
{
return desktopLifetime.MainWindow as Window;
}
return null;
}
}
+18 -45
View File
@@ -4,7 +4,6 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Disposables;
using System.Reactive.Disposables.Fluent;
using System.Reactive.Linq;
using AtomUI.Controls;
using AtomUI.Data;
using AtomUI.Desktop.Controls.DialogPositioning;
@@ -275,12 +274,6 @@ public partial class Dialog : TemplatedControl,
set => SetValue(DialogHostTypeProperty, value);
}
public Task<object?>? ResultTask
{
get;
private set;
}
public DialogStandardButtons StandardButtons
{
get => GetValue(StandardButtonsProperty);
@@ -401,7 +394,6 @@ public partial class Dialog : TemplatedControl,
private bool _ignoreIsOpenChanged;
private DialogOpenState? _openState;
private Action<IDialogHost?>? _dialogHostChangedHandler;
private IDisposable? _modalSubscription;
private CancellationTokenSource? _frameCancellationTokenSource;
private bool _startupLocationCalculated;
private bool _opening;
@@ -446,16 +438,16 @@ public partial class Dialog : TemplatedControl,
var token = _frameCancellationTokenSource.Token;
var frame = new DispatcherFrame();
token.Register(() => frame.Continue = false);
var resultTask = OpenAsync();
var _ = OpenAsync();
Dispatcher.UIThread.PushFrame(frame);
return resultTask?.Result;
return Result;
}
public Task<object?>? OpenAsync()
public async Task OpenAsync()
{
if (_openState != null || _opening)
{
return null;
return;
}
_opening = true;
@@ -466,7 +458,7 @@ public partial class Dialog : TemplatedControl,
IDialogHost? dialogHost = null;
CompositeDisposable relayBindingDisposables = new CompositeDisposable();
DialogHost? windowDialogHost = null;
OverlayDialogHost? overlayDialogHost = null;
if (DialogHostType == DialogHostType.Window)
{
windowDialogHost = CreateDialogHost(topLevel, this);
@@ -479,7 +471,7 @@ public partial class Dialog : TemplatedControl,
var dialogLayer = DialogLayer.GetDialogLayer(placementTarget);
if (dialogLayer != null)
{
overlayDialogHost = CreateOverlayDialogHost(dialogLayer, this);
var overlayDialogHost = CreateOverlayDialogHost(dialogLayer, this);
OverlayInputPassThroughElement = overlayDialogHost;
RelayOverlayDialogBindings(relayBindingDisposables, overlayDialogHost);
overlayDialogHost.CustomButtons.AddRange(CustomButtons);
@@ -535,6 +527,12 @@ public partial class Dialog : TemplatedControl,
}
var inputManager = AvaloniaLocator.Current.GetService<IInputManager>();
inputManager?.Process.Subscribe(ListenForNonClientClick).DisposeWith(handlerCleanup);
TaskCompletionSource? modalTsc = null;
if (IsModal)
{
modalTsc = new TaskCompletionSource();
}
var cleanupPopup = Disposable.Create((dialogHost, handlerCleanup), state =>
{
@@ -554,6 +552,7 @@ public partial class Dialog : TemplatedControl,
dialogAwareDataContext.NotifyClosed();
}
Closed?.Invoke(this, EventArgs.Empty);
modalTsc?.TrySetResult();
});
if (DialogHostType == DialogHostType.Window)
{
@@ -596,7 +595,7 @@ public partial class Dialog : TemplatedControl,
{
if (topLevel is Window windowTopLevel)
{
windowDialog.ShowDialog(windowTopLevel);
await windowDialog.ShowDialog(windowTopLevel);
}
}
else
@@ -609,32 +608,6 @@ public partial class Dialog : TemplatedControl,
dialogHost.Show();
}
if (IsModal)
{
var tcs = new TaskCompletionSource<object?>();
var disposables = new CompositeDisposable(
[
Observable.FromEventPattern(
x => Closed += x,
x => Closed -= x)
.Take(1)
.Subscribe(_ =>
{
_modalSubscription?.Dispose();
}),
Disposable.Create(() =>
{
_modalSubscription = null;
// owner!.Activate();
tcs.SetResult(Result);
})
]);
_modalSubscription = disposables;
ResultTask = tcs.Task;
}
using (BeginIgnoringIsOpen())
{
SetCurrentValue(IsOpenProperty, true);
@@ -642,8 +615,10 @@ public partial class Dialog : TemplatedControl,
Opened?.Invoke(this, EventArgs.Empty);
_opening = false;
_dialogHostChangedHandler?.Invoke(Host);
return ResultTask;
if (modalTsc != null)
{
await modalTsc.Task;
}
}
private protected virtual DialogHost CreateDialogHost(TopLevel topLevel, Dialog dialog)
@@ -761,8 +736,6 @@ public partial class Dialog : TemplatedControl,
_openState = null;
_dialogHostChangedHandler?.Invoke(null);
_modalSubscription?.Dispose();
_modalSubscription = null;
using (BeginIgnoringIsOpen())
{
SetCurrentValue(IsOpenProperty, false);
@@ -1,4 +1,3 @@
using AtomUI.Controls;
using Avalonia.Controls;
namespace AtomUI.Desktop.Controls;
@@ -7,11 +6,14 @@ public record DialogOptions
{
public string? Title { get; init; }
public PathIcon? TitleIcon { get; init; }
public bool IsLightDismissEnabled { get; init; } = false;
/// <summary>
/// 次选项仅 Overlay Host 类型的弹窗有效
/// </summary>
public bool IsLightDismissEnabled { get; init; }
public bool IsModal { get; init; } = true;
public bool IsResizable { get; init; } = false;
public bool IsResizable { get; init; }
public bool IsClosable { get; init; } = true;
public bool IsMaximizable { get; init; } = false;
public bool IsMaximizable { get; init; }
public double MaxHeight { get; init; } = double.PositiveInfinity;
public double MaxWidth { get; init; } = double.PositiveInfinity;
@@ -21,11 +23,11 @@ public record DialogOptions
public double Height { get; init; } = double.NaN;
/// <summary>
/// 仅仅对 Window Host 类型的弹窗有效
/// 次选项仅 Window Host 类型的弹窗有效
/// </summary>
public bool IsMinimizable { get; init; } = true;
public bool IsDragMovable { get; init; } = false;
public bool IsDragMovable { get; init; }
public bool IsFooterVisible { get; init; } = true;
public Control? PlacementTarget { get; init; }
public Dimension? HorizontalOffset { get; init; }
@@ -11,6 +11,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Metadata;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace AtomUI.Desktop.Controls;
@@ -38,7 +39,7 @@ public class MessageBox : TemplatedControl,
public static readonly StyledProperty<IDataTemplate?> ContentTemplateProperty = Dialog.ContentTemplateProperty.AddOwner<MessageBox>();
public static readonly StyledProperty<MessageBoxStyle> StyleProperty =
AvaloniaProperty.Register<MessageBox, MessageBoxStyle>(nameof (Style));
AvaloniaProperty.Register<MessageBox, MessageBoxStyle>(nameof (Style), MessageBoxStyle.Information);
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<MessageBox>();
@@ -250,37 +251,145 @@ public class MessageBox : TemplatedControl,
public MessageBox()
{
CustomButtons.CollectionChanged += new NotifyCollectionChangedEventHandler(HandleCustomButtonsChanged);
CustomButtons.CollectionChanged += HandleCustomButtonsChanged;
}
public object? Open()
{
if (_dialog == null)
{
return null;
}
Debug.Assert(_dialog != null);
return _dialog.Open();
}
public Task<object?>? OpenAsync()
public async Task OpenAsync()
{
if (_dialog == null)
{
return null;
}
return _dialog.OpenAsync();
Debug.Assert(_dialog != null);
await _dialog.OpenAsync();
}
public void Cancel()
{
_dialog?.Reject();
Debug.Assert(_dialog != null);
_dialog.Reject();
}
public void Confirm()
{
_dialog?.Accept();
Debug.Assert(_dialog != null);
_dialog.Accept();
}
#region 静态 API
public static object? ShowMessageBox<TView, TViewModel>(TViewModel? dataContext, MessageBoxOptions? options = null,
TopLevel? topLevel = null)
where TView : Control, new()
{
var dialogManager = FindDialogManager(topLevel);
var messageBox = CreateMessageBox(new TView(), dataContext, options);
if (options?.HostType == DialogHostType.Window)
{
messageBox.PlacementTarget = dialogManager;
}
dialogManager.Children.Add(messageBox);
var result = messageBox.Open();
dialogManager.Children.Remove(messageBox);
return result;
}
public static object? ShowMessageBox(Control content, object? dataContext = null, MessageBoxOptions? options = null, TopLevel? topLevel = null)
{
var dialogManager = FindDialogManager(topLevel);
var messageBox = CreateMessageBox(content, dataContext, options);
if (options?.HostType == DialogHostType.Window)
{
messageBox.PlacementTarget = dialogManager;
}
dialogManager.Children.Add(messageBox);
var result = messageBox.Open();
dialogManager.Children.Remove(messageBox);
return result;
}
public static async Task<object?> ShowMessageBoxAsync<TView, TViewModel>(TViewModel? dataContext, MessageBoxOptions? options = null, TopLevel? topLevel = null)
where TView : Control, new()
{
var dialogManager = FindDialogManager(topLevel);
var messageBox = CreateMessageBox(new TView(), dataContext, options);
if (options?.HostType == DialogHostType.Window)
{
messageBox.PlacementTarget = dialogManager;
}
messageBox.Closed += (_, _) => dialogManager.Children.Remove(messageBox);
dialogManager.Children.Add(messageBox);
await Dispatcher.UIThread.InvokeAsync(async () => await messageBox.OpenAsync());
return messageBox.Result;
}
public static async Task<object?> ShowMessageAsync(Control content, object? dataContext = null, MessageBoxOptions? options = null, TopLevel? topLevel = null)
{
var dialogManager = FindDialogManager(topLevel);
var messageBox = CreateMessageBox(content, dataContext, options);
if (options?.HostType == DialogHostType.Window)
{
messageBox.PlacementTarget = dialogManager;
}
messageBox.Closed += (_, _) => dialogManager.Children.Remove(messageBox);
dialogManager.Children.Add(messageBox);
var tsc = new TaskCompletionSource<object?>();
await Dispatcher.UIThread.InvokeAsync(async () =>
{
await messageBox.OpenAsync();
tsc.SetResult(messageBox.Result);
});
await tsc.Task;
return messageBox.Result;
}
private static MessageBox CreateMessageBox(Control content, object? dataContext, MessageBoxOptions? options)
{
var messageBox = new MessageBox()
{
Title = options?.Title,
IsLightDismissEnabled = options?.IsLightDismissEnabled ?? false,
IsModal = options?.IsModal ?? true,
IsDragMovable = options?.IsDragMovable ?? false,
Style = options?.Style ?? MessageBoxStyle.Information,
PlacementTarget = options?.PlacementTarget,
HorizontalOffset = options?.HorizontalOffset,
VerticalOffset = options?.VerticalOffset,
HostType = options?.HostType ?? DialogHostType.Overlay,
IsCenterOnStartup = options?.IsCenterOnStartup ?? true,
Content = content,
DataContext = dataContext,
Width = options?.Width ?? double.NaN,
Height = options?.Height ?? double.NaN,
MinWidth = options?.MinWidth ?? 0d,
MinHeight = options?.MinHeight ?? 0d,
MaxWidth = options?.MaxWidth ?? double.PositiveInfinity,
MaxHeight = options?.MaxHeight ?? double.PositiveInfinity,
IsConfirmLoading = options?.IsConfirmLoading ?? false,
IsLoading = options?.IsLoading ?? false,
};
if (options?.Icon != null)
{
messageBox.Icon = options.Icon;
}
return messageBox;
}
private static GlobalDialogManager FindDialogManager(TopLevel? topLevel)
{
var toplevel = topLevel ?? Window.GetMainWindow();
var dialogManager = toplevel.FindDescendantOfType<GlobalDialogManager>();
if (dialogManager == null)
{
throw new InvalidOperationException("The DialogManager was not found in TopLevel; you may not be using the atom:Window class.");
}
return dialogManager;
}
#endregion
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
@@ -306,13 +415,13 @@ public class MessageBox : TemplatedControl,
base.OnApplyTemplate(e);
if (_dialog != null)
{
_dialog.Opened -= HandleDialogOpened;
_dialog.Closed -= HandleDialogClosed;
_dialog.Rejected -= HandleDialogCancelled;
_dialog.Accepted -= HandleDialogConfirmed;
_dialog.Finished -= HandleDialogFinished;
_dialog.Opened -= HandleDialogOpened;
_dialog.Closed -= HandleDialogClosed;
_dialog.Rejected -= HandleDialogCancelled;
_dialog.Accepted -= HandleDialogConfirmed;
_dialog.Finished -= HandleDialogFinished;
}
_dialog = e.NameScope.Find<Dialog>(MessageBoxThemeConstants.DialogPart);
_dialog = e.NameScope.Find<MessageBoxDialog>(MessageBoxThemeConstants.DialogPart);
if (_dialog != null)
{
_dialog.Opened += HandleDialogOpened;
@@ -450,4 +559,4 @@ public class MessageBox : TemplatedControl,
}
}
}
}
}
@@ -1,5 +1,4 @@
using System.Reactive.Disposables;
using AtomUI.Controls;
using AtomUI.Data;
using AtomUI.Desktop.Controls.Primitives;
using Avalonia;
@@ -0,0 +1,31 @@
using Avalonia.Controls;
namespace AtomUI.Desktop.Controls;
public record MessageBoxOptions
{
public DialogHostType HostType { get; init; } = DialogHostType.Overlay;
public string? Title { get; init; }
public PathIcon? Icon { get; init; }
public MessageBoxStyle Style { get; init; } = MessageBoxStyle.Information;
public bool IsModal { get; init; } = true;
public bool IsDragMovable { get; init; }
public Control? PlacementTarget { get; init; }
public Dimension? HorizontalOffset { get; init; }
public Dimension? VerticalOffset { get; init; }
public bool IsCenterOnStartup { get; init; } = true;
/// <summary>
/// 次选项仅 Overlay Host 类型的弹窗有效
/// </summary>
public bool IsLightDismissEnabled { get; init; }
public bool IsLoading { get; init; }
public bool IsConfirmLoading { get; init; }
public double MaxHeight { get; init; } = double.PositiveInfinity;
public double MaxWidth { get; init; } = double.PositiveInfinity;
public double MinHeight { get; init; } = 0d;
public double MinWidth { get; init; } = 0d;
public double Width { get; init; } = double.NaN;
public double Height { get; init; } = double.NaN;
}
@@ -61,19 +61,18 @@
</ControlTemplate>
</Setter>
<Style Selector="^ /template/ atom|IconPresenter#StyleIconPresenter">
<Setter Property="Width" Value="{DynamicResource {x:Static atom:MessageBoxTokenKey.StyleIconSize}}" />
<Setter Property="Height" Value="{DynamicResource {x:Static atom:MessageBoxTokenKey.StyleIconSize}}" />
</Style>
<Style Selector="^[Style=Confirm]">
<Style Selector="^ /template/ atom|IconPresenter#StyleIconPresenter">
<Setter Property="IconBrush" Value="{DynamicResource {x:Static atom:SharedTokenKey.ColorWarning}}" />
</Style>
</Style>
<Style Selector="^ /template/ atom|IconPresenter#StyleIconPresenter">
<Setter Property="Width" Value="{DynamicResource {x:Static atom:MessageBoxTokenKey.StyleIconSize}}" />
<Setter Property="Height" Value="{DynamicResource {x:Static atom:MessageBoxTokenKey.StyleIconSize}}" />
</Style>
<Style Selector="^[Style=Information]">
<Style Selector="^ /template/ atom|IconPresenter#StyleIconPresenter">
<Setter Property="IconBrush" Value="{DynamicResource {x:Static atom:SharedTokenKey.ColorInfo}}" />
@@ -339,6 +339,16 @@ public class Window : AvaloniaWindow,
ConfigureCustomResizerVisible();
}
public static Window? GetMainWindow()
{
var lifetime = Application.Current?.ApplicationLifetime;
if (lifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
{
return desktopLifetime.MainWindow as Window;
}
return null;
}
protected override void OnClosed(EventArgs e)
{
Dispose();
@@ -2,6 +2,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
namespace AtomUI.Desktop.Controls;