Click here to Skip to main content
Click here to Skip to main content

Using P/Invoke to Automate Database Signon

, 24 Aug 2004
Rate this:
Please Sign up or sign in to vote.
Using P/Invoke to automate database signon.

Sample Image - InteropSignon.jpg

Introduction

I recently was faced with the challenge of automating a reporting system at work. The problem I ran into was that the database which holds the business data (MAS 90, a popular accounting software package) prompts for logon credentials each time an ODBC connection is opened.

In addition to user name and password, a three character company code is expected at logon (our system is configured to handle payroll, accounts payable, accounts receivable, and job costing for several separate companies), and I couldn't find a way to make it work by simply altering the connection string. Therefore, I had to resort to something slightly more complicated.

That something turned out to be interacting with the native Windows controls on the MAS 90 database login dialog using Platform Invoke.

I want to add that there is most likely one or more other (and quite possibly better) ways to go about this. To be honest, my solution sort of feels like a kludge. It's kind of like picking the lock on your front door every day instead of having a key made. But, you'll end up inside the house either way.

Using the code

Since I was targeting a single known window, my first step was to use the excellent Spy++ tool that ships with Visual Studio .NET to find out how the controls on the logon screen fit together.

The logon screen contains ten controls: two ComboBoxes, one Edit control (TextBox), four Static controls (three Labels and one icon), and three Buttons (only two are recognizable as Buttons).

The four controls we are concerned with are the two ComboBoxes, the TextBox, and the first Button. The order of the controls on the form is important as we'll find out a little later.

In order to get a hold of the main window, we use the FindWindow API, which is defined in user32.dll. We use the DllImportAttribute to declare it in our C# code. We'll also need FindWindow's companion function, FindWindowEx, to locate the controls within the main window, so we'll declare that as well.

using System.Runtime.InteropServices;

[DllImport("user32.dll")]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

[DllImport("user32.dll")]
static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, 
    string lpszClass, string lpszWindow);

I've wrapped both (among others we'll come to later) functions in an overloaded function. Wrapping Windows API functions when using them from managed code is generally the recommended practice. It also allows me to use a stateful object to deal with the windows, rather than keeping track of a bunch of pointers and calling functions in a completely procedural (read non-OO) way.

public class WindowFinder
{
    private IntPtr _handle;

    public WindowFinder(){}

    public static IntPtr Find(string className, string text)
    {
        return FindWindow(className, text);
    }

    public static IntPtr Find(IntPtr parent, 
       IntPtr lookAfter, string className, string text)
    {
        return FindWindowEx(parent, lookAfter, className, text);
    }

    public IntPtr Handle
    {
        get{ return _handle; }
    }

    //...
    //rest of class
}

If a window with the given class name and text is found, its handle is returned. If no window matching the description is found, the return value is equal to IntPtr.Zero. After obtaining the handle to the main window, we can use FindWindowEx to locate the actual controls that we'll be manipulating.

WindowFinder mainWindow = new WindowFinder();
WindowFinder companyWindow = new WindowFinder();
WindowFinder userWindow = new WindowFinder();
WindowFinder passwordWindow = new WindowFinder();
WindowFinder okButtonWindow = new WindowFinder();

//Get the main logon screen
mainWindow.Find("#32770", "MAS 90 and MAS 200 Database Signon");

//Now we can use the main window as the first 
//parameter to find the child windows
//We'll use IntPtr.Zero as the second parameter 
//for the first child window, since
//it's the first one we're looking for
companyWindow.Find(mainWindow.Handle, 
              IntPtr.Zero, "ComboBox", "");

//Now each call to WindowFinder.Find will use 
//the handle to window we just found
//as the second parameter.  Otherwise, we'd end up 
//finding the same window again
userWindow.Find(mainWindow.Handle, 
     companyWindow.Handle, "ComboBox", "");

passwordWindow.Find(mainWindow.Handle, 
     userWindow.Handle, "Edit", "");

okButtonWindow.Find(mainWindow.Handle, 
     passwordWindow.Handle, "Button", "&OK");

Now that we have the handles to all our controls wrapped up, all that's left to do is input the proper values into the ComboBoxes and Textbox and "click" the Button.

We need to declare a couple more functions (and some constants) from our Windows API grab bag in order to accomplish this. Here they are:

private const int SEARCH_ALL = -1;
private const int CB_SELECTSTRING = 0x014D; //ComboBox
private const int WM_SETTEXT = 0x000C; //TextBox
private const int BM_CLICK = 0x00F5; //Button

[DllImport("user32.dll")]
private static extern bool SetWindowText(IntPtr hWnd, string lpString);

//We need two overloads of SendMessage for our purposes

[DllImport("user32.dll", CharSet=CharSet.Auto)]
static extern IntPtr SendMessage(IntPtr hWnd, 
          uint Msg, int wParam, string lParam);

[DllImport("user32.dll", CharSet=CharSet.Auto)]
static extern IntPtr SendMessage(IntPtr hWnd, 
          uint Msg, int wParam, int lParam);

//and I'll add the wrappers to the WindowFinder class
public void SelectComboItem(string text)
{
    SendMessage(this.Window, CB_SELECTSTRING, SEARCH_ALL, text);
}

public void SetEditText(string text)
{
    SendMessage(this.Window, WM_SETTEXT, 0, text);
}

public void ClickButton()
{
  SendMessage(this.Window, BM_CLICK, 0, 0);
}

That last bit of code makes one of the possible improvements to this solution quite obvious. The WindowFinder class contains methods to operate on ComboBoxes, TextBoxes, and Buttons. Obviously, a window can't be all three of these things at once, so two of them are always going to be invalid. I did it this way because it was quick, but a better solution might be to design a base class that encapsulates common functionality, and derive from it for specific controls.

Anyway, it's pretty trivial from here to finish the job. Here's the rest of the code for dealing with the logon screen:

//company, user, and password are strings defined elsewhere in the program.
//In this particular case, they are retrieved from the app's config file

companyWindow.SelectComboboxItem(company);

userWindow.SelectComboItem(user);

passwordWindow.SetTextBoxText(password);

okButtonWindow.ClickButton();

With that, the logon is accepted and the connection to the database is opened.

One small hurdle still remains before the code can function properly: The call to open the connection is what triggers the logon screen to appear, but it's a blocking call, so it doesn't return until the credentials have been accepted. To get around this, I simply create a separate thread for the code I've just shown to run on and then call OdbcConnection.Open from the main thread.

Thread t = new Thread(new ThreadStart(RunCredentialAutomator));
t.Start();

_connection.Open();

...

private void RunCredentialAutomator()
{
    ...
    
    IntPtr invalidHandle = IntPtr.Zero;
    do
    {
        mainWindow.Find("#32770", "MAS 90 and MAS 200 Database Signon");
    }while(mainWindow == invalidHandle);
    ...
}

Points of Interest

The most difficult part of .NET Interop is simply knowing the function definitions and what messages to send. For the function definitions, I highly recommend http://www.pinvoke.net/. There you can find hundreds of PInvoke signatures that you can simply copy and paste into your app, as well as tips on using them. The site is a work-in-progress and new information is being added constantly. The site is a wiki, so anyone can contribute.

For what messages are sent and received by individual controls, there is no better source of information than MSDN. For example, to find out how to control ComboBoxes, this is where you'd look. You will likely also have to spelunk through the header files on your machine to find the values for constants used.

This code is used with MAS 90 version 3.71. It may not work with other versions if the logon screen is modified.

The attached source code is for the DLL in which this code resides, but the reporting application that uses it it not included.

This code will not be directly reusable to most people because you are most likely not using MAS 90. I hope somebody will be able to get some use out of it as an example.

History

This is the first version. I'll gladly make changes if (when) someone points where I went wrong.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Charlie Williams
Web Developer
United States United States
No Biography provided

Comments and Discussions

 
GeneralThanks!! Very helpful code! PinmemberMember 203642124-Feb-09 9:25 
GeneralSet the check box and hide it Pinmemberyetty20001-Oct-07 5:56 
GeneralNeed help with Password Pinmemberstaffan120-Dec-05 5:11 
GeneralFindWindow PinmemberCabbi24-Nov-05 20:24 
Questionhow to invoke database from my application Pinsussyeyet25-Sep-05 18:00 
GeneralCode In Zip Archive Doesn't Match Article PinmemberRFID Chris19-May-05 13:14 
GeneralRe: Code In Zip Archive Doesn't Match Article PinmemberCharlie Williams22-May-05 13:36 
QuestionHow can I use this to display a folders security? Pinmemberid10t23-Mar-05 8:04 
QuestionKludge? Pinmemberjeff briggs31-Aug-04 4:56 
GeneralVery Cool! Pinmemberwconnors25-Aug-04 4:35 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140709.1 | Last Updated 25 Aug 2004
Article Copyright 2004 by Charlie Williams
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid