Friday, April 27, 2007

Elevating Tasks in Vista

One of the Stonefield Query dialogs has a command button that allows the user to launch the ODBC Administrator, likely because they need to set up or modify a DSN. The button has "ODBC Admin" as its Caption and the following code in Click:
lcSystem = Thisform.oUtility.GetSystemDirectory()
try
run /n1 &lcSystem..odbcad32.exe
catch
endtry
(GetSystemDirectory is a utility method that returns the location of the Windows System folder; I'll show the code for that later.)

This worked great until I tried it on Vista; it fails there because the ODBC Administrator requires admin privileges and the RUN command doesn't support requesting elevation. Fortunately, there's an easy solution to that: use ShellExecute instead. I changed the code to:
lcSystem = oUtility.GetSystemDirectory()
try
oUtility.ShellExecute(lcSystem + 'odbcad32.exe', 'RunAs')
catch
endtry
(Like GetSystemDirectory, ShellExecute is another utility function. It runs the specified command. The cool thing about ShellExecute is that it'll launch the appropriate program. In the case of an EXE, it runs it. In the case of an HTML file, it launches the default browser. In the case of a DOC file, it launches Word.)

Now when the user clicks the button, they're requested to elevate to administrator before the ODBC Administrator appears. To make it obvious that this will happen, I've followed the Vista convention of adding a shield image to the button to alert the user that elevation will be requested. To do that, I set PicturePosition to 1-Left of Caption, Picture to VistaShield.BMP (an image I created using SnagIt to grab the shield icon displayed in the User Accounts control panel applet), and shortened Caption to just "ODBC" so it all fits. Here's what the button looks like:



Here's the code for GetSystemDirectory:
#define cnMAX_PATH 260
#define ccNULL chr(0)
local lcBuffer
lcBuffer = space(cnMAX_PATH)
declare integer GetSystemDirectory in Win32API ;
string @szBuffer, integer nLength
GetSystemDirectory(@lcBuffer, cnMAX_PATH)
return addbs(alltrim(left(lcBuffer, at(ccNULL, lcBuffer) - 1)))
Here's the code for ShellExecute:
lparameters tcFileName, ;
tcOperation, ;
tcWorkDir, ;
tcParameters
local lcFileName, ;
lcWorkDir, ;
lcOperation, ;
lcParameters, ;
lnShow
if empty(tcFileName)
return -1
endif empty(tcFileName)
lcFileName = alltrim(tcFileName)
lcWorkDir = iif(vartype(tcWorkDir) = 'C', alltrim(tcWorkDir), '')
lcOperation = iif(vartype(tcOperation) = 'C' and not empty(tcOperation), ;
alltrim(tcOperation), 'Open')
lcParameters = iif(vartype(tcParameters) = 'C', alltrim(tcParameters), '')
lnShow = iif(upper(lcOperation) = 'Print', 0, 1)
declare integer ShellExecute in SHELL32.DLL ;
integer nWinHandle, ; && handle of parent window
string cOperation, ; && operation to perform
string cFileName, ; && filename
string cParameters, ; && parameters for the executable
string cDirectory, ; && default directory
integer nShowWindow && window state
return ShellExecute(0, lcOperation, lcFilename, lcParameters, lcWorkDir, ;
lnShow)

4 comments:

Anonymous said...

Hi Doug,
i had to make a few minor modifications to make your code running in my environment, so here you are:
LOCAL lcBuffer, liPathLength, lcNull
liPathLength = 250
lcBuffer = SPACE(liPathLength)
lcNull = CHR(0)

DECLARE integer GetSystemDirectory in Win32API string @szBuffer, integer nLength
GetSystemDirectory(@lcBuffer, liPathlength)

RETURN ADDBS(ALLTRIM(LEFT(lcBuffer, AT(lcNULL, lcBuffer) - 1)))

In 'shellexecute' were two letters missing for 'if empty(tcfilename)'

after these changes everything works fine. A really neat function which is already implemented within my config-app.

Thanks, Tom

Doug Hennig said...

Tom, I don't see the fix you needed for the filename; it does appear as tcFileName in the code I posted. However, you reminded me that the GetSystemDirectory method uses a couple of constants which I hadn't specified in the code, so I edited the blog entry to include them. Thanks!

BethW said...

Hi Doug,

First I want to say thanks for all the help you've given us VFP folks for the transition to Vista. I would be totally lost if it weren't for you!

Could I use WScript.Shell to RUN a file? i.e.:
oWscript = CREATEOBJECT("WScript.Shell")
lcSetupFile = path to setup.exe
oWscript.Run(lcSetupFile)

The code works, and the user is prompted to "Cancel" or "Allow" which is fine with me. I was just wondering if there was some security reason (or something) for using ShellExecute over WScript.Shell.

Thanks!

Doug Hennig said...

Hi Beth.

Yes, WScript works fine too. The only thing to consider is that I believe it may be disabled on some locked down systems.

Doug