Google Analytics

Friday, March 1, 2013

More Custom Actions with WiX


The tutorial gave a relatively simple action to check a product key on install. For my own project I would like to ask the user to specify a remote service uri, check that the uri format is okay, warn the user if it is not, and finally write the value to my application’s configuration file.

First of all, my installer had no user interface at all, so I added the WixUI_FeatureTree as I described in a previous post. As in my tutorial, I am going to add the new custom action code by right-clicking the solution and doing Add > New Project and selecting Windows Installer XML > C# Custom Action Project and I will name it CustomInstallActions.

 The custom action will read in what the user input as SERVICEURI and will output a URIACCEPTED value to the install session.  For right now I am simply testing whether the string that is passed in can be used to form a URI.  The CustomAction attribute decorating the class indicates the method is an entry point for the installer custom action.

public class CheckUriAction
    {
        /// 
        ///     Checks that user input is a well-formed URI.
        /// 
        /// 
        ///     The installation session.
        /// 
        /// 
        ///     The . This will always be success so
        ///     long as the evaluation can be made. It does not indicate
        ///     whether the URI is well-formed or not.
        /// 
        [CustomAction]
        public static ActionResult CheckUri(Session session)
        {
            // log that custom action is being called.
            session.Log("Checking user input URI");
            
            // get the user string input from the install session
            var uri = session["SERVICEURI"];
            
            // check that the string passed in can be used to construct a valid URI
            var isURI = Uri.IsWellFormedUriString(uri, UriKind.Absolute);
            
            // write the evaluation back to the installation session
            session["URIACCEPTED"] = isURI ? "1" : "0";
            
            // return succes. This allows the installation to continue and the
            // user will be able to change the input if necessary. A different
            // ActionResult would cause the installation to end.
            return ActionResult.Success;
        }

To my Product.wxs WiX file I add a Fragment to reference this custom action and the dll that contains it. As I explained in a previous post, the C# custom action gets wrapped by [someEXE] into a managed custom action. This is handled automatically if you use the WiX C# CustomAction project, so the actual dll we reference is the wrapped one that has the .CA.dll extension.

<Fragment>
    <CustomAction Id='CheckingUri' BinaryKey='CheckUri.CA' DllEntry='CheckUri' />
    <Binary Id='CheckUri.CA' SourceFile='..\..\..\CheckURIAction\bin\Debug\CustomInstallActions.CA.dll' />
</Fragment>

The DllEntry attribute of the CustomAction is the name of the method that we designated as the entry point in our class. The Id of the CustomAction element is what we will use to reference this in our installation UI.

This UI is a new dialog I add to the WiX project that will ask for the user’s input.  By right-clicking on the WiX project and selecting Add > New Item > WiX > WiX File I’m given the skeleton of a WiX fragment. To this I add a UI component and within that a Dialog I will call ServiceDlg. If you followed the WiX tutorial you’ve done this. The key controls to look at are the “NameEdit” Edit control and its property called SERVICEURL and the “Next” PushButton control. When the user pushes the Next button it will “Publish” the CheckingUri action we specified in the fragment above. The value of SERVICEURL will be passed through the session to the action. In turn the action adds the URIACCEPTED property to the session, which we evealute to determine which dialog to show to the user. In the case there the URI is not accepted, we show the InvalidURIDlg that we will add below. If everything is ok we allow the user to move on to the CustomizeDlg dialog which is already built in to the WixUI_FeatureTree set.

<Dialog Id="ServiceDlg" Width="370" Height="270" Title="[ProductName] Setup" NoMinimize="yes">
        <Control Id="ServiceLabel" Type="Text" X="45" Y="73" Width="100" Height="15" TabSkip="no" Text="&EED Service Address:" />
        <Control Id="NameEdit" Type="Edit" X="45" Y="85" Width="220" Height="18" Property="SERVICEURI" Text="{80}" />
        <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="&Back">
          <Publish Event="NewDialog" Value="LicenseAgreementDlg">1</Publish>
        </Control>
        <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="&Next">
          <Publish Event="DoAction" Value="CheckingUri">1</Publish>
          <Publish Event="SpawnDialog" Value="InvalidURIDlg">URIACCEPTED = "0"</Publish>
          <Publish Event="NewDialog" Value="CustomizeDlg">URIACCEPTED = "1"</Publish>
        </Control>
        <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="Cancel">
          <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
        </Control>
        <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="WixUI_Bmp_Banner" />
        <Control Id="Description" Type="Text" X="25" Y="23" Width="280" Height="15" Transparent="yes" NoPrefix="yes">
          <Text>[ProductName] needs access to the DVS Core Service</Text>
        </Control>
        <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
        <Control Id="Title" Type="Text" X="15" Y="6" Width="200" Height="15" Transparent="yes" NoPrefix="yes">
          <Text>{\WixUI_Font_Title}Core Service Configuration</Text>
        </Control>
        <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
      </Dialog>

I add the InvalidURIDlg to this same UI element in my ServiceDlg.wxs since they go hand-in-hand. This dialog is created from the SpawnDialog event in the ServiceDlg dialog, so it has that dialog as a parent. The dialog displays a message to the user and then the user’s one option is to click a button to make the dialog go away (and return back to the ServiceDlg).

<Dialog Id="InvalidURIDlg" Width="260" Height="85" Title="[ProductName] [Setup]" NoMinimize="yes">
  <Control Id="Return" Type="PushButton" X="100" Y="57" Width="56" Height="17" Default="yes" Cancel="yes" Text="&Return">
    <Publish Event="EndDialog" Value="Return">1</Publish>
  </Control>
  <Control Id="Text" Type="Text" X="48" Y="15" Width="194" Height="30" TabSkip="no">
    <Text>The service location provided is not a valid URI.</Text>
  </Control>
</Dialog>
 
The final thing I need to do show this dialog is fit this new dialog into the install sequence.  I add the dialog reference to the same Fragment where I reference the FeatureTree set. Then I add the events to insert the ServiceDlg between the LicenseAgreementDlg and CustomizeDlg of the FeatureTree set.

 <Fragment>
    <UI Id="MyWixUI_FeatureTree">
      <UIRef Id="WixUI_FeatureTree" />
      <UIRef Id="WixUI_ErrorProgressText" />
 
      <DialogRef Id="ServiceDlg" />
 
      <Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="ServiceDlg" Order="2">LicenseAccepted = "1"</Publish>
      <Publish Dialog="CustomizeDlg" Control="Back" Event="NewDialog" Value="ServiceDlg" Order="2">1</Publish>
    </UI>
  </Fragment>

You can run the install as it is and see the custom action “in action” but at this point it isn’t particularly useful unless we do something with the value that the user input. My goal is to write this value to the App.config of my application as the address for a service endpoint. Initially I just want to try to write the name to the appSettings element of the App.config. This is really quite simple with WiX using the XmlFile utility extension.

First we need a reference from the WiX project to the WixUtilExtension library. With that reference in place WiX will handle the correct commands to compile the library in with our installer. So to our main Wix root element we need to add the XML namespace for util.

<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
     xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
 
Then to the same component that contains the configuration file that we want to change we add a series of XmlFile elements. The first will create a new node “add” to the appSettings node in our config file. Note that the configuration must have an appSettings node to begin with, AND even if the node is empty it must have separate opening and close tags. will not work; you need to have under the configuration element. The second element will add a “key” attribute to the “add” element and give it a value of “ServiceUri”. The final element will give a “value” attribute to our element with the value of the SERVICEURI that was passed in from the user. The INSTALLFOLDER in the file references the directory I gave the Id of INSTALLFOLDER which is the main install folder for the application.

         <File Id="File.ElectionDataService.Config" Name="ElectionService.exe.config" Source="$(var.ServicePath)\ElectionService.exe.config"/>
        <util:XmlFile Id='Settings.ElectionService.Config1' File='[INSTALLFOLDER]ElectionService.exe.config'                      
         Action='createElement' Name='add' ElementPath='//configuration/appSettings' Sequence='1' />
        <util:XmlFile Id='Settings.ElectionService.Config2' File='[INSTALLFOLDER]ElectionService.exe.config'
         Action='setValue' Name='key' Value='ServiceUri' ElementPath='//configuration/appSettings/add' Sequence='2' />
        <util:XmlFile Id='Settings.ElectionService.Config3' File='[INSTALLFOLDER]ElectionService.exe.config'
         Action='setValue' Name='value' Value='[SERVICEURI]' ElementPath='//configuration/appSettings/add' Sequence='3' />
 
Running the installation now results in the following appSettings block in the configuration file:
   

Simply writing to the appSettings is okay, but what I really want to do is save the user input in the configuration as the endpoint for a service that the application needs. To do this I need to refine where the XmlFile elements write to. My config file already has the endpoint element that I need; I just want to fill in the address attribute. Before installation it looks like this:
<endpoint binding="wsHttpBinding" bindingConfiguration="wsHttpBindingNoSec"
    contract="ICoordinateInterface" name="WSHttpBinding_ICoordinateInterface">
 
I really only need a single XmlFile element in my Wix file that will add the address attribute to that endpoint node with a value of whatever the user entered. Another consideration is that I actually have multiple endpoints for different services in the configuration file, so I need to select the endpoint with the service interface that corresponds to the one I want to change. I replace the sequence of XmlFile elements above with a new one.

         <File Id="File.ElectionDataService.Config" Name="DVS.EMS.Services.ElectionData.ElectionDataService.exe.config" Source="$(var.ServicePath)\DVS.EMS.Services.ElectionData.ElectionDataService.exe.config"/>
        <util:XmlFile Id='Settings.ElectionDataService.Config' File='[INSTALLFOLDER]DVS.EMS.Services.ElectionData.ElectionDataService.exe.config' 
          Action='setValue' Name='address' Value='[SERVICEURI]' ElementPath='//configuration/system.serviceModel/client/endpoint[\[]@contract="ICoordinateInterface"[\]]' Sequence='1' />

The bracketed @contract=… attribute in the ElementPath picks out the individual endpoint element with the service interface that I want to change, but not the others. The brackets need to be escaped with the pattern shown.

Now running the install my configuration ends up with the following endpoint:

1 comment:

dirt said...

Thanks for the article.

Everything is working, but I don't see session.Log messages in the %TEMP%\log file.

=====
session.Log("Checking license..."); // Does NOT show in log
string Pid = session["PIDKEY"];
session["PIDACCEPTED"] = Pid.StartsWith("1") ? "1" : "0"; // DOES show in log

// If this code is fired, session.Log does NOT show in log
// but the record[0] message DOES show in log (see below)
session.Log("ERROR Checking license: {0}", ex.ToString());
Record record = new Record(0);
record[0] = "Unable to check license!";
session.Message(InstallMessage.Error, record);
return ActionResult.Failure;
=====

=====
MSI (c) (D4:D8) [08:11:47:543]: Doing action: LicenseCheckAction
Action 8:11:47: LicenseCheckAction.
Action start 8:11:47: LicenseCheckAction.
MSI (c) (D4:EC) [08:11:47:545]: Invoking remote custom action. DLL: C:\Users\POS1User\AppData\Local\Temp\MSIF008.tmp, Entrypoint: LicenseCheck
MSI (c) (D4:F0) [08:11:47:547]: Cloaking enabled.
MSI (c) (D4:F0) [08:11:47:547]: Attempting to enable all disabled privileges before calling Install on Server
MSI (c) (D4:F0) [08:11:47:549]: Connected to service for CA interface.
MSI (c) (D4!40) [08:11:49:327]: PROPERTY CHANGE: Adding PIDACCEPTED property. Its value is '1'.
MSI (c) (D4!40) [08:12:32:046]: Product: MyApp -- Unable to check license!

Action ended 8:12:32: LicenseCheckAction. Return value 3.
DEBUG: Error 2896: Executing action LicenseCheckAction failed.
=====