microsoft / microsoft-ui-xaml

WinUI: a modern UI framework with a rich set of controls and styles to build dynamic and high-performing Windows applications.
MIT License
6.38k stars 683 forks source link

Unable to display ContentDialog. This element is already associated with a XamlRoot #4990

Closed nlogozzo closed 2 years ago

nlogozzo commented 3 years ago

Describe the bug

I have a custom MVVM framework with a ContentDialogService for displaying ContentDialogs inside a ViewModel. However, the ContentDialog requires the XamlRoot property to be set, however the MainWindow's XamlRoot is always null when I debug. and I get this error:

An exception of type 'System.ArgumentException' occurred in System.Private.CoreLib.dll but was not handled in user code
WinRT information: This element is already associated with a XamlRoot, it cannot be associated with a different one until it is removed from the previous XamlRoot.
Value does not fall within the expected range.

Below is my code for the ContentDialogService, MainWindow XAML, and MainWindow Code-Behind.

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Threading.Tasks;

namespace Nickvision.WinUI.MVVM.Services
{
    /// <summary>
    /// A service that contains methods for working with content dialogs
    /// </summary>
    public class ContentDialogService : IContentDialogService
    {
        private XamlRoot _mainXamlRoot;

        /// <summary>
        /// Constructs a ContentDialogService
        /// </summary>
        /// <param name="mainXamlRoot">The main window's content's XamlRoot to display the ContentDialog</param>
        public ContentDialogService(XamlRoot mainXamlRoot) => _mainXamlRoot = mainXamlRoot;

        /// <summary>
        /// Shows a content dialog
        /// </summary>
        /// <param name="text">The text of the content dialog</param>
        /// <param name="title">The title of the content dialog</param>
        /// <param name="closeButtonText">The text of the close button</param>
        /// <param name="primaryButtonText">The text of the primary button (optional)</param>
        /// <param name="secondaryButtonText">The text of the secondary button (optional)</param>
        /// <returns>The ContentDialogResult</returns>
        public async Task<ContentDialogResult> ShowAsync(string text, string title, string closeButtonText, string primaryButtonText = null, string secondaryButtonText = null)
        {
            var dialog = new ContentDialog()
            {
                Title = title,
                Content = text,
                CloseButtonText = closeButtonText,
                PrimaryButtonText = primaryButtonText,
                SecondaryButtonText = secondaryButtonText,
                XamlRoot = _mainXamlRoot
            };
            return await dialog.ShowAsync();
        }

        /// <summary>
        /// Shows a custom content dialog
        /// </summary>
        /// <typeparam name="T">A ViewModelBase</typeparam>
        /// <param name="viewModel">The ViewModel representing the ContentDialog</param>
        /// <returns>The ContentDialogResult</returns>
        public async Task<ContentDialogResult> ShowAsync<T>(T viewModel) where T : ViewModelBase
        {
            var dialog = ViewLocator.ContentDialogFromViewModel(viewModel);
            dialog.XamlRoot = _mainXamlRoot;
            return await dialog.ShowAsync();
        }
    }
}
<Window
    x:Class="NickvisionWinApp.Views.MainWindowView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:NickvisionWinApp.Views"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" Activated="Window_Activated">

    <NavigationView Name="NavigationView" PaneDisplayMode="Top" IsBackButtonVisible="Collapsed" IsSettingsVisible="False" SelectedItem="{x:Bind ViewModel.SelectedNavigationItem, Mode=TwoWay}">
        <NavigationView.MenuItems>
            <NavigationViewItem Content="Home" Icon="Home" Tag="Home" IsSelected="True"/>
        </NavigationView.MenuItems>

        <NavigationView.FooterMenuItems>
            <NavigationViewItem Icon="Setting" Tag="Settings"/>
        </NavigationView.FooterMenuItems>

        <Frame Content="{x:Bind ViewModel.SelectedPage, Mode=TwoWay}"/>
    </NavigationView>
</Window>
using Microsoft.UI.Xaml;
using Nickvision.WinUI.Helpers;
using Nickvision.WinUI.MVVM.Services;
using NickvisionWinApp.ViewModels;

namespace NickvisionWinApp.Views
{
    public sealed partial class MainWindowView : Window
    {
        public MainWindowViewModel ViewModel { get; private set; }

        public MainWindowView()
        {
            InitializeComponent();
            ViewModel = new MainWindowViewModel(new ContentDialogService(NavigationView.XamlRoot), new ProgressDialogService(NavigationView.XamlRoot));
            //this.Content.XamlRoot is also null
            Title = ViewModel.Title;
        }

        private void Window_Activated(object sender, WindowActivatedEventArgs args) => this.Maximize();
    }
}

Steps to reproduce the bug

Steps to reproduce the behavior:

  1. Create a new WinUI in Desktop application
  2. Download and reference the Nickvision.WinUI Nuget Package. (When you try to build you'll get an error. See Issue #4983 . You'll have to manually add these two files to the Nuget package (Steps on issue #4454) https://we.tl/t-RlUTQP9xrn)
  3. Create a button and click event inside the MainWindow
  4. Inside the click event code:
    IContentDialogService service = new ContentDialogService(this.Content.XamlRoot);
    await service.ShowAsync();
  5. Run the application and click the button and you'll get the error:
    An exception of type 'System.ArgumentException' occurred in System.Private.CoreLib.dll but was not handled in user code
    WinRT information: This element is already associated with a XamlRoot, it cannot be associated with a different one until it is removed from the previous XamlRoot.
    Value does not fall within the expected range.

    Expected behavior

    The ContentDialog should be displayed correctly, without errors.

Version Info

WinUI 3 - Project Reunion 0.5.6 Windows 10 V20H2

NuGet package version: [WinUI 3 - Project Reunion 0.5: 0.5.6] [Nickvision.WinUI V2021.5.0.6-beta]

Windows app type: UWP Win32
Yes
Windows 10 version Saw the problem?
Insider Build (xxxxx)
October 2020 Update (19042) Yes
May 2020 Update (19041)
November 2019 Update (18363)
May 2019 Update (18362)
October 2018 Update (17763)
April 2018 Update (17134)
Fall Creators Update (16299)
Creators Update (15063)
Device form factor Saw the problem?
Desktop Yes
Xbox
Surface Hub
IoT
StephenLPeters commented 3 years ago

@Austin-Lamb and @JesseCol FYI

sigmarsson commented 3 years ago

Gentlemen, i witness the same error with the latest Uno framework even though the dialog's XamlRoot is assigned to the Shell's one.

sigmarsson commented 3 years ago

@StephenLPeters will you fix this ?

image

Austin-Lamb commented 3 years ago

Hey folks, sorry for not noticing this issue earlier. The XamlRoot property only gets set when the element in question gets loaded into a visual tree - that's how we know what tree of XAML content it's associated with. For a UIElement this makes sense, as it could be added to any tree so we can't infer it until it really is in one.

We should consider exposing a Window.XamlRoot property which could be more eagerly available for scenarios like this (@marb2000 please consider that).

For now, what you should do is wait for Window.Content.Loaded to be raised, at which point Window.Content.XamlRoot will be valid - this is true of any UIElement, not just Window.Content, but that's your use case in the code sample above.

@nlogozzo - could you see if that works for your case?

sigmarsson commented 3 years ago

@Austin-Lamb I carry out somethign like this but to no avail :

        protected override UIElement CreateShell()
        {
           var shell = Container.Resolve<Shell>();

#if HAS_UNO_WINUI || NETCOREAPP
            shell.Loaded += (s, e) =>
            {
                MainXamlRoot = (s as UIElement).XamlRoot;
            };
#endif
            return shell;
        }

        protected override void InitializeShell(UIElement shell)
        {
#if WINDOWS
            Window = new Window();
            Window.Activate();
#else
            Window = Window.Current;
#endif
            Window.Content = shell;
        }

Window.Content.Loaded ain't existing inside Uno.

image

Austin-Lamb commented 3 years ago

@sigmarsson - I feel like I'm getting lost. Uno is a separate codebase run by the Uno team/company at this github: https://github.com/unoplatform/uno

If this issue only repros in Uno, please report the issue there for their team to look at.

Does this issue repro for you in WinUI 3? And in your screenshot it looks like you're getting an instance of XamlRoot back, so can you clarify what isn't working? Do you happen to have a minimal repro you could share all the code for in case the snippets are not revealing enough about where the problem may be?

sigmarsson commented 3 years ago

@Austin-Lamb , No Doubt !

I am just trying to open a dialog as the Prism's DialogService is harnessed here and this issue hopefully provides more details what is happening and going wrong .

Yes, it is compiled as Uno's WinUI 3 (aka ReUnion 0.8.1 ) Desktop Head, so I thought it is the same WinUI 3 lib. Please tell if I shall delegate this backlog to Uno or I shall ask the Prism engineers again.

And yes, the XamlRoot has value in the callback. Is it worth to use ContentDialog by the way ? I am unable to alter its dimensions, I read somewhere - it is because a legacy UWP architecture restricts it and puts a crimp on changing the width and height.

Should my previous comments not be sufficient to resolve this, I am offering screen sharing rather than git.

sigmarsson commented 3 years ago

Setting here the dialog's XamlRoot :

image

nlogozzo commented 3 years ago

I believe this issue is related. I'm trying to show a custom ContentDialog on my Activated event on a C++ WinUI app. Here is the code:

IAsyncAction MainWindow::WindowActivated(const IInspectable& sender, const WindowActivatedEventArgs& args)
    {
        if (!m_opened)
        {
            Configuration configuration = Configuration::Load();
            if (configuration.IsFirstTimeOpen())
            {
                WelcomeDialog welcomeDialog;
                welcomeDialog.XamlRoot(Menu().XamlRoot());
                co_await welcomeDialog.ShowAsync();
            }
            m_opened = true;
        }
    }

However, I'm now getting the following issues:

Exception thrown at 0x7666B512 (KernelBase.dll) in NickvisionApplication.exe: WinRT originate error - 0x80070490 : 'Windows.Graphics.Display: GetForCurrentView must be called on a thread that is associated with a CoreWindow.'.

Exception thrown at 0x7666B512 (KernelBase.dll) in NickvisionApplication.exe: WinRT originate error - 0x80070057 : 'This element is already associated with a XamlRoot, it cannot be associated with a different one until it is removed from the previous XamlRoot.'.

Exception thrown at 0x7666B512 (KernelBase.dll) in NickvisionApplication.exe: WinRT originate error - 0x80070490 : 'Windows.Graphics.Display: GetForCurrentView must be called on a thread that is associated with a CoreWindow.'.

Exception thrown at 0x7666B512 (KernelBase.dll) in NickvisionApplication.exe: WinRT originate error - 0x80000019 : 'Only a single ContentDialog can be open at any time.'.
nlogozzo commented 3 years ago

Temporary Fix: Don't use Activated event of Window. Instead, create a Loaded event for the Grid (or whatever control you have` for the window.

sigmarsson commented 3 years ago

@nlogozzo , I do not know . I do attach to the Shell's Loaded event. Your first issue is a threading one; "GetForCurrentView must be called on a thread that is associated with a CoreWindow."

nlogozzo commented 3 years ago

Well I never call GetForCurrentView anywhere in the method nor do I switch threads in that method or in the WelcomeDialog code.

Austin-Lamb commented 3 years ago

@sigmarsson - It appears you are setting the XamlRoot on your UserControl, but you need to set it on the ContentDialog itself so the dialog knows what tree of content you want it to show up in (what window, basically).

I'm not sure what's going on with the originate errors you're seeing about GetForCurrentView - can you have Visual Studio break on WinRT Originate Error (see the Exceptions window to do this) to get a callstack for when that is happening? You'll need to have a mixed mode debugger attached to get the native bits of the callstack.

sigmarsson commented 3 years ago

@Austin-Lamb , okay i changed the UserControl to ContentDialog but to no avail. Same ex.

So the dialog xaml is as now :

<ContentDialog 
    x:Class="Weather.History.Mvvm.Views.SettingsDialog"
    xmlns:prismmvvm="using:Prism.Mvvm"
    prismmvvm:ViewModelLocator.AutowireViewModel="True" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:myctr="using:Weather.History.UI.Control"
    xmlns:myux="using:Weather.History.Mvvm.Model"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
    Height="768"
    Width="1024">

The XamlRoot magic is left everywhere the same.

mqudsi commented 3 years ago

I had a same problem with the initialization of a Pivot. Merely including it in my XAML would crash because it would call GetForCurrentView in its initialization. I tore my hair out trying to figure it out, copied the code to a new directory, and started adding things in one-by-one. In the end, diff -ur old/ new/ revealed zero differences (except minor differences in generated code) but "old" triggered the crash in Pivot init and "new" didn't. I just deleted "old" and started using "new" instead. Something weird is going on.

sigmarsson commented 3 years ago

@mqudsi , every now an then kick back. we all build applications piece by piece. Sometimes by tiny pieces, hence these projects shall become a hobby rather than a duty at these stages.

yoshiask commented 3 years ago

I have a UWP app (just UWP, not Uno) that I'm trying to port to WinUI 3 [1.0.0-preview3]. The following code is present in click handler for a button in a XAML page:

var editDialog = new Views.Auth.EditProfileDialog(UserService.CurrentProfile);
if (await editDialog.ShowAsync() == ContentDialogResult.Primary)
{
    // ...
}

The exception is thrown at the ShowAsync() call.

RajeetGoyal commented 2 years ago

As per the docs, there is a need to manually set the XamlRoot on the dialog to the root of the XAML host.

contentDialog.XamlRoot = elementAlreadyInMyAppWindow.XamlRoot;
await contentDialog.ShowAsync();

Microsoft Documentation Link

jamiehankins commented 2 years ago

As per the docs, there is a need to manually set the XamlRoot on the dialog to the root of the XAML host.

contentDialog.XamlRoot = elementAlreadyInMyAppWindow.XamlRoot;
await contentDialog.ShowAsync();

Microsoft Documentation Link

Of course, that link is a 404 these days. It's been a whole seven months, I guess it's too much to expect for docs to be that long-lived.

anlocnghg commented 2 years ago

I found this documentation link (updated 07/08/2022) useful. I tried and it works. As the docs said, just manually set the XamlRoot of the ContentDialog as the XamlRoot of one of the elements, or the content, of the windows itself. The reason is:

By default, content dialogs display modally relative to the root ApplicationView. When you use ContentDialog inside of either an AppWindow or a XAML Island, you need to manually set the XamlRoot on the dialog to the root of the XAML host.

pszyjaciel commented 7 months ago

The XamlRoot property only gets set when the element in question gets loaded into a visual tree - that's how we know what tree of XAML content it's associated with.

Thank you bro. Austin-Lamb. You saved my time.

otdev1 commented 3 months ago

The code below solved the problem for me -

var dialog = new ContentDialog { Title = "My Media Collection", Content = "Adding items to the collection is not yet supported.", CloseButtonText = "OK", XamlRoot = this.XamlRoot }; await dialog.ShowAsync();

Also, have a look at the code snippet for ContentDialog in the WinUI 3 Gallery app https://apps.microsoft.com/detail/9p3jfpwwdzrc?hl=en-US&gl=US

private async void ShowDialog_Click(object sender, RoutedEventArgs e) { ContentDialog dialog = new ContentDialog();

// XamlRoot must be set in the case of a ContentDialog running in a Desktop app
dialog.XamlRoot = this.XamlRoot;
dialog.Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style;
dialog.Title = "Save your work?";
dialog.PrimaryButtonText = "Save";
dialog.SecondaryButtonText = "Don't Save";
dialog.CloseButtonText = "Cancel";
dialog.DefaultButton = ContentDialogButton.Primary;
dialog.Content = new ContentDialogContent();

var result = await dialog.ShowAsync();

}