12

Snoop, the spy utility, uses some powerful technique (probably some sort of reflection) to inspect a running WPF application. Most interesting is the fact, that Snnop is able to readout the entire object structure.

A few days ago I downloaded the Snoop source code and spent some time on studying the internal behavior. Unfortunately, I couldn't find out yet how Snoop is doing these things, so I hope that anybody can help me out.

At work I am currently writing a Coded UI Testing-Framework and it would be fantastic if I had access to the application's object structures because this would allow me to not only assert the UI state.

UPDATE:

This is the code needed:

string filePath = "WpfApp.exe";
AppDomain appDomain = AppDomain.CurrentDomain;
byte[] bytes = System.IO.File.ReadAllBytes(filePath);
Assembly ass = appDomain.Load(bytes);
ass.EntryPoint.Invoke(null, new object[] { });
IntPtr handle = Process.GetCurrentProcess().MainWindowHandle;
Window w = System.Windows.Interop.HwndSource.FromHwnd(handle).RootVisual as Window;

This is already a big help for me, but it is also interesting to find out, how Snoop injects itself into another process.

Dennis Kassel
  • 2,726
  • 4
  • 19
  • 30

4 Answers4

4

You can accomplish what Snoop does by using the WPF VisualTreeHelper and/or the LogicalTreeHelper. Once you get a hold of any visual element, you can pretty much traverse its entire visual tree to see all the elements it contains. Visual tree helper here

So in your UI test, grab the main window and traverse its visual tree to find any element you want and then perform any validations or operations you want on that element.

Furthermore, you may be able to use System.Diagnostics.Process.MainWindowHandle to get the windows handle from an existing process and then use the window's handle to create a wpf window. Its been a while so I dont remember the specifics without doing more research. The code below may help:

Window window = (Window)System.Windows.Interop.HwndSource.FromHwnd(process.MainWindowHandle).RootVisual;
Kess
  • 488
  • 3
  • 11
  • 3
    This doesn't explain how is Snoop "initially" getting access to an external process' WPF Visual tree at all. And it also does not explain how is Snoop able to inspect object instances such as the DataContext which is not a UI element and does not belong into the Visual Tree. – Federico Berasategui Jun 04 '14 at 19:11
  • 1
    @HighCore - Thanks I think I may have misunderstood your question and have updated my answer. So get the process, then get its main window handle. Then convert that handle to a wpf window. – Kess Jun 04 '14 at 19:46
  • 1
    @Kess Just guessing things is a bad approach for an answer here. We'd prefer profound knowledge. – Clemens Jun 04 '14 at 19:49
  • @Kess much better. But that is not how snoop works. As mentioned in my comment, Snoop injects itself into the running process – Federico Berasategui Jun 04 '14 at 19:49
  • @HighCore - Could you please explain why you think snoop injects itself into a running process? I would guess all snoop does is traverse the visual tree and logical tree and therefore only needs the main wpf application. – Kess Jun 04 '14 at 19:57
  • @Clemens - Please reconsider your comment. I strongly believe I am in the right direction - there really wasn't any guess work being done. – Kess Jun 04 '14 at 19:58
  • 1
    "Snoop is most likely using [...]" doesn't sound like you *know* what it does. – Clemens Jun 04 '14 at 20:05
  • @Kess do F5 in Visual Studio (new WPF app or something) then snoop the app, then check the Visual Studio output Window ;) – Federico Berasategui Jun 04 '14 at 20:10
  • @Kess BTW, I agree with @Clemens, you're just `guessing` and that doesn't sound like an acceptable answer in SO. – Federico Berasategui Jun 04 '14 at 20:11
  • @Cemens - You may be right and I cannot speak for how snoop works internally. I am merely providing a solution that can accomplish similar results. – Kess Jun 04 '14 at 20:23
  • This does not work. I ran a WPF application in the background. In a separate project I tried out the given statement, but I always get a null-reference from the FromHwnd-method. UPDATE: Okay, the reason is that both processes don't run in the same app domain. I will try it again – Dennis Kassel Jun 04 '14 at 21:25
  • @GinoBambino - Do you need to run your test from another process or can your test just load the assemblies it needs and then run the app? – Kess Jun 04 '14 at 21:57
  • @Clemens - You and HighCore are right in many ways. Unless the OP plans to load his test and application in the same process this answer is not useful. I will be very glad to delete it. – Kess Jun 04 '14 at 22:19
  • I cannot say for certain if I can load the assembly at runtime. My program gets invoked by the Visual Studio Testing Framework. I will try this out tomorrow. BTW: The given example works! It is absolutely great. I executed a the WPF application-assembly at runtime and then accessed the handle. – Dennis Kassel Jun 04 '14 at 22:26
  • 1
    @GinoBambino - if your application is running in the same app domain as your test, you may also be able to just use var window = Application.Current.MainWindow; – Kess Jun 04 '14 at 22:37
  • You are right. I had to write a separate main-method, because I always got an exception, that "MainWindow.xaml" is not available. Do you have an idea what the generated App-class is doing wrong? – Dennis Kassel Jun 04 '14 at 22:46
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/55101/discussion-between-kess-and-ginobambino). – Kess Jun 04 '14 at 23:16
  • Now this (quite interesting) question has an *accepted* answer that neither answers the original question, nor shows a working alternative. WTF? – Clemens Jun 05 '14 at 05:08
  • I canceled it and still hope that some curious developer have a look on it. – Dennis Kassel Jun 05 '14 at 08:20
  • @GinoBambino - However, was this answer useful to you? – Kess Jun 05 '14 at 09:49
  • It definitely was. However, I think that developer among us will not be interested in this topic, when there is already an accepted answer. I want to wait for further answers, but when nobody have an answer then I will make it the accepted answer again ;-) – Dennis Kassel Jun 05 '14 at 09:57
  • @GinoBambino - I understand. I am also hoping that someone would provide a better answer. – Kess Jun 05 '14 at 09:59
  • FromHwnd just returns null for me – Christian Findlay Oct 22 '18 at 21:59
  • FromHwnd returns null because of FromHwnd does not work across process boundaries. see https://github.com/snoopwpf/snoopwpf/discussions/368#discussioncomment-3466321 – SamBerk Aug 24 '22 at 14:40
3

UPDATE:

Okay, I found the basic code location, that is used by Snoop to provide the injection ability. To my astonishment that code is written C++/CLI. Probably there is a reason for.

And that is the code (I hope that it is okay to post it here):

//-----------------------------------------------------------------------------
//Spying Process functions follow
//-----------------------------------------------------------------------------
void Injector::Launch(System::IntPtr windowHandle, System::String^ assembly, System::String^ className, System::String^ methodName)
{
    System::String^ assemblyClassAndMethod = assembly + "$" + className + "$" + methodName;
    pin_ptr<const wchar_t> acmLocal = PtrToStringChars(assemblyClassAndMethod);

    HINSTANCE hinstDLL; 

    if (::GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCTSTR)&MessageHookProc, &hinstDLL))
    {
        LogMessage("GetModuleHandleEx successful", true);
        DWORD processID = 0;
        DWORD threadID = ::GetWindowThreadProcessId((HWND)windowHandle.ToPointer(), &processID);

        if (processID)
        {
            LogMessage("Got process id", true);
            HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, processID);
            if (hProcess)
            {
                LogMessage("Got process handle", true);
                int buffLen = (assemblyClassAndMethod->Length + 1) * sizeof(wchar_t);
                void* acmRemote = ::VirtualAllocEx(hProcess, NULL, buffLen, MEM_COMMIT, PAGE_READWRITE);

                if (acmRemote)
                {
                    LogMessage("VirtualAllocEx successful", true);
                    ::WriteProcessMemory(hProcess, acmRemote, acmLocal, buffLen, NULL);

                    _messageHookHandle = ::SetWindowsHookEx(WH_CALLWNDPROC, &MessageHookProc, hinstDLL, threadID);

                    if (_messageHookHandle)
                    {
                        LogMessage("SetWindowsHookEx successful", true);
                        ::SendMessage((HWND)windowHandle.ToPointer(), WM_GOBABYGO, (WPARAM)acmRemote, 0);
                        ::UnhookWindowsHookEx(_messageHookHandle);
                    }

                    ::VirtualFreeEx(hProcess, acmRemote, 0, MEM_RELEASE);
                }

                ::CloseHandle(hProcess);
            }
        }
        ::FreeLibrary(hinstDLL);
    }
}
OwnageIsMagic
  • 1,949
  • 1
  • 16
  • 31
Dennis Kassel
  • 2,726
  • 4
  • 19
  • 30
  • 1
    Got 90% of the way through rewriting this in C#, and then found the reason why it was written in C: https://stackoverflow.com/a/9104329 If the application installs a hook procedure for a thread of a different application, the procedure must be in a DLL. -- Also -- To install a global hook, a hook must have a native DLL export to inject itself in another process that requires a valid, consistent function to call into. This behavior requires a DLL export. The .NET Framework does not support DLL exports. – abelito Jan 24 '18 at 23:46
  • 1
    You don't need to use C. Have a look at the Injector class inside the Snoop ManagedInjector64 dll with DNSpy. – Christian Findlay Oct 23 '18 at 01:10
0

Snoop doesn't inspect a WPF from the outside. It injects itself into the application and actually adds the magnify or snoop window to it. Thats also why when you exit snoop the inspection windows actually stay open.

So the 'inspection' code simply inspects the window it wants and it can use all avaible WPF functions to do so. Like the VisualTreeHelper and LogicalTreeHelper as mentioned here earlier.

For a small test framework i build i injected code to add a small proxy object so i can control the application easily (press buttons, change values, execute functions on viewmodels etc).

Wouter Schut
  • 907
  • 2
  • 10
  • 22
  • Yeah. That's right but it still remains unclear what technique Snoop uses to inspect the application and its visual tree. – Dennis Kassel Mar 07 '15 at 23:01
0

The answer above doesn't work for me. It seems a bit vague. I expanded on accepted answer a little with this code:

    var allProcesses = Process.GetProcesses();
    var filteredProcess = allProcesses.Where(p => p.ProcessName.Contains(ProcessSearchText)).First();
    var windowHandle = filteredProcess.MainWindowHandle;
    var hwndSource = HwndSource.FromHwnd(windowHandle);

This answer seems more complete and will work for others if the accepted answer works for anyone. However, this the last line of this code returns null for me.

Christian Findlay
  • 6,770
  • 5
  • 51
  • 103