I am creating a MSI based Installer using Wix.
My Custom Action declaration goes like this...
<Binary Id="CustomActions" SourceFile="DLLs\CustomActions.CA.dll" />
<CustomAction Id="CheckPath" Return="check" Execute="immediate" BinaryKey="CustomActions" DllEntry="CheckPath" />
And under WixUI_InstallDir Dialog UI,
<UI Id="WixUI_InstallDir">
.....
<Publish Dialog="SelectDirDlg" Control="Next" Event="DoAction" Value="CheckPath" Order="2">1</Publish>
.....
</UI>
And in C# file,
[CustomAction]
public static ActionResult CheckPath(Session session)
{
Record record2 = new Record();
record.FormatString = "The path that you have selected is invalid!";
session.Message(InstallMessage.Error | (InstallMessage)MessageButtons.OK, record);
return ActionResult.Success;
}
I am expecting a Message box via the above Custom Action when the user selects an invalid path. But the message box is not shown.
What am I doing wrong?
Custom actions triggered via a DoAction control event cannot show message boxes. See http://msdn.microsoft.com/en-us/library/windows/desktop/aa368322%28v=vs.85%29.aspx.
Related
I have a Blazor Web Assembly project that I started in .NET5. I am using Blazorise and it is working well for me.
A few weeks ago, I upgraded to .NET6, initially within the main project and found that I was getting a strange error.
Shortly after loading the index page, after some back and forth to the server getting user details etc. I am getting an unhandled exception with a bunch of console messages which don't really indicate (to me) where the error is...
I have used an image to condense the messages. I can make the detail available if needed. I have followed some of the links and they take me deep into JS territory.
The strange things about this are:
a) This didn't occur till I updated to NET6 - initially I thought it could be that I had updated wrongly, so I rebuilt the App from the ground up using a NET6 Template. The error came back.
b) I have spent days using a process of elimination to be able to make the error 'go away' - details follow:
I have several components I am using for forms:
-an EditUserForm
-an OrgansationForm
-an AddressForm
They are all components and use Blazorise Modals. Here is the EditUserForm:
#namespace blah.Client.Components.Forms
<Modal #ref="modalRef">
<ModalContent IsCentered="true">
<ModalHeader>
<ModalTitle>Editing #editUser.FullName</ModalTitle>
<CloseButton Clicked="#HideModal" />
</ModalHeader>
<ModalBody MaxHeight="50">
<div class="bg-light">
#if (editUser.IsComplete)
{
<p>Your Information is Complete - Make any changes and Press the 'Save' button.</p>
}
else
{
<p>Your Information is Incomplete - Please ensure you have completed at least your Name, Address, Mobile Number and Gender.</p>
}
</div>
<EditForm Model="#editUser" #onchange="() => Refresh()">
<DataAnnotationsValidator />
<Field>
<FieldLabel>Title</FieldLabel>
<TextEdit Placeholder="Enter Title..." #bind-Text="#editUser.Title" />
</Field>
<Field>
<FieldLabel>First Name</FieldLabel>
<TextEdit Placeholder="Enter First Name..." #bind-Text="#editUser.FirstName" />
</Field>
<Field>
<FieldLabel>Middle Name</FieldLabel>
<TextEdit Placeholder="Enter Middle Name..." #bind-Text="#editUser.MiddleName" />
</Field>
<Field>
<FieldLabel>Last Name</FieldLabel>
<TextEdit Placeholder="Enter Last Name..." #bind-Text="#editUser.FamilyName" />
</Field>
<Field>
<FieldLabel>Email</FieldLabel>
<TextEdit Placeholder="Enter Email..." #bind-Text="#editUser.Email" />
</Field>
<Field>
<FieldLabel>Phone No</FieldLabel>
<TextEdit Placeholder="Enter Phone No..." #bind-Text="#editUser.PhoneNumber" />
</Field>
<Field>
<FieldLabel>Date of Birth</FieldLabel>
<InputDate #bind-Value="#editUser.DoB" />
</Field>
<Field>
<FieldLabel>Gender</FieldLabel>
<Blazorise.Select TValue="AvailablePlayer.Shared.Enums.Gender" #bind-SelectedValue="#editUser.Gender">
#foreach (AvailablePlayer.Shared.Enums.Gender gendertype in Enum.GetValues(typeof(AvailablePlayer.Shared.Enums.Gender)))
{
var gtypetemp = gendertype;
<Blazorise.SelectItem Value="#gtypetemp">
#Enum.GetName(typeof(AvailablePlayer.Shared.Enums.Gender), gtypetemp).Replace('_', ' ');
</Blazorise.SelectItem>
}
</Blazorise.Select>
</Field>
<Field>
<FieldLabel>Address</FieldLabel>
<div>#getlongaddress()</div>
#if (editUser.Address != null && !editUser.Address.IsComplete)
{
<p>The Address is incomplete, please edit</p>
<Button Color="Color.Danger" Clicked="#EditAddress">Edit Address</Button>
}
else
{
<Button Color="Color.Secondary" Clicked="#EditAddress">Edit Address</Button>
}
</Field>
<Field>
<FieldLabel>Sign up as Player and Accept Player T's & C's</FieldLabel>
<InputCheckbox #bind-Value="Isplayer">Accept Player Terms which you can see here</InputCheckbox>
</Field>
#if (Isplayer)
{
<Button Color="Color.Secondary" Clicked="#EditPlayer">Edit Player</Button>
}
<Field>
<FieldLabel>Sign up as a member of an organisation and Accept Member T's & C's</FieldLabel>
<InputCheckbox #bind-Value="Ismember">Accept Member Terms </InputCheckbox>
which you can see here
</Field>
#if (Ismember)
{
<Button Color="Color.Secondary" Clicked="#EditOrgMember">Edit Membership</Button>
}
</EditForm>
<AuthorizeView Roles="Administrator">
<Authorized Context="Auth">
<div class="bg-light">
<p>(Note: to remove Player Role also untick the 'Accept Player Terms' Box)</p>
#foreach (rolebool rolething in rolelist)
{
<div>
<Check TValue="bool" #bind-Checked="rolething.userisin">#rolething.role </Check>
</div>
}
</div>
</Authorized>
</AuthorizeView>
</ModalBody>
<ModalFooter>
<Button Color="Color.Secondary" Clicked="#HideModal">Close</Button>
<Button Color="Color.Primary" Clicked="#SaveAsync">Save Changes</Button>
</ModalFooter>
</ModalContent>
</Modal>
<CascadingValue Name="EditAddress" Value="#editUser.Address">
<AddressForm #ref="addressform" OnAddressSaved="AddressSaved"></AddressForm>
</CascadingValue>
<EditPlayerForm #ref="playereditform" OnPlayerSaved="Refresh" />
<OrganisationForm #ref="organisationForm" />
<Snackbar #ref="snackbar">
<SnackbarBody>
#StatusMessage
</SnackbarBody>
</Snackbar>
The code is in a seperate file and has many functions, so for brevity I just show the basics here:
namespace blah.Client.Components.Forms
{
public partial class EditUserForm : ComponentBase
{
[Inject] UserService UserService { get; set; }
...some stuff...
//private bool _ismember;
protected bool Ismember;
AddressForm addressform = new AddressForm();
EditPlayerForm playereditform = new EditPlayerForm();
OrganisationForm organisationForm = new OrganisationForm();
...some stuff...
protected void EditAddress()
{
addressform.ShowModal();
}
private void AddressSaved(AddressDTO address)
{
editUser.Address = address;
this.StateHasChanged(); // To reload the address.
}
public void Refresh()
{
this.StateHasChanged();
}
}
This is the OrganisationForm:
#namespace blah.Client.Components.Forms
<Modal #ref="modalRef">
<ModalContent IsCentered="true">
<ModalHeader>
<ModalTitle>Editing Organisation</ModalTitle>
<CloseButton Clicked="#HideModal" />
</ModalHeader>
<ModalBody MaxHeight="50">
<EditForm Model="#editOrganisation" #onchange="() => Refresh()">
<DataAnnotationsValidator />
<Field>
<FieldLabel>Organisation Name</FieldLabel>
<TextEdit Placeholder="blah..." #bind-Text="#editOrganisation.Name" />
</Field>
<Field>
<FieldLabel>Business Address</FieldLabel>
<div>#getlongaddress(editOrganisation.BusinessAddress)</div>
#if (editOrganisation.BusinessAddress != null && !editOrganisation.BusinessAddress.IsComplete)
{
<p>The Address is incomplete, please edit</p>
<Button Color="Color.Danger" Clicked="() => EditAddress(editOrganisation.BusinessAddress)">Edit Main Address</Button>
}
else
{
<Button Color="Color.Secondary" Clicked="() => EditAddress(editOrganisation.BusinessAddress)">Edit Main Address</Button>
}
</Field>
<Field>
<FieldLabel>Date Joined</FieldLabel>
<DateEdit #bind-Date="editOrganisation.DateJoined" />
</Field>
</EditForm>
</ModalBody>
<ModalFooter>
<Button Color="Color.Secondary" Clicked="#HideModal">Close</Button>
<Button Color="Color.Primary" Clicked="#SaveAsync">Save</Button>
</ModalFooter>
</ModalContent>
</Modal>
<AddressForm #ref="addressform" OnAddressSaved="AddressSaved"></AddressForm>
<Snackbar #ref="snackbar">
<SnackbarBody>
#StatusMessage
</SnackbarBody>
</Snackbar>
...and the code for the OrganisationForm (with some functions removed)
namespace blah.Client.Components.Forms
{
public partial class OrganisationForm : ComponentBase
{
[Inject] ExecService ExecService { get; set; }
// reference to the modal component
protected Modal modalRef = new Modal();
AddressForm addressform = new AddressForm();
DatePicker<DateTime?> datepicker = new DatePicker<DateTime?>();
[Parameter]
public EventCallback<OrganisationDTO> OnOrganisationSaved { get; set; }
public OrganisationDTO editOrganisation { get; set; } = new OrganisationDTO();
private void AddressSaved(AddressDTO address)
{
editOrganisation.BusinessAddress = address;
this.StateHasChanged(); // To reload the address.
}
protected void EditAddress(AddressDTO addresstoedit)
{
addressform.editAddress = addresstoedit;
addressform.ShowModal();
}
protected async Task SaveAsync()
{
editOrganisation = await ExecService.AddUpdateOrganisationAsync(editOrganisation);
StatusMessage = editOrganisation.Status.Reason;
await snackbar.Show();
HideModal();
await OnOrganisationSaved.InvokeAsync(editOrganisation);
}
}
}
and finally the AddressForm:
#namespace blah.Client.Components.Forms
<Modal #ref="modalRef">
<ModalContent IsCentered="true">
<ModalHeader>
<ModalTitle>Editing Address</ModalTitle>
<CloseButton Clicked="#HideModal" />
</ModalHeader>
<ModalBody MaxHeight="50">
<div class="bg-light">
#if (editAddress.IsComplete)
{
<p>Your Address is Complete - Make any changes and Press the 'Save' button.</p>
}
else
{
<p>Your Address is Incomplete - Please ensure you have at least your Number, Street and PostCode.</p>
}
</div>
<EditForm Model="#editAddress" #onchange="() => checkaddress()">
<DataAnnotationsValidator />
<Field>
<FieldLabel>No. Name or Building</FieldLabel>
<TextEdit Placeholder="Enter House No, Name Building..." #bind-Text="#editAddress.Line1"/>
</Field>
<Field>
<FieldLabel>Street</FieldLabel>
<TextEdit Placeholder="Enter Street..." #bind-Text="#editAddress.Line2" />
</Field>
<Field>
<FieldLabel>Town/City</FieldLabel>
<TextEdit Placeholder="Enter Town/City..." #bind-Text="#editAddress.Line3" />
</Field>
<Field>
<FieldLabel>Region/State</FieldLabel>
<TextEdit Placeholder="Enter Region/State..." #bind-Text="#editAddress.Line4" />
</Field>
<Field>
<FieldLabel>Post Code</FieldLabel>
<TextEdit Placeholder="Enter Post Code..." #bind-Text="#editAddress.PostCode" />
</Field>
</EditForm>
</ModalBody>
<ModalFooter>
<Button Color="Color.Secondary" Clicked="#HideModal">Close</Button>
<Button Color="Color.Primary" Clicked="#SaveAsync">Save</Button>
</ModalFooter>
</ModalContent>
</Modal>
... and the AddressForm code:
namespace blah.Client.Components.Forms
{
public partial class AddressForm : ComponentBase
{
// reference to the modal component
protected Modal add_modal = new Modal();
[CascadingParameter(Name = "EditAddress")]
AddressDTO editAddress { get; set; } = new AddressDTO();
[Parameter]
public EventCallback<AddressDTO> OnAddressSaved { get; set; }
// Since this is going to be usually called from another form feed back the address
// by way of this event.
async Task SaveAsync()
{
await OnAddressSaved.InvokeAsync(editAddress);
await add_modal.Hide();
}
// Display functions
public void ShowModal()
{
add_modal.Show();
}
public void HideModal()
{
add_modal.Hide();
}
public void checkaddress()
{
this.StateHasChanged();
}
}
}
...
I have tried many things, but I keep coming back to the error message that I don't really understand fully as it looks like something is going on with a null in the JS interop but I'm unclear.
I can make the problem go away by reducing the AddressForm to simply this:
#namespace blah.Client.Components.Forms
<h1>Address Form</h1>
<Modal #ref="add_modal">
<ModalContent IsCentered="true">
<h1>Address Form</h1>
</ModalContent>
</Modal>
err most of the time.. then it has come back, although it is difficult to keep track because the Chrome debugger sometimes caches stuff and I have to remember to refresh with the cache off...
If I remove the '#ref="add_modal"' from the Modal definition it has always worked.
I know this is weird and most of the time there is a clue in the error how to find the problem. This seems to come and go, and feels it might be timing related. The error always takes a few seconds to appear after all the rendering has finished, so its like a short timeout or failed promise in JS type of thing.
I have tried many things but the code hasn't really changed since I upgraded to NET6 (along with the other components). Another possibility would be if I could add an exception handler maybe and break on it or write my own message, however, I am not sure where to put that?
I am looking for some pointers of how to track this down tbh as I am pretty much at wits end. The error may be staring me in the face in the messages, but I don't see it.
Many thanks
Brett
I recently upgraded a blazor project to .net6 and had the same issue. One of my projects was failing to build for no apparent reason saying metadata files were missing. There were no other surface errors. I ended up finding a hidden compiler error in my project that VS was not displaying. Code in .razor files doesn’t seem to be monitored by the IDE unless the file is open. I opened all razor files and like magic the real error appeared in the error window.
I also had console errors from code in updated packages. I looked at the updates made to the packages to see what might cause those. Some functions in packages had been modified and given overloads I needed to switch to and that got rid of my other errors.
If you have a good folder and file structure in your project a fast way to do this is to open all files in a folder that contains razor files and wait a few seconds for errors to pop up. Close them if none pop up and move to the next folder.
There may be multiple errors so keep opening files and resolving errors when they pop up until you have gone through every folder.
It takes a little time For a large project but it’s not that bad.
I want to open a PDF on the Phone via the File-Path but i cant figure out how i could do this properly without using 3rd party packages.
You have any suggestion for this?
I already tried to use this on Android:
public void OpenFile(string filePath)
{
var fileToOpen = new Java.IO.File(filePath);
var uri = FileProvider.GetUriForFile(Application.Context, Application.Context.PackageName + ".fileprovider", fileToOpen);
var intent = new Intent();
var mime = IOUtil.GetMimeType(uri.ToString());
intent.SetAction(Intent.ActionView);
intent.SetDataAndType(uri, mime);
intent.SetFlags(ActivityFlags.NewTask);
intent.AddFlags(ActivityFlags.GrantReadUriPermission);
Application.Context.StartActivity(intent);
}
But i get the following Error:
Unhandled Exception:
Java.Lang.NullPointerException: Attempt to invoke virtual method
'android.content.res.XmlResourceParser
android.content.pm.ProviderInfo.loadXmlMetaData(android.content.pm.PackageManager,
java.lang.String)' on a null object reference
first you should addd this code to your manifest file :
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.easyphotopicker.fileprovider"
android:exported="false"
android:grantUriPermissions="true"
tools:replace="android:authorities">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/filepaths"
tools:replace="android:resource"/>
</provider>
and create filepaths :
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<root-path name="root" path="" /> //root directory of the device new File("/");
<files-path name="files" path="" /> //context.getFilesDir()
<cache-path name="cache" path="" /> //context.getCacheDir()
<external-path name="external" path="" /> //Environment.getExternalStorageDirectory()
<external-files-path name="name" path="path" /> //context.getExternalFilesDirs()
<external-cache-path name="name" path="path" /> //getExternalCacheDirs()
</paths>
Your error is telling us that there is no file at the location matching that's passed into the function. There's a few ways of doing this, one of them is as shown. After accepting permissions to access folders and files, this should be one of the simplest ways. You seem to be close:
public void OpenPdfFile(string filename)
{
var f = new Java.IO.File(filename);
if (f.Exists())
{
System.Diagnostics.Debug.WriteLine("File exists!");
try
{
var openFileIntent = new Intent(Intent.ActionView);
openFileIntent.SetDataAndType(Android.Net.Uri.FromFile(f), "application/pdf");
openFileIntent.SetFlags(ActivityFlags.NoHistory);
StartActivity(Intent.CreateChooser(openFileIntent, "Open pdf file"));
}
catch (ActivityNotFoundException)
{
//handle when no available apps
}
}
}
I haven't tested your work, but the first thing would be to see if you added this to the Manifest file
android:authorities="com.{package}.{name}.fileprovider"
since your code says Application.Context.PackageName + ".fileprovider"
I am developing an installation msi file using Wix toolset (in VS 2012) and faced with the problem that I can't add a confirmation dialog when user press button 'Cancel'. I tried to add also custom action dll with the message box, but if the function returns ActionResult.Failure, then my setup shows error dialog.
Original control:
<Control Type="PushButton" Id="progressCancelBtn" Width="56" Height="17" X="296" Y="244">
<Text>Cancel</Text>
<Publish Event="EndDialog" Value="Exit" />
</Control>
Try number 1:
public class CustomActions
{
[CustomAction]
public static ActionResult ShowExitMessageBox(Session session)
{
DialogResult result = MessageBox.Show("Exit installation?", "Exit", MessageBoxButtons.YesNo);
if (result == DialogResult.Yes)
{
return ActionResult.Failure;
}
else
{
return ActionResult.Success;
}
}
}
Custom action:
<CustomAction Id='ShowExitMsgBox' BinaryKey='CustomActions' DllEntry='ShowExitMessageBox' Execute='immediate' Return='ignore'/>
<Binary Id='CustomActions' SourceFile='$(var.CASourcesPath)\CustomActions.CA.dll'/>
And tried to use:
<Control Type="PushButton" Id="cancelBtn" Width="56" Height="17" X="300" Y="244" Cancel="yes">
<Text>Cancel</Text>
<Publish Event="DoAction" Value="ShowExitMsgBox">1</Publish>
<!--Publish Event="EndDialog" Value="Exit" /-->
</Control>
But it shows like failure, instead of cancellation.
Thank you for help in advance.
Update:
I changed my custom action C# code to the next:
public class CustomActions
{
[CustomAction]
public static ActionResult ShowExitMessageBox(Session session)
{
DialogResult result = MessageBox.Show("Exit installation?", "Exit", MessageBoxButtons.YesNo);
if (result == DialogResult.Yes)
{
session["CANCELEDPROP"] = "1";
}
else
{
session["CANCELEDPROP"] = "0";
}
return ActionResult.Success;
}
}
and WIX part:
// in Product.wxs
<Property Id="CANCELEDPROP" Secure="yes">0</Property>
// in UIFile.wxs
<Control Type="PushButton" Id="cancelBtn" Width="56" Height="17" X="300" Y="244" Cancel="yes">
<Text>Cancel</Text>
<Publish Event="DoAction" Value="ShowExitMsgBox">1</Publish>
<Publish Event="EndDialog" Value="Exit">CANCELEDPROP=1</Publish>
</Control>
It works on the first page, but I get an error on progress page: "Windows installer has stopped working" after I pressed some button on my message box. :(
I have found out if I don't use message box, then everything works. It is strange.
Update 2:
I tried to use session.Message instead of Windows.Forms, but message box just doesn't appear. Strange.
[CustomAction]
public static ActionResult ShowExitMessageBox(Session session)
{
Record record = new Record();
record.FormatString = "Exit installation?";
MessageResult result = session.Message(InstallMessage.Warning
| (InstallMessage)System.Windows.Forms.MessageBoxIcon.Warning
| (InstallMessage)System.Windows.Forms.MessageBoxButtons.YesNo,
record);
if (result == MessageResult.Yes)
{
session["CANCELEDPROP"] = "1";
}
else
{
session["CANCELEDPROP"] = "0";
}
return ActionResult.Success;
}
Finally I have found a solution by using CancelDlg from WixUIExtension.dll
(http://wixtoolset.org/documentation/manual/v3/wixui/dialog_reference/wixui_dialogs.html)
Add reference to WixUIExtension.dll (path on my computer is the next C:\Program Files (x86)\WiX Toolset v3.11\bin)
Add reference to icon that would be on the message box (Product.wxs):
< Binary Id="WixUI_Ico_Info" SourceFile="Images/MyIcon.ico" />
Control itself:
< Control Type="PushButton" Id="cancelBtn" Width="56" Height="17" X="300" Y="244" Cancel="yes">
< Text>Cancel
< Publish Event="SpawnDialog" Value="CancelDlg">1
< /Control>
I'm working on an ASP.NET project that has several web.config transformations that are generated at build time (all at once, using MsBuild); one .config file for each deployment environment.
EX:
<Target Name="BeforeBuild">
<TransformXml
Source="Web.Base.config"
Transform="Web.DevServer1.config"
Destination="ConfigBuild\Web.DevServer1.config" />
<TransformXml
Source="Web.Base.config"
Transform="Web.QAServer1.config"
Destination="ConfigBuild\Web.QAServer1.config" />
<!-- ... -->
<!-- ... -->
</Target>
Each transformation has several elements whose values are substituted into the base web.config file. Management and I are concerned that a necessary element could potentially be overlooked by mistake in one of the transformation files.
Is there a way to automatically verify that none of our transformation files are missing any elements, using either MsBuild or some other VisualStudio tool?
Preferably, this check would be carried out at build time.
Is there a way to automatically verify that none of our transformation files are missing any elements, using either MsBuild or some other VisualStudio tool?
You can create a custom task, which compare the config file by using XmlDocument class, if they are different, use Log.LogMessage to output the node message. Like this:
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System.Xml;
namespace Common
{
public class SimpleTask3 : Task
{
private string myProperty;
// The [Required] attribute indicates a required property.
// If a project file invokes this task without passing a value
// to this property, the build will fail immediately.
[Required]
public string MyProperty
{
get
{
return myProperty;
}
set
{
myProperty = value;
}
}
public override bool Execute()
{
// Log a high-importance comment
Log.LogMessage(MessageImportance.High, "The task was passed \"" + myProperty + "\"");
XmlDocument xDoc = new XmlDocument();
xDoc.Load(myProperty + "/web.base.config");
XmlDocument sDoc = new XmlDocument();
sDoc.Load(myProperty + "/ConfigBuild/Web.DevServer1.config");
//compare with them and check the different.
//if different
Log.LogMessage(MessageImportance.High, "different message");
return true;
}
}
}
Web.config:
<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v14.0\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="BeforeBuild">
<!--<Message Text="BuildDependsOn: $(BuildDependsOn)" />-->
<Message Text="Inside of BeforeBuild, time: $([System.DateTime]::Now)" />
<TransformXml Source="D:\Project\Msbuild\App1\App2\Web.Base.config" Transform="D:\Project\Msbuild\App1\App2\Web.DevServer1.config" Destination="D:\Project\Msbuild\App1\App2\ConfigBuild\Web.DevServer1.config" />
<TransformXml Source="D:\Project\Msbuild\App1\App2\Web.Base.config" Transform="D:\Project\Msbuild\App1\App2\Web.QAServer1.config" Destination="D:\Project\Msbuild\App1\App2\ConfigBuild\Web.QAServer1.config" />
</Target>
<UsingTask TaskName="Common.SimpleTask3"
AssemblyFile="D:\Project\Msbuild\App1\Common\bin\Debug\Common.dll"/>
<Target Name="AfterBuild">
<SimpleTask3 MyProperty="D:\Project\Msbuild\App1\App2"/>
</Target>
How can I author a WiX custom action that
Is always called at the end of an installation, at least if there's an install error
Copies the current MSI log file from its current local to the user's APPDATA folder
I have this managed custom action code. Not sure how to author its invocation in my Wix script. Should the custom action be scheduled for after InstallFinalize? Can it be scheduled OnExit="error"?
[CustomAction]
public static void CopyLogFile(Session session)
{
const string company = "MyCompany";
const string product = "MyProduct";
try
{
session.Log("CustomAction.CopyLogFile entry");
var msiLogFilePath = session.CustomActionData["LOGFILEPATH"];
if (msiLogFilePath != null)
{
session.Log("CustomAction.CopyLogFile MSI log filename: {0}", msiLogFilePath);
var localAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var destDirPath = Path.Combine(localAppDataPath, company, product);
var destDir = Directory.CreateDirectory(destDirPath);
session.Log("CustomAction.CopyLogFile Destination directory: {0}", destDir.FullName);
var destFilePath = Path.Combine(destDir.FullName, Path.GetFileName(msiLogFilePath));
File.Copy(msiLogFilePath, destFilePath, true);
session.Log("CustomAction.CopyLogFile Log file copied to: {0}", destFilePath);
}
else
{
session.Log("CustomAction.CopyLogFile File path not found");
}
}
catch (Exception exception)
{
session.Log("CustomAction.CopyLogFile exception {0}", exception);
}
finally
{
if (session != null)
{
session.Log("CustomAction.CopyLogFile exit");
session.Close();
}
}
}
Yes, you can schedule it after InstallFinalize (so did I, but I copy it every time if it is not a complete removal of the package):
<InstallExecuteSequence>
<Custom Action="CopyLogfile" After="InstallFinalize">NOT (REMOVE="ALL" AND NOT UPGRADINGPRODUCTCODE)</Custom>
</InstallExecuteSequence>
Remember to also add it to the UI sequence if you have any. I added it as event to the PushButton in the SetupCompleteSuccess- and SetupCompleteError-dialogs (maybe you need to add it only to the latter one?) like in the following:
<Dialog Id="SetupCompleteSuccess" X="50" Y="50" Width="374" Height="266" Title="[ProductName]" NoMinimize="yes">
<Control Id="OK" Type="PushButton" X="230" Y="243" Width="66" Height="17" Text="&Finish" TabSkip="no" Default="yes" Cancel="yes">
<Publish Event="EndDialog" Value="Exit">1</Publish>
<!-- ### Invoking copying the logfile if the Finish-button is pressed -->
<Publish Event="DoAction" Value="CopyLogfile">MsiLogFileLocation AND NOT (REMOVE="ALL" AND NOT UPGRADINGPRODUCTCODE)</Publish>
</Control>
<!-- ... rest of the dialog ... -->
</Dialog>
Regarding showing it only in case of an error: Maybe checking for the ProductState-properrty? Searched the web for this but didn't find anything useful.
Edit: Maybe a proper way to execute it only in case of an error is the usage of Custom Action flags that execute it only during rollback. In WiX it would be the Execute="rollback"-attribute for the CustomAction-tag.
Don't mess with custom actions after InstallFinalize if you can help it. They are often skipped entirely when deploying via tools such as SCCM, Unicenter, etc...
A custom dialog with a button to open the log file at the end of the install could work ok though.