如何公开 C# 程序集中的方法,这些方法可以在 Visual Studio 安装程序项目 (VS 2019) 中作为自定义操作使用

How to expose methods from a C# assembly which can be consumed as custom actions in Visual Studio Installer Projects (VS 2019)

我正在使用 Visual Studio 2019 的 Visual Studio Installer Projects 扩展。我已经创建了我的安装项目并想要添加自定义操作,我想将其作为 C# 方法实现(我希望这些方法 install/uninstall一张证书)。我不清楚的是自定义操作面板中的自定义操作配置(出现在 Visual Studio 中)与我要调用的 C# 代码之间的链接。我如何创建 class/methods 以便它们对安装程序可见并根据需要激活? class 是否必须具有某种继承性,如果是,需要哪些程序集?

您可以像创建普通 class 一样创建它们。您将需要导入几个命名空间才能执行此操作。

具体

  • Microsoft.Deployment.WindowsInstaller;
  • Microsoft.Tools.WindowsInstallerXml;

这是我不久前为安装程序执行的自定义操作的简化版本。

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;
}

}

我在 this article 中找到了答案。基本上相当于向我的程序集添加一个 Installer class(继承自 System.Configuration.Install.Installer)并覆盖适当的方法。需要将自定义操作属性的 InstallerClass 属性 设置为 True

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);
        }
    }
}

(注意模板会创建对应的Designer.cs文件)

这个貌似不支持传入参数,不过不是我需要的

在 DllExport (https://github.com/3F/DllExport/wiki) 中找到了我想要的答案。这使我能够创建一个 .NET Framework 程序集并将入口点导出到程序集方法,就好像它们是传统的非托管 C++ DLL 导出一样,可以由 msiexec 作为自定义操作调用。

基本步骤:

  1. 创建程序集项目并添加一个class要导出的静态方法。
  2. 添加 DllExport 对项目的 nuget 引用。 nuget 包含一个配置工具;添加 nuget 会启动该工具。您需要确定您的解决方案、解决方案中的项目,以及(令人困惑的)包含 DllExport 属性(应该是 System.Runtime.InteropServices)的命名空间。当您“应用”时,它会修改 vcproj。
  3. 为每个要导出的静态方法添加[DllExport]属性。在构建时,将创建一个程序集,其中包含方法的 C++ 入口点。 注意 默认情况下,除了包含导出的 x86 和 x64 版本程序集之外,还有一个不包含任何导出的程序集的“AnyCPU”版本。您需要使用其中之一。
  4. 将 x86 程序集添加到安装程序项目并在自定义操作中引用程序集和导出的名称。您还可以为 CustomActionData.
  5. 指定一个值

示例代码:

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
        }
    }
}