MahApps / MahApps.Metro

A framework that allows developers to cobble together a better UI for their own WPF applications with minimal effort.
https://mahapps.com
MIT License
9.3k stars 2.45k forks source link

Dialogs crash #4014

Closed qusma closed 2 years ago

qusma commented 3 years ago

Describe the bug

I have a standard MVVM setup for dialogs. XAML:


<controls:MetroWindow [...]
        xmlns:Dialog="clr-namespace:MahApps.Metro.Controls.Dialogs;assembly=MahApps.Metro"
        Dialog:DialogParticipation.Register="{Binding}">

Then in the window constructor I pass the DialogCoordinator.Instance:

InitializeComponent();
ViewModel = new MainViewModel(DialogCoordinator.Instance, [...]);
this.DataContext = ViewModel;

Attempting to create a dialog from the ViewModel, like this:

        public async Task test()
        {
            await DialogService.ShowMessageAsync(this, "test", "test");
        }

Results in the overlay coming up (the window gets grayed out), no dialog appearing, and a few moments later an exception thrown:

System.Threading.Tasks.TaskCanceledException: 'A task was canceled.'

This exception was originally thrown at this call stack: System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(System.Threading.Tasks.Task) System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task) System.Runtime.CompilerServices.TaskAwaiter.GetResult() System.Windows.Threading.DispatcherOperation.Wait(System.TimeSpan) System.Windows.Threading.Dispatcher.InvokeImpl(System.Windows.Threading.DispatcherOperation, System.Threading.CancellationToken, System.TimeSpan) System.Windows.Threading.Dispatcher.Invoke(System.Func, System.Windows.Threading.DispatcherPriority, System.Threading.CancellationToken, System.TimeSpan) System.Windows.Threading.Dispatcher.Invoke(System.Func) MahApps.Metro.Controls.Dialogs.DialogManager.ShowMessageAsync.AnonymousMethod0(System.Threading.Tasks.Task) System.Threading.Tasks.ContinuationResultTaskFromTask.InnerInvoke() System.Threading.Tasks.Task..cctor.AnonymousMethod274_0(object) ... [Call Stack Truncated]

Same thing happens trying to create the dialog directly from the window like this:

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
           await this.ShowMessageAsync("test", "test");
        }

Environment

MahApps.Metro version: v2.4.3
Windows build number: Win10 19042
Visual Studio: 2019 16.8.2
Target Framework: .Net Core 3.1

Screenshots

punker76 commented 3 years ago

@qusma It would be help if you can create a simple app which reproduces this issue. Thx

qusma commented 3 years ago

Here you go: https://filebin.net/ewjesvzqjx7dpn6k

I managed to narrow it down a bit, the issue seems to be related to showing a splash screen first before the main window. (It used to work in the past on .NET Framework btw.)

argentmize commented 3 years ago

Hello everyone! I got the same error:

System.InvalidOperationException HResult=0x80131509 Message = Context is not registered. Consider using DialogParticipation.Register in XAML to bind in the DataContext. Source = MahApps.Metro Трассировка стека: at MahApps.Metro.Controls.Dialogs.DialogCoordinator.GetMetroWindow(Object context) at MahApps.Metro.Controls.Dialogs.DialogCoordinator.ShowMessageAsync(Object context, String title, String message, MessageDialogStyle style, MetroDialogSettings settings) at XXX.ViewModels.XXXViewModel.d__91.MoveNext() in \XXX\ViewModels\XXXViewModel.cs:line 383

View.xaml.cs & DataContext 1 XAML 2 Method 3 There are no errors in this place, the dialog is displayed 4 But if Icall the method here, an exception appears 5 Exception 6

The specified View is created from the ViewModel of another window. If I don't call the method from the constructor, the dialog is displayed correctly. It is not clear why the DataContext is lost if the same method is called from the constructor.

I also checked the assumption of @qusma -- SplashScreen, which I have added to the application resources, does not affect the operation of the dialog box

Environment

timunie commented 3 years ago

Hi @argentmize

Maybe you cannot call this in the constructor because the VM is not registered yet.

I think this is going on:

  1. You set your VM as the new DataContext
  2. Your VM gets created
  3. OPTIONAL: You throw an error and want to show the dialog
  4. The DataContext is created and the Bindings are set now

As you see step 3 should have been done after step 4.

I see two solutions:

  1. Set a flag if the constructor was successful and if not raise the dialog afterwards
  2. don't use the DialogCoordinator here and instead search for the acitve window to show it (I know this breaks the MVVM pattern a bit, but I am using this in such situations)

Solution 2 explained:

var window = App.Current.Windows.OfType<MetroWindow>().FirstOrDefault(x => x.IsActive);
if (window is not null) 
{
    await window.ShowMessageAsync("Error", "My Message");
}
else
{
    MessageBox.Show("FALLBACK DIALOG IF THE ABOVE FAILED");
}

Happy coding Tim

argentmize commented 3 years ago

Hi @timunie!

Thank you for your advice!

I tried the second solution - the dialog box is shown, but only on the previous window. That is, I have a main window (1) with a button that opens the next window (2). Depending on the choice in the second window, another window is created (3), where the dialog box should be displayed. But with your option, the following sequence turns out: there is no dialog, an empty window (3) is created (because the bindings did not work). I close the window and see an error dialog 🤣 I decided to look further for now, your option will work in another project, thx.

I think something is really going on with the initialization order, but I have no guesses and no relevant knowledge. I expected that if the documentation is so sparse, then everything should be simple. I was wrong 😀

Also I tried another solution.

Good luck Augustyn

timunie commented 3 years ago

Hi @argentmize ,

well I think you need some way to detect the window you want to show the message. My assumption was that you may want to show it on the active window. If not, you may test for the Title-Property or the Tag-Property or you replace .OfType<MetroWindow>() with .OfType<MyDerivedWindowClass>().

Happy coding Tim

argentmize commented 3 years ago

Hi @timunie

Understood, thanks. So far, I've chosen to skip the DialogCoordinator and just pass the window object reference through the VM constructor (without MVVM - https://mahapps.com/docs/dialogs/message-dialog)

public partial class XXXView : MetroWindow
{
    public XXXView(int taskId)
    {
        InitializeComponent();
        DataContext = new XXXViewModel(taskId, this);
    }
}
//...
public async void ShowDialog(string title, string text)
{
    await _current.ShowMessageAsync(title, text);
}

public XXXViewModel(int taskId, MetroWindow curr)
{
    _current = curr; // MetroWindow
    try { }
    catch { ShowDialog(Title, Text); }
}

This works as planned. Thanks for the help

Good luck Augustyn