Wednesday, September 16, 2009

Restoring WPF window of another process

Recently I was working on a WPF single instance application, which hides it’s main window and shows system tray icon. My task was to restore main window whenever user tries to open another instance of the application. The good thing I have control over source code, which means I can do anything to the target app. However there are 3 bad things related to WPF

WPF shortfalls
  • no out of the box support for single instance applications (in WinForms it is just a flag in project properties tab)
  • it is hard to get a handle of main window of another process, if that window is hidden. Process.MainWindowHandle would return 0.
  • when restoring window of a different process WPF does not listen for window events and thus WPF thread does not start rendering, as a result you get black window with XP blue frame around it.
1st problem

First problem is easily ISolvable either using Process.GetProcessByName(yourAppName) when returns true and process id is different from current process, then most likely there is another instance of the app is running, unless name of your app for some reason, is the same as some other app running on the box. In that case you can use mutex to solve the problem. In fact using Mutex is a more robust approach. You can find implementations in here.

2nd Problem

Second problem turned out to be lengthy and requires use of SharedMemoryFile and a bunch of other APIs. You can see how it is solved in the same article. This article however does not address WPF issue, since it was written prior WPF release.

3d Problem

While easily solvable, it took me some time to figure it out.
When you have a code like this:

'' if mutex was not created that means other instance is running, 
'' so we need to restore window of other application.
If Not IsMutexCreated Then
Try
Dim mainWindowHanle As IntPtr = System.IntPtr.Zero

SyncLock GetType(FilesView)
mainWindowHanle = MemoryMappedFile.ReadHandle("Local\sharedMemoryFilesView")
End SyncLock

If mainWindowHanle <> IntPtr.Zero Then
Dim result As Boolean
result = ShowWindowAsync(mainWindowHanle, SW_SHOWDEFAULT)
result = SetForgroundWindow(mainWindowHanle)
result = UpdateWindow(mainWindowHanle)
SetFocus(mainWindowHanle)
End If
Catch ex As Exception

End Try
Application.Current.Shutdown()

Try
_mutex.ReleaseMutex()
Catch ex As Exception

End Try
End If

the ShowWindow or ShowWindowAsync function will indeed restore the window, only with a little problem. It is going to be black. As WPF rendering runs on a separate thread and apparently not listening for main window events :(. Notice that I am not using GC.KeepAlive and GC.Collect like in the original article, but I declared mutex as a class member of Application class, which is a main class in WPF applications. In my case reference to mutex object is kept until Application class is disposed, which is when application shuts down. So GC (Garbage Collector) will not reclaim memory occupied by object mutex because it would have active reference.

blackWindow

So WPF thread is silent when some other process calls window restore or window show. Well, here is a good thing I mentioned in the beginning. I have complete control of the source code. And it means I can include event listener in the application main message loop and from there restore WPF window.

Since I know I am calling ShowWindow and SetFocus I can concentrate on the events which are fired in those two cases. It is most likely would be GotFocus, Activated or IsVisibleChanged events for WPF window.
In there I call Show method, and … it works!



Private Sub FilesView_Activated(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Activated
Application.Current.MainWindow.Show()
End Sub

Private Sub FileView_GotFocus(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.GotFocus
Application.Current.MainWindow.Show()
' or simply
' Me.Show() 'if you are already inside main window class
End Sub

' this should also work
Private Sub FilesView_IsVisibleChanged(ByVal sender As System.Object, ByVal e As System.Windows.DependencyPropertyChangedEventArgs) Handles MyBase.IsVisibleChanged
Me.Show()
End Sub

That’s it. A lot of sweat for a simple problem, but hey it is ISolvable :).

3 comments:

  1. Thanks for the post . It would be great if you could post a downloadable link for your application

    ReplyDelete
  2. Current application has a lot of other functionality and will not be a good learning example. At this point I don't have time to create another app with just this functionality, but may do it in some future.

    If you have any other questions or anything in particular that you would like to know or was not clear from the post, then please let me know. I can add more code sample if necessary.

    ReplyDelete
  3. Thanks Ivan! Based on your solution i implement a 3 step quick solution that solved my problem. Hope this information can help someone also!

    To Maximize a hidden window of another process you must do this 3 steps (very easy):

    1- Get the MainWindowHandle Because the application is hidden the MainWindowHandle is 0 (false). To overcome this problem you can use the FindWindow

    //Get the MainWindowHandle
    IntPtr hwnd = FindWindow(null, );
    2- Use ShowWindowAsync()

    bool result = ShowWindowAsync(hwnd, 3); //3= Maximize application
    3- Implement one of this application events: "GotFocus", "Activated" or "IsVisibleChanged" (I only tested the "Activated" event)

    private void WindowApplication_Activated(object sender, EventArgs e)
    {
    Application.Current.MainWindow.Show();
    }

    ReplyDelete