Eric Ouellet
Eric Ouellet

Reputation: 11754

In WPF, how to set a Window Owner of a Window build on another thread (another Dispatcher)

I got the following exception: InvalidOperationException : The calling thread cannot access this object because a different thread owns it.

when I try to set the Owner of a window that is build on another thread than the Owner.

I know that I can only update UI object from the proper thread, but why I can't just set the owner if it come from another thread? Can I do it on another way ? I want to make the progress window the only one which can have input entries.

This is the portion of code where bug occurs:

    public partial class DlgProgress : Window
{
    // ******************************************************************
    private readonly DlgProgressModel _dlgProgressModel;

    // ******************************************************************
    public static DlgProgress CreateProgressBar(Window owner, DlgProgressModel dlgProgressModel)
    {
        DlgProgress dlgProgressWithProgressStatus = null;
        var listDlgProgressWithProgressStatus = new List<DlgProgress>();
        var manualResetEvent = new ManualResetEvent(false);
        var workerThread = new ThreadEx(() => StartDlgProgress(owner, dlgProgressModel, manualResetEvent, listDlgProgressWithProgressStatus));
        workerThread.Thread.SetApartmentState(ApartmentState.STA);
        workerThread.Start();
        manualResetEvent.WaitOne(10000);
        if (listDlgProgressWithProgressStatus.Count > 0)
        {
            dlgProgressWithProgressStatus = listDlgProgressWithProgressStatus[0];
        }

        return dlgProgressWithProgressStatus;
    }

    // ******************************************************************
    private static void StartDlgProgress(Window owner, DlgProgressModel progressModel, ManualResetEvent manualResetEvent, List<DlgProgress> listDlgProgressWithProgressStatus)
    {
        DlgProgress dlgProgress = new DlgProgress(owner, progressModel);
        listDlgProgressWithProgressStatus.Add(dlgProgress);
        dlgProgress.ShowDialog();
        manualResetEvent.Set();
    }

    // ******************************************************************
    private DlgProgress(Window owner, DlgProgressModel dlgProgressModel)
    {
        if (owner == null)
        {
            throw new ArgumentNullException("Owner cannot be null");
        }

        InitializeComponent();
        this.Owner = owner; // Can't another threads owns it exception

Upvotes: 5

Views: 5519

Answers (4)

Ievgen
Ievgen

Reputation: 4443

Answer above was correct. But I will try to summarize:

[DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);

public static void SetOwnerWindowMultithread(IntPtr windowHandleOwned, IntPtr intPtrOwner)
{
    if (windowHandleOwned != IntPtr.Zero && intPtrOwner != IntPtr.Zero)
    {
        SetWindowLong(windowHandleOwned, GWL_HWNDPARENT, intPtrOwner.ToInt32());
    }
}

Code to get WPF handler:

public static IntPtr GetHandler(Window window)
{
    var interop = new WindowInteropHelper(window);
    return interop.Handle;
}

Note that window should be initialized before set owner call! (can be set in window.Loaded or window.SourceInitialized event)

var handler = User32.GetHandler(ownerForm);

var thread = new Thread(() =>
{
    var window = new DialogHost();
    popupKeyboardForm.Show();
    SetOwnerWindowMultithread(GetHandler(popupKeyboardForm), handler);
    Dispatcher.Run();
});

thread.IsBackground = true;
thread.Start();

Also SetParent can be used. Than you dont need to convert handlers:

[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

Note that parent and owner have different meanings. Win32 window Owner vs window Parent?

Upvotes: 2

fartwhif
fartwhif

Reputation: 359

internal class WindowHelp
{
    private const int GWL_HWNDPARENT = -8;
    [DllImport("user32.dll")]
    private static extern IntPtr SetWindowLong(IntPtr hwnd, int index, int newStyle);
    public static void SetOwnerWindow(IntPtr hwndOwned, IntPtr intPtrOwner)
    {
        try
        {
            if (hwndOwned != IntPtr.Zero && intPtrOwner != IntPtr.Zero)
            {
                SetWindowLong(hwndOwned, GWL_HWNDPARENT, intPtrOwner.ToInt32());
            }
        }
        catch { }
    }
}
WindowInteropHelper helper = new WindowInteropHelper(owner);
_messageBox.Loaded += (sender, e) =>
{
    IntPtr windowHandleOwned = new WindowInteropHelper(_messageBox).Handle;
    owner.Dispatcher.Invoke(new Action(() =>
    {
        WindowHelp.SetOwnerWindow(windowHandleOwned, helper.Handle);
    }));
};

A problem this has is that when the application is shutting down and the owned window is still open it will try to execute something that can fail, my guess is that it's trying to close all owned windows.

System.ComponentModel.Win32Exception
  HResult=0x80004005
  Message=Invalid window handle
  Source=WindowsBase
  StackTrace:
   at MS.Win32.ManagedWndProcTracker.HookUpDefWindowProc(IntPtr hwnd)
   at MS.Win32.ManagedWndProcTracker.OnAppDomainProcessExit()
   at MS.Win32.ManagedWndProcTracker.ManagedWndProcTrackerShutDownListener.OnShutDown(Object target, Object sender, EventArgs e)
   at MS.Internal.ShutDownListener.HandleShutDown(Object sender, EventArgs e)

A disadvantage of giving it its own thread is that you have to keep track of the child window and close it when the main window is closing before the application reaches the later stages of shut down:

private void View_Closing(object sender, CancelEventArgs e)
{
    UIGlobal.SelfThreadedDialogs.ForEach(k =>
    {
        try
        {
            if (k != null && !k.Dispatcher.HasShutdownStarted)
            {
                k.Dispatcher.InvokeShutdown();
                //k.Dispatcher.Invoke(new Action(() => { k.Close(); }));
            }
        }
        catch { }
    });
}

The price of having this kind of "multi-threaded and related" behavior.

Sometimes the tracking and/or the owner's View_Closing code is not needed. Sometimes you only need tracking to keep a reference to the owned windows so that they aren't garbage collected before application shutdown occurs. It depends. See what works for your situation.

Upvotes: 0

Eric Ouellet
Eric Ouellet

Reputation: 11754

I made it according based mainly on Hans Passant suggestion. Important, I suspect that this code should only work on 32 bits because I use "ToInt32" on IntPtr.

This is the code:

WindowHelper function:

        // ******************************************************************
    private const int GWL_HWNDPARENT = -8; // Owner --> not the parent

    [DllImport("user32.dll")]
    static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);

    // ******************************************************************
    public static void SetOwnerWindow(Window owned, IntPtr intPtrOwner)
    {
        try
        {
            IntPtr windowHandleOwned = new WindowInteropHelper(owned).Handle;
            if (windowHandleOwned != IntPtr.Zero && intPtrOwner != IntPtr.Zero)
            {
                SetWindowLong(windowHandleOwned, GWL_HWNDPARENT, intPtrOwner.ToInt32());
            }
        }
        catch (Exception ex)
        {
            Debug.Print(ex.Message);
        }
    }

    // ******************************************************************

Calling function:

  using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Threading;
using HQ.Util.General.Threading;
using HQ.Util.Unmanaged;

namespace HQ.Wpf.Util.Dialog
{
    /// <summary>
    /// Interaction logic for DlgProgressWithProgressStatus.xaml
    /// </summary>
    public partial class DlgProgress : Window
    {
        // ******************************************************************
        private readonly DlgProgressModel _dlgProgressModel;

        // ******************************************************************
        public static DlgProgress CreateProgressBar(Window owner, DlgProgressModel dlgProgressModel)
        {
            DlgProgress dlgProgressWithProgressStatus = null;
            var listDlgProgressWithProgressStatus = new List<DlgProgress>();
            var resetEvent = new ManualResetEvent(false);

            IntPtr windowHandleOwner = new WindowInteropHelper(owner).Handle;
            dlgProgressModel.Owner = owner;
            dlgProgressModel.IntPtrOwner = windowHandleOwner;

            var workerThread = new ThreadEx(() => StartDlgProgress(dlgProgressModel, resetEvent, listDlgProgressWithProgressStatus));
            workerThread.Thread.SetApartmentState(ApartmentState.STA);
            workerThread.Start();
            resetEvent.WaitOne(10000);
            if (listDlgProgressWithProgressStatus.Count > 0)
            {
                dlgProgressWithProgressStatus = listDlgProgressWithProgressStatus[0];
            }

            return dlgProgressWithProgressStatus;
        }

        // ******************************************************************
        private static void StartDlgProgress(DlgProgressModel progressModel, ManualResetEvent resetEvent, List<DlgProgress> listDlgProgressWithProgressStatus)
        {
            DlgProgress dlgProgress = new DlgProgress(progressModel);
            listDlgProgressWithProgressStatus.Add(dlgProgress);
            resetEvent.Set();
            dlgProgress.ShowDialog();
        }

        // ******************************************************************
        private DlgProgress(DlgProgressModel dlgProgressModel)
        {
            if (dlgProgressModel.Owner == null)
            {
                throw new ArgumentNullException("Owner cannot be null");
            }

            InitializeComponent();
            // this.Owner = owner; // Can't another threads owns it exception

            if (dlgProgressModel == null)
            {
                throw new ArgumentNullException("dlgProgressModel");
            }

            _dlgProgressModel = dlgProgressModel;
            _dlgProgressModel.Dispatcher = this.Dispatcher;
            _dlgProgressModel.PropertyChanged += _dlgProgressModel_PropertyChanged;
            DataContext = _dlgProgressModel;
        }

        // ******************************************************************
        // Should be call as a modal dialog
        private new void Show()
        {
            throw new Exception("Should only be used as modal dialog");
        }

        // ******************************************************************
        void _dlgProgressModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {

            //          if (e.PropertyName == "IsJobCanceled" || e.PropertyName == "IsJobCompleted" || e.PropertyName == "IsProgressCompleted")
            // Faster if we don't check strings and check condition directly 
            {
                if (_dlgProgressModel.HaveConditionToClose())
                {
                    if (_dlgProgressModel.IsJobCanceled == true)
                    {
                        SetDialogResult(false);
                    }
                    else
                    {
                        SetDialogResult(true);
                    }
                }
            }
        }

        // ******************************************************************
        private void SetDialogResult(bool result)
        {
            this._dlgProgressModel.Dispatcher.BeginInvoke(new Action(() =>
                {
                    this.DialogResult = result;
                }), DispatcherPriority.Background);
        }

        // ******************************************************************
        private bool _isFirstTimeLoaded = true;

        private Timer _timer = null;
        // ******************************************************************
        private void WindowLoaded(object sender, RoutedEventArgs e)
        {
            if (_isFirstTimeLoaded)
            {
                WindowHelper.SetOwnerWindow(this, _dlgProgressModel.IntPtrOwner);
                Dispatcher.BeginInvoke(new Action(ExecuteDelayedAfterWindowDisplayed), DispatcherPriority.Background);
                _isFirstTimeLoaded = false;

                if (_dlgProgressModel.FuncGetProgressPercentageValue != null)
                {
                    TimerCallback(null);
                    _timer = new Timer(TimerCallback, null, _dlgProgressModel.MilliSecDelayBetweenCall, _dlgProgressModel.MilliSecDelayBetweenCall);
                }
            }
        }

        // ******************************************************************
        private void TimerCallback(Object state)
        {
            Dispatcher.BeginInvoke(new Action(() =>
                {
                    _dlgProgressModel.ValueCurrent = _dlgProgressModel.FuncGetProgressPercentageValue();
                }));
        }

        // ******************************************************************
        private void ExecuteDelayedAfterWindowDisplayed()
        {
            if (_dlgProgressModel._actionStarted == false)
            {
                _dlgProgressModel._actionStarted = true;
                Task.Factory.StartNew(ExecuteAction);
            }
        }

        // ******************************************************************
        private void ExecuteAction()
        {
            _dlgProgressModel.ExecuteAction();
            _dlgProgressModel._actionTerminated = true;
            _dlgProgressModel.IsJobCompleted = true;
        }

        // ******************************************************************
        private void CmdCancel_Click(object sender, RoutedEventArgs e)
        {
            this._dlgProgressModel.IsJobCanceled = true;
        }

        // ******************************************************************
        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            if (! _dlgProgressModel.HaveConditionToClose())
            {
                e.Cancel = true;
                return;
            }

            WindowHelper.SetOwnerWindow(this, 0);

            this.CmdCancel.IsEnabled = false;
            this.CmdCancel.Content = "Canceling...";
            this._dlgProgressModel.Dispose();
        }

        // ******************************************************************
    }
}

Upvotes: 2

bash.d
bash.d

Reputation: 13207

It's not about setting the owner. If you want to manipulate a control in WPF from another thread, you'll need to create a delegate and pass this to the dispatcher of the control.

if(Control.Dispatcher.CheckAccess())
{
    //The control can be accessed without using the dispatcher.
    Control.DoSomething();
}
else{
     //The dispatcher of the control needs to be informed
     MyDelegate md = new MyDelegate( delegate() { Control.DoSomething(); });
     Control.Dispatcher.Invoke(md, null);
}

See this post.

Upvotes: -1

Related Questions