Welcome to MSDN Blogs Sign in | Join | Help

Enabling Assistive Devices programmatically for UI Automation on the Mac

As part of our effort to test Windows Presentation Foundation Everywhere (WPF/E) on the Mac, I'm working on a UI Automation tool to drive WPF/E apps from out of process.  In the anticipation that WPF/E will eventually support Accessibility (and I assure you it will!), we need to programmatically turn on the checkbox in Mac OS X to enable the AXAPI (Mac's technology similar to MSAA).

It turns out this option in the UI is controlled by the magical file: "/private/var/db/.AccessibilityAPIEnabled".  If this file exists, the checkbox is checked, and we can drive applications through AXAPI.  If you delete the file, then accessibility is obviously disabled.  This operation is reversible on the fly and does not appear to require a reboot to swap, which is really handy.

The final requirement is to create this file programmatically, in my case, through a shell script... although I'm sure there's a cleaner, more involved way to do it.  This is the quick and (very) dirty way.  Put the following script into a file, and execute it from a Mac Terminal window with "sh myFile".

#!/bin/sh
cd /private/var/db
Echo “$ADMINPWD” | sudo touch .AccessibilityAPIEnabled

Of course, emitting the admin password in a text file is what really constitutes "dirty", but it suits my immediate purposes.  Someone on the Mac Office team suggested compiling this into an app and setting the "sticky" bit by installing with "1755 permissions" so I don't have to embed the admin pword in my script, which sounds like a good next step.  There are many, many more issues to go =).  I'm still very new to automation issues on the Mac.  We have experts in the company, but having me bother them isn't always optimal, so I'd definitely be interested in any good online resources that you community folks have in mind.  Any help is greatly appreciated!

Thanks,

Bri

Posted by brianmcm | 1 Comments

Yahoo! Messenger built in Windows Presentation Foundation

You've probably seen this already, but if not, you should really check out the video preview of Yahoo!'s messenger app built with WPF.  http://messenger.yahoo.com/windowsvista.php is the link.  I can't wait to see this thing running live.  It really seems like they've captured the power of what you can do with WPF.

My team within Microsoft is working on releasing our first beta of Cider, which is a visual designer targetting developers wanting to write cool WPF apps in a WYSIWYG editing environment within Visual Studio 9.0.  Our designer will fit in with the rest of the Microsoft Expression technologies, so you'll be able to open your Cider apps in Blend to spice them up with beautiful gradients and animations, but still get your awesome development, layout, and debugging experience within Visual Studio and seamlessly go back and forth between your Expression apps to get your forms looking the way you like.

Of course, after watching this Yahoo! Messenger demo, the question we're all asking is... why not MSN Messenger?  Good question.  I have no idea.  I hope MSN does something sweet, too, otherwise I may have a reason to get an @yahoo email address ;-0.

Bri

Posted by brianmcm | 0 Comments

Huge VS Debugger Fix in Orcas (IMHO) =)

The bug in the VS debugger that has been a thorn in my side for well over a year is now fixed in the next version of VS!  If you're a religious VS developer, I'm almost positive you've hit this bug before, although you may not have noticed.  The bug in Whidbey is basically that "Set Next Statement" in the right-click menu is broken when your cursor is at the end of a line.  In reality, it thinks the insertion point is on the next line, so it tries to set the next statement there, instead.

This bug can manifest itself in one of two ways.  If the line after the line your cursor is on is a valid breakpoint line, then the next statement will be set to that line.  If this happens to you once, you will probably just think that you mis-clicked, and you will simply attempt to set the next statement to the previous line again.  However, if the next line is not a valid line for a breakpoint, then you will see an error message saying, "unable to set the next statement to this location."

Here's an official MS knowledge base article on the issue, as well. http://support.microsoft.com/kb/919808

This bug really bothered me, because it made me change the way I do work.  Anytime the tool makes me conciously do something differently than what I'm used to doing, it's a bad bug.  I was unable to convince the triage team for Whidbey, however =(.  The good news is, after fighting for this bug off-and-on for over a year and a half, it will now be fixed in the next version!

Bri

Posted by brianmcm | 0 Comments

Using StringBuilder to improve performance of your apps

TestSuites.cs file generated by my model, and parse it out into multiple .cs files each representing one testcase.  This sounded great to me, because my TestSuites.cs file was 113Mb, and splitting it out into around 300 testcase files sounded good to me.
 
However, when I ran the tool for about 3 hours without it completing, I began to wonder what was going on.  From debugging it with a friend of mine, David Owens, we discovered that the main reason it took so long was because the original tool author was using normal string concatenation during the parsing.  Here is some sample code which illustrates the problem.

static void Main(string[] args) {
    string contents = "";
    DateTime dtStart = DateTime.Now;

    StreamReader sr = new StreamReader(@"C:\TestSuites.cs");
    while (!sr.EndOfStream) {
        contents += sr.ReadLine();  // Don’t do this =)
    }
    DateTime dtEnd = DateTime.Now;
    TimeSpan ts = dtEnd.Subtract(dtStart);
    Console.WriteLine(contents + "\r\n" + ts.Seconds.ToString());
} 


If you’ve written code like this before, and you’re familiar with some of the static analysis tools like FxCop, you’ve probably been informed that this is bad.  The offending line is “contents += sr.ReadLine();”.  The root of the bug is that strings in .NET are “immutable.”  Basically, what that means is, whenever you assign a new string to a variable, the old string gets marked for garbage collection, and new memory is allocated to house the new string.  In this case, the code file is about 1.64 million lines, and memory allocation is bigger than the last.  Based on an average line length, it would take approximately 87.3 terra-bytes of memory to store all the strings allocated in this example.  Now you see why my laptop was thrashing for over 3 hours trying to parse this file.  Time for Microsoft to upgrade my laptop, IMO =).
 
Enter, the System.Text.StringBuilder class.  The idea behind this class is to allocate a fixed chunk of memory to build the string, and only allocate a new buffer if the string you're building becomes too long for the original allocation.  Once you're ready to use it, it allocates an immutable string object of the correct size.  In this example, the fix is to use the Append() method to build up my uber string, like so:

static void Main(string[] args) {
    StringBuilder contents = new StringBuilder("");
    DateTime dtStart = DateTime.Now;
            
    StreamReader sr = new StreamReader(@"C:\TestSuites.cs");
    while (!sr.EndOfStream) {
        contents.Append(sr.ReadLine());
    }
 
    DateTime dtEnd = DateTime.Now;
    TimeSpan ts = dtEnd.Subtract(dtStart);
    Console.WriteLine(contents + "\r\n" + ts.Seconds.ToString());
}

Now, each time I read a new line from the file, rather than allocating a new immutable string to store the current contents, the StringBuilder class just stores a pointer to that substring in memory until the last line of my Main() function where it outputs the string to the console window.  It takes quite a bit longer to output the 113Mb of my file to the console than it does to execute the while loop.  But the perf improvement is amazing.  My while loop now executes in 24 seconds instead of several hours!!
 
Fun stuff,
Bri

Posted by brianmcm | 2 Comments

ISU Tech Talk on WPF and UIAutomation

I had the pleasure of speaking to the ISU students on Tuesday this week.  I gave a high-level overview of WPF (Avalon) and Windows UIAutomation (vNext of MSAA).  I wanted to go ahead and get my basic slide deck posted in case anyone wanted it.  Sorry I don't have videos of the demos.

It was fun to show people some of the cool new features of Sparkle and Cider.  I got some great questions from the students, and it was cool to see them really "get" Microsoft's WPF story.  One of the students (Chris Sammis) has even been downloading WinFX betas and doing real development with it and supplying us with feedback and bug reports.  Kudos to Chris!

I also got to play Micro-Santa and held a raffle to give away cool prizes including, but not limited to:  MS Office 2003 Pro, Halo2, Fable, Age of Empires 3, Rise of Nations, MS Money, tons of T-Shirts, and an XBox 360!!  Congratulations to Brandon Kennedy on winning the XBox.  I hope he has fun w/that between mid-terms =). 

Thanks a lot to the NSBE (National Society of Black Engineers) for sponsoring the event and providing the Pizza and pop.  I appreciate the students who took the time to help organize and advertise this event.  There were 85 people at the talk, which my MS recruiters tell me was a record attendance, so thanks to everyone who made it successful.  I would definitely do this again!

Bri

Posted by brianmcm | 1 Comments

Attachment(s): ISU WPF Presentation.zip

PInvoke Declarations for My Sample Code

Thanks to Ed from Comcast for pointing out that my 1/17/06 post regarding Windows Forms ID's was missing the NativeMethods.cs file.  Sorry for the inconvenience, Ed.  Here's the NativeMethods.cs file that will help you get the code up and running.

using System;
using System.Runtime.InteropServices;

namespace GetWinFormsId
{
    /// <summary>
    /// Summary description for NativeMethods.
    /// </summary>
    public class NativeMethods
    {
        
        [DllImport("kernel32.dll")]
        public static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle,
            uint dwProcessId);
        [DllImport("kernel32.dll")]
        public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress,
            UIntPtr dwSize, uint flAllocationType, PageProtection flProtect);
        [DllImport("user32.dll", SetLastError=true)]
        public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
        [DllImport("kernel32.dll")]
        public static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress,
            UIntPtr dwSize, uint dwFreeType);
        [DllImport("kernel32.dll")]
        public static extern bool CloseHandle(IntPtr hObject);
        [DllImport("kernel32.dll")]
        public static extern IntPtr MapViewOfFile(IntPtr hFileMappingObject, uint
            dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffsetLow,
            UIntPtr dwNumberOfBytesToMap);
        [DllImport("kernel32.dll")]
        public static extern bool UnmapViewOfFile(IntPtr lpBaseAddress);
        [DllImport("kernel32.dll", SetLastError=true)]
        public static extern IntPtr CreateFileMapping(IntPtr hFile,
            IntPtr lpFileMappingAttributes, PageProtection flProtect, int dwMaximumSizeHigh,
            int dwMaximumSizeLow, string lpName);
        [DllImport("user32.dll")]
        public static extern IntPtr SendMessage(IntPtr hwnd, int wMsg, IntPtr wParam, IntPtr lParam);
        [DllImport("kernel32.dll")]
        public static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress,
            [Out] byte [] lpBuffer, UIntPtr nSize, IntPtr lpNumberOfBytesRead);
        [DllImport("Kernel32.dll", EntryPoint="RtlMoveMemory", SetLastError=false)]
        public static extern void MoveMemoryFromByte(IntPtr dest, ref byte src, int size);
        [DllImport("Kernel32.dll", EntryPoint="RtlMoveMemory", SetLastError=false)]
        public static extern void MoveMemoryToByte(ref byte dest, IntPtr src, int size);
        [DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        public static extern int RegisterWindowMessage(string lpString);



        //=========== Win95/98/ME Shared memory staff===============
        public const int STANDARD_RIGHTS_REQUIRED = 0xF0000;
        public const short SECTION_QUERY = 0x1;
        public const short SECTION_MAP_WRITE = 0x2;
        public const short SECTION_MAP_READ = 0x4;
        public const short SECTION_MAP_EXECUTE = 0x8;
        public const short SECTION_EXTEND_SIZE = 0x10;
        public const int SECTION_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SECTION_QUERY | SECTION_MAP_WRITE | SECTION_MAP_READ | SECTION_MAP_EXECUTE | SECTION_EXTEND_SIZE;
        public const int FILE_MAP_ALL_ACCESS = SECTION_ALL_ACCESS;

        //============NT Shared memory constant======================
        public const short PROCESS_VM_OPERATION = 0x8;
        public const short PROCESS_VM_READ = 0x10;
        public const short PROCESS_VM_WRITE = 0x20;
        public const long PROCESS_ALL_ACCESS = 0x1F0FFF;
        public const short MEM_COMMIT = 0x1000;
        public const short MEM_RESERVE = 0x2000;
        public const short MEM_DECOMMIT = 0x4000;
        public const int MEM_RELEASE = 0x8000;
        public const int MEM_FREE = 0x10000;
        public const int MEM_PRIVATE = 0x20000;
        public const int MEM_MAPPED = 0x40000;
        public const int MEM_TOP_DOWN = 0x100000;


        public const int INVALID_HANDLE_VALUE = -1;
        


    }

    [Flags]
    public enum PageProtection : uint 
    {
        NoAccess =     0x01,
        Readonly =     0x02,
        ReadWrite =    0x04,
        WriteCopy =    0x08,
        Execute =      0x10,
        ExecuteRead =      0x20,
        ExecuteReadWrite = 0x40,
        ExecuteWriteCopy = 0x80,
        Guard =        0x100,
        NoCache =      0x200,
        WriteCombine =     0x400,
    }
}
Posted by brianmcm | 2 Comments

Getting the WinForms ID of a Control

Happy New Year and stuff! =)

If you've read my article on issues related to automating Windows Forms with traditional Win32-like UI Automation tools, then you know about what we refer to as the Windows Forms ID.  If you're not familiar with the issue, here's a quick intro.  Basically, when automating Win32 UI in the past, the typical way that you located a control through code was by using the Control ID.  You probably had some method in your automation framework called "FindControlById()" or something similar that took a window handle and an integer as parameters, and then searched the descendants of that window handle for a window who's Control ID matched the one you passed in.  Well that's all great, except this method is useless in automating Windows Forms.  That's because the Control ID for Windows Forms UI is a mirror of the HWND of the control.  Thus, it will not be the same on subsequent launches of the app.

The replacement for Control ID's in Windows Forms is "Windows Forms ID."  When you develop a Windows Forms application in Visual Studio using the designer, it will automatically set the ".Name" property for your controls to default values (usually "button1," "button2," "listBox1," etc.).  So what you really want to use in your automation is to be able to ask a particular control what it's Name property is set to.  Windows Forms supports just that.  If you send a WM_GETCONTROLNAME message using a standard SendMessage Windows API to any Windows Forms control, it will respond in the LPARAM with it's Name property.  The code in my article is written in Rational Visual Test, which is kind of pseudo-Basic I would say.  However, I often get asked for C# code to get the WinformsId, so I'm going to paste it in below.

As you can see, it has a public static method called GetWinFormsId() that takes an HWND as a parameter and returns a string representing the Name.  So all you have to do is add this code to a C# project, and then write some code to walk the Windows Hierarchy using the GetWindow() Function or similar API's to find the HWNDs of the controls on your form.  Then you can call WinFormsUtilities.GetWinFormsId() on those HWNDS to see if you've found the HWND of the control you're looking for.

using System;
using System.Text;
using System.ComponentModel;

namespace GetWinFormsId
{
    /// <summary>
    /// Summary description for WinFormsUtilities.
    /// </summary>
    public class WinFormsUtilities
    {
        private static int GetControlNameMessage = 0;

        static WinFormsUtilities()
        {
            GetControlNameMessage = NativeMethods.RegisterWindowMessage("WM_GETCONTROLNAME");
        }

        public static string GetWinFormsId(IntPtr hWnd)
        {
            return XProcGetControlName(hWnd, GetControlNameMessage);
        }

        protected static string XProcGetControlName(IntPtr hwnd, int msg)
        {
            //define the buffer that will eventually contain the desired window's WinFormsId

            byte[] bytearray = new byte[65536];

            //allocate space in the target process for the buffer as shared memory
            IntPtr bufferMem = IntPtr.Zero; //base address of the allocated region for the buffer
            IntPtr written= IntPtr.Zero;  //number of bytes written to memory
            IntPtr retHandle= IntPtr.Zero;
            bool retVal;

            
            //creating and reading from a shared memory region is done differently in Win9x then in newer OSs
            IntPtr processHandle= IntPtr.Zero;
            IntPtr fileHandle= IntPtr.Zero;

            if(!(Environment.OSVersion.Platform == PlatformID.Win32Windows))
            {   
                try
                {
                    uint size; //the amount of memory to be allocated
                    size = 65536;

                    processHandle = NativeMethods.OpenProcess(NativeMethods.PROCESS_VM_OPERATION | NativeMethods.PROCESS_VM_READ | NativeMethods.PROCESS_VM_WRITE, false, GetProcessIdFromHWnd(hwnd));

                    if(processHandle.ToInt64() == 0)
                    {
                        throw new Win32Exception();
                    }

                    bufferMem = NativeMethods.VirtualAllocEx(processHandle, IntPtr.Zero, new UIntPtr(size), NativeMethods.MEM_RESERVE | NativeMethods.MEM_COMMIT, PageProtection.ReadWrite);

                    if(bufferMem.ToInt64() == 0)
                    {
                        throw new Win32Exception();
                    }

                    //send message to the control's hWnd for getting the specified control name
                    retHandle = NativeMethods.SendMessage(hwnd, msg, new IntPtr(size), bufferMem);

                    //now read the TVITEM's info from the shared memory location
                    retVal = NativeMethods.ReadProcessMemory(processHandle, bufferMem, bytearray, new UIntPtr(size), written);
                    if(!retVal)
                    {
                        throw new Win32Exception();
                    }
                }
                finally
                {
                    //free the memory that was allocated
                    retVal = NativeMethods.VirtualFreeEx(processHandle, bufferMem, new UIntPtr(0), NativeMethods.MEM_RELEASE);
                    if(!retVal)
                    {
                        throw new Win32Exception();
                    }
                    NativeMethods.CloseHandle(processHandle);
                }
            }
            else
            {
                try
                {
                    int size2; //the amount of memory to be allocated
                    size2 = 65536;

                    fileHandle = NativeMethods.CreateFileMapping(new IntPtr(NativeMethods.INVALID_HANDLE_VALUE), IntPtr.Zero, PageProtection.ReadWrite, 0, size2, null);
                    if(fileHandle.ToInt64() == 0)
                    {
                        throw new Win32Exception();
                    }
                    bufferMem = NativeMethods.MapViewOfFile(fileHandle, NativeMethods.FILE_MAP_ALL_ACCESS, 0, 0, new UIntPtr(0));
                    if(bufferMem.ToInt64() == 0)
                    {
                        throw new Win32Exception();
                    }
                    NativeMethods.MoveMemoryFromByte(bufferMem, ref bytearray[0], size2);

                    retHandle = NativeMethods.SendMessage(hwnd, msg, new IntPtr(size2), bufferMem);

                    //read the control's name from the specific shared memory for the buffer
                    NativeMethods.MoveMemoryToByte(ref bytearray[0], bufferMem, 1024);

                }
                finally
                {
                    //unmap and close the file
                    NativeMethods.UnmapViewOfFile(bufferMem);
                    NativeMethods.CloseHandle(fileHandle);
                }
            }

            //get the string value for the Control name
            return ByteArrayToString(bytearray);

        }

        private static uint GetProcessIdFromHWnd(IntPtr hwnd)
        {
            uint pid;
            
            NativeMethods.GetWindowThreadProcessId(hwnd, out pid);

            return pid;
        }

        private static string ByteArrayToString(byte[] bytes)
        {
            if(Environment.OSVersion.Platform == PlatformID.Win32Windows)
            {
                // Use the Ansii encoder
                return Encoding.Default.GetString(bytes).TrimEnd('\0');
            }
            else
            {
                // use Unicode
                return Encoding.Unicode.GetString(bytes).TrimEnd('\0');
            }
        }
    }
}
Posted by brianmcm | 13 Comments

Who am I and why should anyone care?

The "why should you care?" part is much harder to answer, so I'll just start with who I am =).  I'm Brian McMaster, and I'm a Test Architect at Microsoft.  I'm currently working on "Cider."  Cider is the codename for the line of business application designer for Windows Presentation Foundation (aka Avalon) apps.  I will continue to call WPF, "Avalon," because it's a much more elegant name and easier to type =).  Avalon is an extremely cool framework, and Cider is shaping up to be an awesome designer for these types of applications.  Some of the other products I have worked on in the past are Access, Visual Basic, Visual InterDev, and Windows Forms. 

I've been working at Microsoft full-time for almost exactly 9 years (excluding a few internships that I worked while in college).  In fact, that reminds me that I need to buy a bunch of M & M's.  There's a silly tradition at Microsoft where you put x number of pounds of M&M's outside your office where x is equal to the number of years you've been working there.  At 10 years, there's another silly tradition where the company gives you 10 shares of Microsoft stock.  Of course, since the stock experts have told me that Microsoft is more of a value stock now instead of a growth stock, this means that my 10 shares will not appreciate in the way that shares have in the past.

When I first started working here, there were daily conversations about stock prices amongst the employees.  Sample applications for app-writing contests often involved writing the coolest ticker controls, or stock option vesting applications.  Back then the 10 shares might have become exceptionally valuable in short order due to splits.  10-20-40-80-160, possibly several thousand dollars worth =).  Ah, but we must not give up.  With XBox already out, and Vista coming out, and a new Office, and of course Visual Studio + Cider, we have a lot of potential.

So getting back to why you might care.  My main expertise at Microsoft has revolved around automated testing.  I've worked on various automated testing tools for technologies like HTML, Win32, Office, Windows Forms, and now Avalon.  Windows Forms has some subtle (and some not-so-subtle) differences from Win32 which makes it tricky to automate your applications.  So far, we've done a less than optimal job of helping our customers automate the testing of their Windows Forms applications.  This is something we're trying to remedy as quickly as possible with our next version of Visual Studio Team System.  In order to help explain the diferences and offer some guidance for our customers on how they might adapt their existing automated tools to Windows forms, I wrote this MSDN article.  http://msdn2.microsoft.com/en-us/library/ms996405.aspx.  It's certainly not a great article, but it is pretty good at describing the Windows Forms technology from an automation perspective.

I have a few more blog entries in mind right now, so hopefully, I'll be able to provide some useful tips and articles in the future.

-Bri

Posted by brianmcm | 0 Comments
 
Page view tracker