using System; using System.Diagnostics.CodeAnalysis; using System.Reflection; using AsyncAwaitBestPractices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Threading; using FluentAvalonia.UI.Controls; using StabilityMatrix.Avalonia.ViewModels.Base; namespace StabilityMatrix.Avalonia.Controls; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] public class BetterContentDialog : ContentDialog { #region Reflection Shenanigans for setting content dialog result [NotNull] protected static readonly FieldInfo? ResultField = typeof(ContentDialog).GetField( "_result", BindingFlags.Instance | BindingFlags.NonPublic ); protected ContentDialogResult Result { get => (ContentDialogResult)ResultField.GetValue(this)!; set => ResultField.SetValue(this, value); } [NotNull] protected static readonly MethodInfo? HideCoreMethod = typeof(ContentDialog).GetMethod( "HideCore", BindingFlags.Instance | BindingFlags.NonPublic ); protected void HideCore() { HideCoreMethod.Invoke(this, null); } // Also get button properties to hide on command execution change [NotNull] protected static readonly FieldInfo? PrimaryButtonField = typeof(ContentDialog).GetField( "_primaryButton", BindingFlags.Instance | BindingFlags.NonPublic ); protected Button? PrimaryButton { get => (Button?)PrimaryButtonField.GetValue(this)!; set => PrimaryButtonField.SetValue(this, value); } [NotNull] protected static readonly FieldInfo? SecondaryButtonField = typeof(ContentDialog).GetField( "_secondaryButton", BindingFlags.Instance | BindingFlags.NonPublic ); protected Button? SecondaryButton { get => (Button?)SecondaryButtonField.GetValue(this)!; set => SecondaryButtonField.SetValue(this, value); } [NotNull] protected static readonly FieldInfo? CloseButtonField = typeof(ContentDialog).GetField( "_closeButton", BindingFlags.Instance | BindingFlags.NonPublic ); protected Button? CloseButton { get => (Button?)CloseButtonField.GetValue(this)!; set => CloseButtonField.SetValue(this, value); } static BetterContentDialog() { if (ResultField is null) { throw new NullReferenceException("ResultField was not resolved"); } if (HideCoreMethod is null) { throw new NullReferenceException("HideCoreMethod was not resolved"); } if (PrimaryButtonField is null || SecondaryButtonField is null || CloseButtonField is null) { throw new NullReferenceException("Button fields were not resolved"); } } #endregion private FABorder? backgroundPart; protected override Type StyleKeyOverride { get; } = typeof(ContentDialog); public static readonly StyledProperty IsFooterVisibleProperty = AvaloniaProperty.Register< BetterContentDialog, bool >("IsFooterVisible", true); public bool IsFooterVisible { get => GetValue(IsFooterVisibleProperty); set => SetValue(IsFooterVisibleProperty, value); } public static readonly StyledProperty ContentVerticalScrollBarVisibilityProperty = AvaloniaProperty.Register( "ContentScrollBarVisibility", ScrollBarVisibility.Auto ); public ScrollBarVisibility ContentVerticalScrollBarVisibility { get => GetValue(ContentVerticalScrollBarVisibilityProperty); set => SetValue(ContentVerticalScrollBarVisibilityProperty, value); } public static readonly StyledProperty MinDialogWidthProperty = AvaloniaProperty.Register("MinDialogWidth"); public double MinDialogWidth { get => GetValue(MinDialogWidthProperty); set => SetValue(MinDialogWidthProperty, value); } public static readonly StyledProperty MaxDialogWidthProperty = AvaloniaProperty.Register("MaxDialogWidth"); public double MaxDialogWidth { get => GetValue(MaxDialogWidthProperty); set => SetValue(MaxDialogWidthProperty, value); } public static readonly StyledProperty MaxDialogHeightProperty = AvaloniaProperty.Register("MaxDialogHeight"); public double MaxDialogHeight { get => GetValue(MaxDialogHeightProperty); set => SetValue(MaxDialogHeightProperty, value); } public static readonly StyledProperty ContentMarginProperty = AvaloniaProperty.Register("ContentMargin"); public Thickness ContentMargin { get => GetValue(ContentMarginProperty); set => SetValue(ContentMarginProperty, value); } public static readonly StyledProperty CloseOnClickOutsideProperty = AvaloniaProperty.Register("CloseOnClickOutside"); /// /// Whether to close the dialog when clicking outside of it (on the blurred background) /// public bool CloseOnClickOutside { get => GetValue(CloseOnClickOutsideProperty); set => SetValue(CloseOnClickOutsideProperty, value); } public BetterContentDialog() { AddHandler(LoadedEvent, OnLoaded); } /// protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); if (CloseOnClickOutside) { if (e.Source is Popup || backgroundPart is null) return; var point = e.GetPosition(this); if ( !backgroundPart.Bounds.Contains(point) && (Content as Control)?.DataContext is ContentDialogViewModelBase vm ) { vm.OnCloseButtonClick(); } } } private void TryBindButtons() { if ((Content as Control)?.DataContext is ContentDialogViewModelBase viewModel) { viewModel.PrimaryButtonClick += OnDialogButtonClick; viewModel.SecondaryButtonClick += OnDialogButtonClick; viewModel.CloseButtonClick += OnDialogButtonClick; } else if (Content is ContentDialogViewModelBase viewModelDirect) { viewModelDirect.PrimaryButtonClick += OnDialogButtonClick; viewModelDirect.SecondaryButtonClick += OnDialogButtonClick; viewModelDirect.CloseButtonClick += OnDialogButtonClick; } else if ( (Content as Control)?.DataContext is ContentDialogProgressViewModelBase progressViewModel ) { progressViewModel.PrimaryButtonClick += OnDialogButtonClick; progressViewModel.SecondaryButtonClick += OnDialogButtonClick; progressViewModel.CloseButtonClick += OnDialogButtonClick; } // If commands provided, bind OnCanExecuteChanged to hide buttons // otherwise link visibility to IsEnabled if (PrimaryButton is not null) { if (PrimaryButtonCommand is not null) { PrimaryButtonCommand.CanExecuteChanged += (_, _) => PrimaryButton.IsEnabled = PrimaryButtonCommand.CanExecute(null); // Also set initial state PrimaryButton.IsEnabled = PrimaryButtonCommand.CanExecute(null); } else { PrimaryButton.IsVisible = IsPrimaryButtonEnabled && !string.IsNullOrEmpty(PrimaryButtonText); } } if (SecondaryButton is not null) { if (SecondaryButtonCommand is not null) { SecondaryButtonCommand.CanExecuteChanged += (_, _) => SecondaryButton.IsEnabled = SecondaryButtonCommand.CanExecute(null); // Also set initial state SecondaryButton.IsEnabled = SecondaryButtonCommand.CanExecute(null); } else { SecondaryButton.IsVisible = IsSecondaryButtonEnabled && !string.IsNullOrEmpty(SecondaryButtonText); } } if (CloseButton is not null) { if (CloseButtonCommand is not null) { CloseButtonCommand.CanExecuteChanged += (_, _) => CloseButton.IsEnabled = CloseButtonCommand.CanExecute(null); // Also set initial state CloseButton.IsEnabled = CloseButtonCommand.CanExecute(null); } } } protected void OnDialogButtonClick(object? sender, ContentDialogResult e) { Dispatcher.UIThread.Post(() => { Result = e; HideCore(); }); } protected override void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); TryBindButtons(); } /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); backgroundPart = e.NameScope.Find("BackgroundElement"); if (backgroundPart is not null) { backgroundPart.Margin = ContentMargin; } } private void OnLoaded(object? sender, RoutedEventArgs? e) { TryBindButtons(); // Find the named grid // https://github.com/amwx/FluentAvalonia/blob/master/src/FluentAvalonia/Styling/ // ControlThemes/FAControls/ContentDialogStyles.axaml#L96 var border = VisualChildren[0] as Border; var panel = border?.Child as Panel; var faBorder = panel?.Children[0] as FABorder; // Set dialog bounds if (MaxDialogWidth > 0) { faBorder!.MaxWidth = MaxDialogWidth; } if (MinDialogWidth > 0) { faBorder!.MinWidth = MinDialogWidth; } if (MaxDialogHeight > 0) { faBorder!.MaxHeight = MaxDialogHeight; } var border2 = faBorder?.Child as Border; // Named Grid 'DialogSpace' if (border2?.Child is not Grid dialogSpaceGrid) throw new InvalidOperationException("Could not find DialogSpace grid"); var scrollViewer = dialogSpaceGrid.Children[0] as ScrollViewer; var actualBorder = dialogSpaceGrid.Children[1] as Border; // Get the parent border, which is what we want to hide if (scrollViewer is null || actualBorder is null) { throw new InvalidOperationException("Could not find parent border"); } var subBorder = scrollViewer.Content as Border; var subGrid = subBorder?.Child as Grid; if (subGrid is null) throw new InvalidOperationException("Could not find sub grid"); var contentControlTitle = subGrid.Children[0] as ContentControl; // Hide title if empty if (Title is null or string { Length: 0 }) { contentControlTitle!.IsVisible = false; } // Set footer and scrollbar visibility states actualBorder.IsVisible = IsFooterVisible; scrollViewer.VerticalScrollBarVisibility = ContentVerticalScrollBarVisibility; // Also call the vm's OnLoad if (Content is Control { DataContext: ViewModelBase viewModel }) { viewModel.OnLoaded(); Dispatcher.UIThread.InvokeAsync(viewModel.OnLoadedAsync).SafeFireAndForget(); } } }