Eric's Technical Outlet

Learning the hard way so you don't have to

WiX: Add Browse for File Capability to Installer

Do you want to add the ability for a user to browse for a file to your WiX installer project? The problem is fairly straightforward, and according to my searches, a lot of people have solved it. Unfortunately, no one seems to want to publish it. Here’s how I solved it.

Also, if you’re looking for a way to have an external custom action update a text box, that’s here, too.

Clarification

Based on some messages that I’ve received, I’m not entirely certain that I made it clear what this is intended to do. I needed to include a file in the final installation that I do not have the rights to distribute. The installer that I built asks the user to browse for that file. WiX uses that selection to populate a property. A CopyFile element is then used to transfer that to the installation folder. I didn’t talk about my usage because all that the code I’ve presented here does is populate a WiX property. What you do with that property is up to you. I know that some people have had issues after that point. Also, even though everything I designed worked perfectly when I first wrote this, I recently recompiled my installer with a more recent version of WiX and the CopyFile portion no longer functions and doesn’t generate a message for me to troubleshoot. The property is still populated so I know that this code works as presented. I just can’t guarantee that whatever you want to do with the property is going to work.

What You Need

To make this work exactly as I did, you need the WiX toolset. The best place to go for that is to start on the official page at http://wixtoolset.org/ and follow their links. The version used in this post is 3.9 R2. You’ll also need Visual Studio 2013 Community Edition. The WiX toolset can automatically integrate itself into VC2013 to create skeleton WiX projects for you and compile them into functioning MSIs. Without that, you’ll have to do more manual work. I’ve included enough information to code the C++ Custom Action, but for anything else you’ll either need to install VS with WiX or follow the WiX documentation to build projects manually.

Edit October 12, 2015: Also works with VS2015 and WiX version 3.10. I did need to manually add “C:\Program Files (x86)\WiX Toolset v3.10\SDK\VS2015\inc” to my C++ Custom Action project’s include directories.

Create a Property

In your product.wxs file, create a property to hold the name of the source file. MSI won’t allow properties to be empty, so you have to give it a value. You can make it just a single space, if you want, or you could give it a starting potential value. I usually create this property right in the primary element so that I don’t have to use references to it, but you can place it anywhere you like. Example:

<Property Id="DLLSOURCE" Value="C:\temp\source.dll"/>

Design the Dialog Object

In your product.wxs (or any .wxs file you’re linking in), build a dialog box that contains a control of type “Edit” and a control of type “Button”. The Edit control must have its Property attribute set to the same property you created above. Example:

<Control Id="SourceFile" Type="Edit" X="20" Y="94" Width="260" Height="18" Property="DLLSOURCE"/>
<Control Id="BrowseSource" Type="PushButton" X="283" Y="94" Width="18" Height="18" Text="..."/>

Note: If the user manually changes anything in the Edit control, then the automatic update just stops working. Going to a different page in the dialog and returning shows that the property is updating. I haven’t yet found a way around this. Consider adding a Disabled=”yes” attribute

Create the Custom Action DLL

If you have WiX integrated into Visual Studio, this is very easy. In the existing solution that holds your WiX project, create a new C++ Custom Action project. In its stdafx.h file, include commdlg.h (also include strutil.h from the WiX toolset, as it will be needed for a utility function in the custom action):

// stdafx.h : include file for standard system include files,
// or project specific include files that are used frequently, but
// are changed infrequently
//

#pragma once

#include "targetver.h"

#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files:
#include <windows.h>
#include <strsafe.h>
#include <msiquery.h>

// WiX Header Files:
#include <wcautil.h>

// TODO: reference additional headers your program requires here
#include <commdlg.h>
#include "strutil.h"

Start with a little house cleaning. You probably don’t want your custom action to just be called “CustomAction1”. Start with the CustomAction.DEF file. Underneath EXPORTS, change CustomAction1 to be whatever you do want your custom action to be called. Near the top of the CustomAction.cpp file, change the CustomAction1 function so that its name matches what you used in the DEF file.

Now, you’re going to modify the CustomAction.cpp file so that it creates and prepares an OPENFILENAME object. You are then going to retrieve the value of the property that you created in the product.wxs file in order to “seed” this OPENFILENAME object. Then, you are going to call the Windows API function GetOpenFileName to present a dialog box for the user to find the file that you’re interested in. If the user selects a valid file, you’re going to populate the supplied property with this file name and location. If the user does not provide a valid file, you’re just going to leave the property alone. Finally, you’re going to give control back to MSI.

#include "stdafx.h"

UINT __stdcall BrowseForDll(MSIHANDLE hInstall)
{
    HRESULT hr = S_OK;
    UINT er = ERROR_SUCCESS;
    /* File name selection variables */
    HANDLE hExistingFile;
    WIN32_FIND_DATA fdExistingFileData;
    OPENFILENAME ofn;
    LPWSTR szSourceFileName;
    wchar_t szFoundFileName[MAX_PATH] = L"";

    hr = WcaInitialize(hInstall, "BrowseForDll");
    ExitOnFailure(hr, "Failed to initialize");

    WcaLog(LOGMSG_STANDARD, "Initialized.");

    hr = WcaGetProperty(L"DLLSOURCE", &szSourceFileName);
    ExitOnFailure(hr, "The expected property DLLSOURCE was not found.");

    /* Prepare variables */
    SecureZeroMemory(&fdExistingFileData, sizeof(fdExistingFileData));
    SecureZeroMemory(szFoundFileName, sizeof(szFoundFileName));
    SecureZeroMemory(&ofn, sizeof(ofn));

    /* Determine if the supplied file exists */
    // start by retrieving the property
    hExistingFile = FindFirstFile(szSourceFileName, &fdExistingFileData);
    if (INVALID_HANDLE_VALUE != hExistingFile)
    {
        StringCchCopy(szFoundFileName, sizeof(szFoundFileName), szSourceFileName);
        FindClose(hExistingFile);
    }

    /* Prepare OFN */
    ofn.lStructSize = sizeof(ofn);
    ofn.lpstrTitle = L"Locate DLL";
    ofn.hwndOwner = GetActiveWindow(); // can also use NULL to be a little bit safer, although the dialog won't be modal in that case
    ofn.lpstrFile = szFoundFileName;
    ofn.nMaxFile = sizeof(szFoundFileName);
    ofn.lpstrInitialDir = NULL;
    ofn.lpstrFilter = L"DLLs (*.dll)\;0;*.dll\;0;All files\;0;*.*\;0;";
    ofn.nFilterIndex = 1;
    ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;

    /* Present dialog to user to select a file */
    if (GetOpenFileName(&ofn))
    {
        // user selected a file; populate the property
        WcaSetProperty(L"DLLSOURCE", ofn.lpstrFile);
    }

LExit:
    ReleaseStr(szSourceFileName);
    er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
    return WcaFinalize(er);
}

// DllMain - Initialize and cleanup WiX custom action utils.
extern "C" BOOL WINAPI DllMain(
    __in HINSTANCE hInst,
    __in ULONG ulReason,
    __in LPVOID
    )
{
    switch(ulReason)
    {
    case DLL_PROCESS_ATTACH:
        WcaGlobalInitialize(hInst);
        break;

    case DLL_PROCESS_DETACH:
        WcaGlobalFinalize();
        break;
    }

    return TRUE;
}

Reference Your Custom Action

To make this all as painless as possible, you can have your WiX project depend on the Custom Action project, which ensures that it’s always using the latest version of that DLL.

Start by right-clicking on the WiX project in Visual Studio’s Solution Explorer and clicking Add Reference. Change to the Projects tab. In the list box at the top, highlight the name of your Custom Action project. Click Add. The project should then appear in the Selected projects and components list box at the bottom. Click OK and it should appear in the WiX project’s References section in the Solution Explorer tree.

Now, in the product.wxs file, first create a reference to the custom action DLL, then define a custom action that calls that DLL:


<Binary Id="BinaryBrowseForDLL" SourceFile="$(var.WiXBrowseForDLL.TargetPath)"/>
<CustomAction Id="BROWSEFORDLL" Execute="immediate" BinaryKey="BinaryBrowseForDLL" DllEntry="BrowseForDll"/>

In the above, the first line (Binary) is what tells MSI that it needs to include the DLL. The Id matters because that’s how the custom action will know what binary item to call. The SourceFile could be hardcoded if you like, but what we’re doing here takes advantage of the dependency that we created earlier. What I’ve entered as WiXBrowseForDLL is the name of my custom action project. Substitute that part for your project name, but leave the rest the same. This will allow the WiX compiler to automatically go find the latest version of your custom action without being told that it’s been rebuilt or that you’re switching between Debug and Release modes. Probably my favorite part is that if you modify your custom action files and forget to build that project, building this one will handle that one as well.

The CustomAction line wires up MSI to the custom action DLL. The Id is how you’ll call it from your button. Execute tells it to run immediately when the action is invoked. The BinaryKey is a reference to the Id of the Binary element in the previous line. The DllEntry is the function that you exported in your custom action’s DEF file.

Invoke the Custom Action

You’re almost there! All that’s left is to connect your button to the custom action. The best place to do this is inside the same element where you define the dialog, but outside of the dialog itself. You need that button to do two things and do them in a specific order. The first thing it needs to do is call your custom action. After that, it needs to let MSI know that the property has been updated. Otherwise, the Edit box won’t know that it’s supposed to change its display contents and the user will think the browse operation didn’t work:


<Publish Dialog="BrowseForFileDlg" Control="BrowseSource" Event="DoAction" Value="BROWSEFORDLL" Order="1">1</Publish>
<Publish Dialog="BrowseForFileDlg" Control="BrowseSource" Property="DLLSOURCE" Value="[DLLSOURCE]" Order="2">1</Publish>

Complete Custom Dialog Sample

This is a complete sample of a dialog that uses the above control:

<Fragment>
<UI Id="BrowseForFileDlg">
<Dialog Id="BrowseForFileDlg" Width="370" Height="270" Title="Locate DLL" KeepModeless="yes">
<Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="!(loc.InstallScopeDlgBannerBitmap)" />
<Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
<Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
<Control Id="Description" Type="Text" X="25" Y="23" Width="280" Height="20" Transparent="yes" NoPrefix="yes" Text="Enter the path to the necessary DLL" />
<Control Id="Title" Type="Text" X="15" Y="6" Width="200" Height="15" Transparent="yes" NoPrefix="yes" Text="{\WixUI_Font_Title}Locate DLL" />

<Control Id="SourceFileLabel" Type="Text" X="20" Y="60" Width="290" Height="32" NoPrefix="yes" Text="Specify the location of the necessary DLL:"/>
<Control Id="SourceFile" Type="Edit" X="20" Y="94" Width="260" Height="18" Property="DLLSOURCE"/>
<Control Id="BrowseSource" Type="PushButton" X="283" Y="94" Width="18" Height="18" Text="..."/>

<Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="!(loc.WixUIBack)"/>
<Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="!(loc.WixUINext)"/>
<Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.WixUICancel)"/>
</Dialog>
<Publish Dialog="BrowseForFileDlg" Control="BrowseSource" Event="DoAction" Value="BROWSEFORDLL" Order="1">1</Publish>
<Publish Dialog="BrowseForFileDlg" Control="BrowseSource" Property="DLLSOURCE" Value="[DLLSOURCE]" Order="2">1</Publish>
<Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="BrowseForFileDlg" Order="2">LicenseAccepted="1"</Publish>
<Publish Dialog="BrowseForFileDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg">1</Publish>
<Publish Dialog="BrowseForFileDlg" Control="Next" Event="NewDialog" Value="SetupTypeDlg">1</Publish>
<Publish Dialog="BrowseForFileDlg" Control="Cancel" Event="SpawnDialog" Value="CancelDlg">1</Publish>
<Publish Dialog="SetupTypeDlg" Control="Back" Event="NewDialog" Value="F5LBBrowseForiControlDlg">1</Publish>
</UI>
</Fragment>

Once that’s complete, you just need to add in a reference to this UI object so that MSI knows to load it; that’s usually in your primary element:

<UIRef Id="BrowseForFileDlg"/>

This particular example uses the WixUI_Mondo dialog set. This dialog shows up immediately after the license agreement. If you use a different set and copy/paste the above UI example, be mindful that you correct the button overrides near the end.

All Done!

That’s all that’s necessary to retrieve the file name. Any other actions can now just reference the DLLSOURCE property that you created.

Advertisements

7 responses to “WiX: Add Browse for File Capability to Installer

  1. JB Dufour September 30, 2016 at 2:47 pm

    Thanks for this clear and complete example. I have adapted it easily

    Like

  2. Matthew Liang June 14, 2016 at 10:16 am

    Thank You Eric, your blog post was very helpful.

    Like

  3. Linus Törnil October 23, 2015 at 8:52 am

    Hi. I tried this and I can’t get it to work. When I push the button to get the BrowseForDll the msi terminates. No error given.

    //Linus

    Like

    • Eric Siron October 23, 2015 at 8:59 am

      Turn on logging. MSIEXEC /i yourapp.msi /L*v c:\temp\install_log.txt

      Like

      • Linus Törnil October 26, 2015 at 1:37 am

        Hi. I turned on logging. This is the relevant section I got.

        Åtgärd 07:27:24: BrowseForFileDlg. Dialog created
        MSI (c) (94:E0) [07:27:25:566]: Doing action: BROWSEFORDLL
        MSI (c) (94:E0) [07:27:25:566]: Note: 1: 2205 2: 3: ActionText
        Åtgärd 07:27:25: BROWSEFORDLL.
        Åtgärd inleds 07:27:25: BROWSEFORDLL.
        MSI (c) (94:6C) [07:27:25:626]: Invoking remote custom action. DLL: C:\Users\linus\AppData\Local\Temp\MSI5E76.tmp, Entrypoint: BrowseForDll
        MSI (c) (94:CC) [07:27:25:628]: Cloaking enabled.
        MSI (c) (94:CC) [07:27:25:628]: Attempting to enable all disabled privileges before calling Install on Server
        MSI (c) (94:CC) [07:27:25:628]: Connected to service for CA interface.
        Åtgärd slutförs 07:27:25: BROWSEFORDLL. Returvärde: 3.
        MSI (c) (94:E0) [07:27:25:690]: Note: 1: 2205 2: 3: Error
        MSI (c) (94:E0) [07:27:25:690]: Note: 1: 2228 2: 3: Error 4: SELECT `Message` FROM `Error` WHERE `Error` = 2896
        DEBUG: Error 2896: Executing action BROWSEFORDLL failed.
        Ett oväntat fel uppstod när det här paketet installerades. Kanske har ett fel uppstått i paketet. Felkod: 2896. Argument: BROWSEFORDLL, ,
        Åtgärd slutförs 07:27:25: WelcomeDlg. Returvärde: 3.
        Åtgärd slutförs 07:27:25: INSTALL. Returvärde: 3.
        MSI (c) (94:34) [07:27:25:692]: Destroying RemoteAPI object.
        MSI (c) (94:CC) [07:27:25:693]: Custom Action Manager thread ending.

        Best regards
        Linus

        Like

      • Eric Siron October 26, 2015 at 9:54 am

        It’s tough to determine where the application is tripping up, but it appears that your DLL is returning error code 3 (The system cannot find the path specified) and your WiX application is not prepared for that. Did you do copy/paste of my code or did you modify it? Mine should handle a non-existent file.
        If it’s not that, then most of the links I’m finding for error code 2896 indicate that it is a security problem with the target file. Since the error is not clear, it could even mean that it is having a security problem reading your DLL, not the specified file.

        Like

      • Linus Törnil October 26, 2015 at 9:59 am

        Thank you for the repons. We took a different approch in just selecting the directory.

        Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: