我最近与 ExcelDNA 合作了一些示例项目,以了解有关该技术的更多信息。我希望实现的功能之一是让用户在 Excel 中使用 C#、VB 或 F# 创建自己的函数。最初的路径是利用 ExcelDNA 作者发布的示例代码。但是,该代码适用于 Roslyn 之前的编译器。还有另一篇 Stack Overflow 帖子这里对审核也很有帮助。
对于后 Roslyn 编译器,我利用 Rick Strahl 的 Westwind.Scripting 库来动态编译函数,它非常适合编译函数并允许在 Excel 中注册。我的代码在这里:
using ExcelDna.Integration;
using ExcelDna.IntelliSense;
using System.Reflection;
using System.Runtime.InteropServices;
using Westwind.Scripting;
namespace TestExcelDna
{
[ComVisible(false)]
public class AddIn : IExcelAddIn
{
public void AutoOpen()
{
try
{
// Rosyln warmup
// at app startup - runs a background task, but don't await
_ = RoslynLifetimeManager.WarmupRoslyn();
IntelliSenseServer.Install();
RegisterFunctions();
IntelliSenseServer.Refresh();
}
catch (Exception ex)
{
var error = ex.StackTrace;
Console.WriteLine(error);
}
}
public void AutoClose()
{
IntelliSenseServer.Uninstall();
}
public void RegisterFunctions()
{
var script = new CSharpScriptExecution() { SaveGeneratedCode = true };
script.AddDefaultReferencesAndNamespaces();
var code = $@"
using System;
namespace MyApp
{{
public class Math
{{
public Math() {{}}
public static string TestAdd(int num1, int num2)
{{
// string templates
var result = num1 + "" + "" + num2 + "" = "" + (num1 + num2);
Console.WriteLine(result);
return result;
}}
public static string TestMultiply(int num1, int num2)
{{
// string templates
var result = $""{{num1}} * {{num2}} = {{ num1 * num2 }}"";
Console.WriteLine(result);
result = $""Take two: {{ result ?? ""No Result"" }}"";
Console.WriteLine(result);
return result;
}}
}}
}}";
// need dynamic since current app doesn't know about type
dynamic math = script.CompileClass(code);
Console.WriteLine(script.GeneratedClassCodeWithLineNumbers);
// Grabbing assembly for below registration
dynamic mathClass = script.CompileClassToType(code);
var assembly = mathClass.Assembly;
if (!script.Error)
{
Assembly asm = assembly;
Type[] types = asm.GetTypes();
List<MethodInfo> methods = new();
// Get list of MethodInfo's from assembly for each method with ExcelFunction attribute
foreach (Type type in types)
{
foreach (MethodInfo info in type.GetMethods(BindingFlags.Public | BindingFlags.Static))
{
methods.Add(info);
}
}
Integration.RegisterMethods(methods);
}
else
{
//MessageBox.Show("Errors during compile!");
}
}
}
}
上面的代码可以用于注册函数,但是我们没有从 ExcelFunction 和 ExcelArgument 属性提供的函数向导中获得好处。因此,我想利用 ExcelDna.Integration 库中的这些属性。
但是,当添加到要编译的代码中时,编译器找不到ExcelDna.Integration库。问题似乎是 ExcelDna.Integration.dll 未包含在已发布的工件中。上面用于测试的代码的调整(不起作用)在这里:
using ExcelDna.Integration;
using ExcelDna.IntelliSense;
using System.Reflection;
using System.Runtime.InteropServices;
using Westwind.Scripting;
namespace TestExcelDna
{
[ComVisible(false)]
public class AddIn : IExcelAddIn
{
public void AutoOpen()
{
try
{
// Rosyln warmup
// at app startup - runs a background task, but don't await
_ = RoslynLifetimeManager.WarmupRoslyn();
IntelliSenseServer.Install();
RegisterFunctions();
IntelliSenseServer.Refresh();
}
catch (Exception ex)
{
var error = ex.StackTrace;
Console.WriteLine(error);
}
}
public void AutoClose()
{
IntelliSenseServer.Uninstall();
}
public void RegisterFunctions()
{
var script = new CSharpScriptExecution() { SaveGeneratedCode = true };
script.AddDefaultReferencesAndNamespaces();
script.AddAssembly("ExcelDna.Integration.dll");
var code = $@"
using System;
using ExcelDna.Integration;
namespace MyApp
{{
public class Math
{{
public Math() {{}}
[ExcelFunction(Name = ""TestAdd"", Description = ""Returns 'TestAdd'"")]
public static string TestAdd(int num1, int num2)
{{
// string templates
var result = num1 + "" + "" + num2 + "" = "" + (num1 + num2);
Console.WriteLine(result);
return result;
}}
[ExcelFunction(Name = ""TestMultiply"", Description = ""Returns 'TestMultiply'"")]
public static string TestMultiply(int num1, int num2)
{{
// string templates
var result = $""{{num1}} * {{num2}} = {{ num1 * num2 }}"";
Console.WriteLine(result);
result = $""Take two: {{ result ?? ""No Result"" }}"";
Console.WriteLine(result);
return result;
}}
}}
}}";
// need dynamic since current app doesn't know about type
dynamic math = script.CompileClass(code);
Console.WriteLine(script.GeneratedClassCodeWithLineNumbers);
// Grabbing assembly for below registration
dynamic mathClass = script.CompileClassToType(code);
var assembly = mathClass.Assembly;
if (!script.Error)
{
Assembly asm = assembly;
Type[] types = asm.GetTypes();
List<MethodInfo> methods = new();
// Get list of MethodInfo's from assembly for each method with ExcelFunction attribute
foreach (Type type in types)
{
foreach (MethodInfo info in type.GetMethods(BindingFlags.Public | BindingFlags.Static))
{
methods.Add(info);
}
}
Integration.RegisterMethods(methods);
}
else
{
//MessageBox.Show("Errors during compile!");
}
}
}
}
有谁知道如何在 ExcelDNA 动态编译函数中利用 ExcelFunction 和 ExcelArgument 属性?
编辑2023年8月21日
我已将示例项目存储库上传到 GitHub,其中包含 RegisterFunctionsWorks 和 RegisterFunctionsDoNotWork 供那些想要使用代码的人使用。可以在here找到该存储库。
问题在于动态编译程序集未加载到加载项的 AssemblyLoadContext(其中类型解析正常工作)。解决此问题的方法似乎是将
Westwind.Scripting.CSharpScriptExecution
对象的 AlternateAssemblyLoadContext
属性设置为加载项的 ALC,如下所示:
var script = new CSharpScriptExecution() { SaveGeneratedCode = true };
script.AlternateAssemblyLoadContext = AssemblyLoadContext.GetLoadContext(this.GetType().Assembly);
script.AddDefaultReferencesAndNamespaces();
这对于您的示例来说似乎已经足够好了,即使您没有从动态编译的代码中引用
ExcelDna.Integration
,您也应该这样做,因为您确实希望将新程序集及其依赖项加载到正确的 ALC 中,其中有可能。
在某些情况下,其他依赖项的加载仅在代码运行时发生,这可能需要您在某些地方输入上下文反射范围。所以你有时可能需要这样的代码:
using (var ctx = System.Runtime.Loader.AssemblyLoadContext.EnterContextualReflection(this.GetType().Assembly))
{
// ... run code that loads extra assemblies here
}
不幸的是,没有办法让类型加载在 .NET 6+ 下像以前在 .NET Framework 下一样工作,我们在 .NET Framework 中使用 AppDomains 来隔离加载项。