'How to expose methods from a C# assembly which can be consumed as custom actions in Visual Studio Installer Projects (VS 2019)
I am using the Visual Studio Installer Projects extension for Visual Studio 2019. I have created my setup project and want to add custom actions which I want to implement as C# methods (I want the methods to install/uninstall a certificate). What I am not clear on is the linkage between the custom action configuration in the Custom Actions panel (appearing in Visual Studio) and the C# code I want to invoke. How do I create the class/methods such that they will be visible to the installer and activated as required? Does the class have to have a certain inheritance, and if so, what assemblies are required?
Solution 1:[1]
Found the answer I was looking for in DllExport (https://github.com/3F/DllExport/wiki). This allowed me to create a .NET Framework assembly and export entry points to assembly methods as if they were conventional unmanaged C++ DLL exports, which can be invoked by msiexec as custom actions.
Basic steps:
- Create the assembly project and add a class with static methods to be exported.
- Add DllExport nuget reference to the project. The nuget includes a configuration tool; adding the nuget launches the tool. You need to identify your solution, the project within your solution, and (confusingly) the namespace containing the DllExport attribute (which should be
System.Runtime.InteropServices). When you "Apply", it modifies the vcproj. - Add the
[DllExport]attribute to each static method to be exported. On build, an assembly will be created which has C++ entry points to the methods. NOTE by default, there will be an "AnyCPU" version of the assembly which does not contain any exports, in addition to x86 and x64 versions of the assembly which do. You need to use one of those. - Add the x86 assembly to the installer project and reference the assembly and exported name in the custom action. You can also specify a value for CustomActionData.
Sample code:
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
namespace CustomActionFrameworkTest
{
public class CustomActions
{
/// <summary>
/// Import the MsiGetProperty method
/// </summary>
/// <param name="hInstall">Handle to the install instance</param>
/// <param name="szName">Property name we want the value of</param>
/// <param name="szValueBuf">Value</param>
/// <param name="pchValueBuf">Length of value (buffer size on input, actual length output)</param>
/// <returns></returns>
[DllImport("msi.dll", CharSet = CharSet.Unicode)]
static extern int MsiGetProperty(int hInstall, string szName,
[Out] StringBuilder szValueBuf, ref int pchValueBuf);
[DllExport]
public static int InstallTest(int handle)
{
// Debugger.Launch(); // If you need to debug
var sb = new StringBuilder(512); // Initialize to an arbitrary size
int size = 512; // Must give the function a buffer size
var status = MsiGetProperty(handle, "CustomActionData", sb, ref size);
if (status == 0)
{
var message = $"Got value '{sb}' from CustomActionData";
MessageBox.Show(message, "Test", 0);
}
else
{
MessageBox.Show($"MsiGetProperty failed with error code: {status}", "Error", 0);
}
return 0;// 0= success, 1602 = user exit, 1603 = failure
}
}
}
Solution 2:[2]
You can create them just like you would a normal class. You will need to import a couple of namespaces to do it.
Specifically
- Microsoft.Deployment.WindowsInstaller;
- Microsoft.Tools.WindowsInstallerXml;
Here is a shortened version of a custom action I did for an installer a little while back.
using Microsoft.Deployment.WindowsInstaller;
using Microsoft.Tools.WindowsInstallerXml;
public class CustomActions : WixExtension
{
[CustomAction]
public static ActionResult CreateAndPopulateStorageFolder(Session session)
{
ActionResult result = ActionResult.Success;
try
{
session.Log("In Custom Action: CreateAndPopulateStorageFolder");
if (!Directory.Exists("C:\\Path"))
{
session.Log("In Custom Action: CreateAndPopulateStorageFolder: Directory does not exist");
Directory.CreateDirectory("C:\\Path");
Directory.CreateDirectory("C:\\Path");
Directory.CreateDirectory("C:\\Path");
}
}
catch (Exception ex)
{
session.Log(ex.Message);
result = ActionResult.Failure;
}
return result;
}
}
Solution 3:[3]
I found the answer in this article. Basically amounts to adding an Installer class to my assembly (inherits from System.Configuration.Install.Installer) and overriding the appropriate methods. Need to set the InstallerClass property to True for the Custom Action Properties.
using System.Collections;
using System.ComponentModel;
using System.Configuration.Install;
using System.IO;
namespace InstallerCustomAction
{
[RunInstaller(true)]
public partial class MyInstaller : Installer
{
public MyInstaller()
{
InitializeComponent();
}
public override void Install(IDictionary stateSaver)
{
File.WriteAllText(@"C:\InstallTest.txt", "This is a test of an install action");
base.Install(stateSaver);
}
}
}
(Note that the template creates a corresponding Designer.cs file)
This doesn't seem to support incoming parameters, but it's not something I need.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Zenilogix |
| Solution 2 | Jared Harmon |
| Solution 3 | Zenilogix |
