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: