📜 ⬆️ ⬇️

Custom Action in WiX

Custom Actions is one of the most important elements in WiX , allowing you to perform any actions in the process of installing or removing a program that expands the capabilities of WiX. With the help of Custom Action, we can connect to our installer VBScript, JScript, Dll library, executable module and perform any actions in the process of the installer.

Consider an example - during the installation of the program we need to specify the path to the file on the local computer, let's say the license file.

To begin with, we will create a project, add a window with a choice of the license file (so that the article is not filled with unnecessary code, I will only give you important code fragments in my opinion. You can download and view the sources of the project at the end of the article).

I created a project, excluded unnecessary windows from the installation process, added a new window in which the file was selected. As a result, the installer will contain the following windows:
')
1. Greetings

2. Select license file


3. Starting installation

4. End of installation

The project includes the following files:

Let's add the Custom Action to our project, for this we will add the CustomAction.wxs file to the project .

<? xml version ="1.0" encoding ="UTF-8" ? >
< Wix xmlns ="http://schemas.microsoft.com/wix/2006/wi" >
< Fragment >
< Binary Id ="BinBrowseForFile"
SourceFile ="BrowseForFile.vbs" />
< CustomAction Id ='BrowseForFile'
BinaryKey ="BinBrowseForFile"
VBScriptCall ="CallTheAction"
Return ="check" />
</ Fragment >
</ Wix >


* This source code was highlighted with Source Code Highlighter .

With the help of the Binary element, we can add files to our project that will not be installed on the target computer, but will participate in the process of the installer. The SourceFile parameter specifies the path to the file, in this case, BrowseForFile.vbs .

The CustomAction element describes some action performed by a script, a dynamic library, which we can perform during the installation / uninstall process. Consider its parameters:

BinaryKey is a reference to the Binary element.
VBScriptCall sets the name of the VBScript function to call.
Return in this case, check , indicates that the script will be called synchronously, and the returned value will be checked for success. The check value is the default value.

Other possible values ​​for Return :
asyncNoWait is an asynchronous call, the installer will not wait until the end of execution, which may occur even after the installer has completed. It can be useful when we want to run our program after the installation process is complete.
asyncWait is an asynchronous call, but, unlike the previous one, the installer will wait for completion.
Ignore is the same as check , but without checking the result.

The CustomAction element has a lot of other parameters, which list does not make sense here, a full description of the CustomAction element. You can find it here .

Understood, go to the contents of the file BrowseForFile.vbs

Function CallTheAction
Set objDialog = CreateObject( "UserAccounts.CommonDialog" )
objDialog.Filter = " |*.txt| |*.*"
objDialog.FilterIndex = 1
objDialog.InitialDir = "C:\"

Dim intResult : intResult = objDialog.ShowOpen

If intResult = 0 Then
CallTheAction = msiDoActionStatusUserExit
Exit Function
End If

Session. Property ( "LICENSE_FILE_PATH" ) = objDialog.FileName
CallTheAction = msiDoActionStatusSuccess
End Function


* This source code was highlighted with Source Code Highlighter .

This script displays a file selection dialog and returns the result. If the user specifies a file and clicks the Open button, the script will return the path to the file by setting the value of the LICENSE_FILE_PATH variable defined in the Product.wxs file

< Property Id ="LICENSE_FILE_PATH" > C:\ </ Property >

For the CustomAction call, we will have the Browse button, defined in the WixUI_License.wxs file .

< Control Id ="ButtonBrowse"
Type ="PushButton"
...
Text ="" >
< Publish Event ="DoAction" Value ="BrowseForFile" Order ="1" > 1 </ Publish >
</ Control >


* This source code was highlighted with Source Code Highlighter .

The Publish element, in this case, is responsible for the reaction for pressing the button. The value of the Event (DoAction) parameter indicates the need to call CustomAction with the identifier specified in the Value parameter (BrowseForFile).

Now we need to output the value of the LICENSE_FILE_PATH variable that stores the path to our file. The display will deal with the text field:

< Control Id ="TextLicensePath"
Type ="Edit"
...
Property ="LICENSE_FILE_PATH" >
</ Control >


* This source code was highlighted with Source Code Highlighter .

If we now assemble and run the installer, then after clicking the Browse button and selecting a file, we will see that our text field is not updated.



In order to understand the cause of the problem, click the Back button, then Next , the field has been updated. Again, select the file and “cover” our window with another window, then return it. The field has been updated again. Yeah, so we did everything right, and the problem is that the UI does not update the field contents when the Property changes. What to do?

One option is to create a duplicate LicenseDlg window and switch between these windows when you click the Browse button. The implementation is simple. Create a copy of the file WixUI_License.wxs , call it, for example, WixUI_License2.wxs . Rename the LicenseDlg dialog to LicenseDlg2 . In the script wizard add:

< Publish Dialog ="LicenseDlg2" Control ="Back" Event ="NewDialog" Value ="WelcomeDlg" > 1 </ Publish >
< Publish Dialog ="LicenseDlg2" Control ="Next" Event ="NewDialog" Value ="VerifyReadyDlg" > 1 </ Publish >

< Publish Dialog ="LicenseDlg" Control ="ButtonBrowse" Event ="NewDialog" Value ="LicenseDlg2" > 1 </ Publish >
< Publish Dialog ="LicenseDlg2" Control ="ButtonBrowse" Event ="NewDialog" Value ="LicenseDlg" > 1 </ Publish >

* This source code was highlighted with Source Code Highlighter .

The first two lines duplicate the behavior of the dialog LicenseDlg , the second are responsible for the transitions between duplicate windows.

We start, check, work. But I absolutely do not like this method. Duplicate code, and the need to delete, copy, rename the dialogue every time you make changes to its structure.

Now, if it were possible to somehow “tie” C ++ here, I would instantly figure out how to update the window ... But you can. As it was already written at the beginning of the article, we can use Dll libraries as sources for CustomAction . I will say more, a ready-made library with source codes, for our task, is available on the site www.installsite.org .

If you look at the sources of this library, we see that the BrowseForFile function takes the initial value of the file path from the PATHTOFILE property, displays the file selection dialog and, if successful, saves the received path value to the PATHTOFILE . Then it finds the window that needs to be updated, by the name of the Default window - the InstallShield Wizard , finds in it an element with the name of the class RichEdit20W and sets it with the text equal to the selected path to the file.

You can take this library, correct the search window, filter in the file selection dialog and connect to your project. And you can, based on this code, write your own, more universal library. In my opinion, the library should be able to accept as an input parameter a filter for the file selection dialog and correctly find the installer window.

Let's create a library for our project. To do this, add a new project to the project ( File - Add - New Project ). Project Type Win32 Project .





We connect to the project library Msi.lib



Next, we will create a file with a description of the exported functions, we will immediately agree that ours will be called BrowseForFile

LIBRARY "BrowseForFile"
EXPORTS
BrowseForFile
There is a second, simpler way to create a Custom Action in C ++. To do this, select the new project in the window for selecting a new project.



But for the first time it is better to create everything with your hands in order to understand how it works, and only then you can trust the creation to the master.

Create a BrowseForFile function

UINT __stdcall BrowseForFile(MSIHANDLE hInstall)
{
long lErrMsg = 0;

TCHAR szOriginalPath[MAX_PATH] = {0};
TCHAR szDialogFilter[MAX_PATH] = {0};
TCHAR szIndex[8] = {0};

DWORD cchValue;

// BFF_PATH_TO_FILE
cchValue = _countof(szOriginalPath);
MsiGetProperty(hInstall, TEXT( "BFF_PATH_TO_FILE" ), szOriginalPath, &cchValue);

// BFF_FILE_DIALOG_FILTER
cchValue = _countof(szDialogFilter);
MsiGetProperty(hInstall, TEXT( "BFF_FILE_DIALOG_FILTER" ), szDialogFilter, &cchValue);

size_t nFilterLength = wcslen(szDialogFilter);

for (size_t i = 0; i < nFilterLength; ++i)
{
if (szDialogFilter[i] == '|' )
{
szDialogFilter[i] = '\0' ;
}
}

OPENFILENAME ofn = {0};

// OPENFILENAME.
ofn.lStructSize = sizeof (ofn);
ofn.hwndOwner = GetForegroundWindow();
ofn.lpstrFile = szOriginalPath;
ofn.nMaxFile = _countof(szOriginalPath);
ofn.lpstrFilter = szDialogFilter;
ofn.nFilterIndex = 0;
ofn.lpstrFileTitle = NULL;
ofn.nMaxFileTitle = 0;
ofn.lpstrInitialDir = NULL;

if (GetOpenFileName(&ofn))
{
// .
MsiSetProperty(hInstall, TEXT( "BFF_PATH_TO_FILE" ), szOriginalPath);
}

return ERROR_SUCCESS;
}


* This source code was highlighted with Source Code Highlighter .

At this stage, our library can do everything the same as the script did. Add the ability to update the field. In the example, the search was performed by the name of the window, in my opinion this is not correct. From a project to a project, the name of the window may change, I would like to find a universal way. Having set myself such a task, I decided to check, the first thing that came to mind - and what class do installer windows have? Looking at the installer window information using the Spy ++ utility (included in Visual Studio), I found that the window has the class MsiDialogCloseClass . Here on it will rely. Add the code:

// .
BOOL CALLBACK EnumChildProc(HWND hWnd, LPARAM lParam)
{
TCHAR szBuffer[100] = {0};

GetClassName(hWnd, (LPTSTR)&szBuffer, _countof(szBuffer));

if (_wcsicmp(szBuffer, (_T( "RichEdit20W" ))) == 0)
{
// .
*(HWND*)lParam = hWnd;

return FALSE;
}

return TRUE;
}


* This source code was highlighted with Source Code Highlighter .

And in the BrowseForFile function, inside the condition:

if (GetOpenFileName(&ofn))

add:
// .
HWND hInstallerWnd = FindWindow(_T( "MsiDialogCloseClass" ), NULL);

if (hInstallerWnd != NULL)
{
HWND hWndChild = NULL;

EnumChildWindows(hInstallerWnd, EnumChildProc, (LPARAM)&hWndChild);

if (hWndChild != NULL)
{
SendMessage(hWndChild, WM_SETTEXT, 0, (LPARAM)szOriginalPath);
}
}


* This source code was highlighted with Source Code Highlighter .

Everything is ready to use our library in the installation package. In the file Product.wxs, we will declare two new properties.
< Property Id ="BFF_PATH_TO_FILE" ></ Property >
< Property Id ="BFF_FILE_DIALOG_FILTER" > (*.*)|*.*| (*.txt)|*.txt|| </ Property >

Through the BFF_PATH_TO_FILE property , our Dll library will “communicate” with the installer.
The BFF_FILE_DIALOG_FILTER property contains a filter for the file selection dialog.

Let's change the description of CustomAction to:

< Binary Id ="BinBrowseForFile" SourceFile ="..\Debug\BrowseForFile.DLL" />
< CustomAction Id ="BrowseForFile"
BinaryKey ="BinBrowseForFile"
DllEntry ="BrowseForFile"
Return ="check" />


* This source code was highlighted with Source Code Highlighter .

And we will add a new CustomAction , which will assign the LICENSE_FILE_PATH property we need to the value received as a result of the Dll call ( BFF_PATH_TO_FILE )

< CustomAction Id ='AssignPathToProperty'
Property ='LICENSE_FILE_PATH'
Value ='[BFF_PATH_TO_FILE]' />


* This source code was highlighted with Source Code Highlighter .

The last thing left to do is change the code responsible for pressing the button, we will add to it a call to the new CustomAction

< Control Id ="ButtonBrowse"
...
Text ="" >
< Publish Event ="DoAction" Value ="BrowseForFile" Order ="1" > 1 </ Publish >
< Publish Event ="DoAction" Value ="AssignPathToProperty" Order ="2" > 1 </ Publish >
</ Control >


* This source code was highlighted with Source Code Highlighter .

Done, you can collect and check.

Custom Action can be called not only in response to any action from the user, but also automatically, in the process of the installer. Let's look at a simple example in which we will need to create the Config folder in the program folder during the installation process, and delete this folder during the uninstall process (there is another way to do the same using the child Directory section. In this case, this method was chosen for the sake of ).

Start by creating a folder during the installation process. Create a new CustomAction :

< CustomAction Id ="CreateConfigFolder" Script ="vbscript" >
<! [CDATA[
On Error Resume Next
Set objFso = CreateObject("Scripting.FileSystemObject")
strFolderPath = Session.Property("INSTALLLOCATION") & "\Config"
objFso.CreateFolder(strFolderPath)
]] >
</ CustomAction >


* This source code was highlighted with Source Code Highlighter .

As you can see from the example, the script code can be written directly into the body of the CustomAction element, while there is no need to create the Binary element and refer to it.

To delete a folder during the removal process:

< CustomAction Id ="RemoveConfigFolder" Script ="vbscript" >
<! [CDATA[
On Error Resume Next
Set objFso = CreateObject("Scripting.FileSystemObject")
strFolderPath = Session.Property("INSTALLLOCATION") & "\Config"
objFso.DeleteFolder(strFolderPath)
]] >
</ CustomAction >


* This source code was highlighted with Source Code Highlighter .

Actions are added, now someone has to call them and run. Add the following code to the Product.wxs file:

< InstallExecuteSequence >
< Custom Action ="CreateConfigFolder" After ="InstallFinalize" > Not Installed </ Custom >
< Custom Action ="RemoveConfigFolder" Before ="RemoveFiles" > Installed </ Custom >
</ InstallExecuteSequence >


* This source code was highlighted with Source Code Highlighter .

The InstallExecuteSequence section allows you to place within yourself other partitions, which you can specify when they will be used. In our example, the folder will be created after the InstallFinalize stage is completed (installation is completed), and the deletion is done before the RemoveFiles stage (file deletion).

Notice that the Custom sections contain some text inside. These are logical conditions that determine whether an action will be performed or not. Installed is a sign that the product is already installed. Those. CreateConfigFolder will work if the product is not yet installed. RemoveConfigFolder if the product is installed, i.e. in the process of removing the program.

That's all, I hope it was not too boring. If you have questions, feel free to ask.

Project sources
Binary Dll-ki with brief instructions for use

Links to previous articles: Part 1 (getting started) , Part 2 (project organization) , Part 3 (custom windows)

Source: https://habr.com/ru/post/71917/


All Articles