使用ExcelDNA动态编译Excel自定义函数时,如何利用ExcelFunction和ExcelArgument属性?

问题描述 投票:0回答:1

我最近与 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找到该存储库。

c# excel-formula excel-dna excel-udf
1个回答
0
投票

问题在于动态编译程序集未加载到加载项的 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 来隔离加载项。

© www.soinside.com 2019 - 2024. All rights reserved.