cad.net dll动态加载,插件式架构,在dll查找引用了的dll,查找dll依赖,dll热插拔,加载dll运行出错.

需求

应用需求1:

我们cad.net开发都会面临一个问题,加载了的dll无法实现覆盖操作,也就是cad一直打开的状态下,netload两次版本不一样的dll,它只会用第一次载入的....没法做到热插拔....

应用需求2: 

制作一个拖拉dll到cad加载,但是不想通过发送netload到命令栏加载...

在这两个需求之下,已有的资料 明经netloadx 似乎是不二之选...

成因

提出上面的两个需求仅仅是我为了这篇文章想为什么需要这个技术而已.......编的   ( >,< )

真正令我开始研究是因为若海提出的: 明经netloadx 在 a.dll 引用了 b.dll 时候, 为什么不会成功调用...

我首先想到是依赖,于是乎,我试图尝试直接 Assembly.Load(File.ReadAllBytes(path)) 在加载目录的每个文件,并没有报错...

  这里打断一下,Assembly.Load(byte),转为byte是为了实现热插拔,所以Assembly.LoadForm()没有byte重载,也就无法拷贝到内存中去,故此不考虑...

然后出现了一个事情, 能使用单独的命令,却还是不能跨 dll 调用....也就是有运行出错...runtime error

然后我弄了好几天查找了一下那么多的资料..........还翻遍了微软各个函数.............

最终,我还是问了群里的大佬,南胜回答了我,但是我用他的代码出现了几个问题:

他获取的路径是clr寻找路径之一,我需要改到加载路径上面的...这里各位可以自行去看看clr的寻找未知dll的方式...

以及他只支持一个引用的dll,而我需要知道引用的引用的引用的引用的引用的引用的引用的引用的引用.....的dll

所以需要对他的代码修改一番.

工程开始

项目结构

首先,一共有四个项目,

    1: 直接netload的项目,也就是 [cad主插件项目].

    2: testa [cad次插件]

    3: testb [给a引用]

    4: testc [给b引用]

testa 引用 testb , testb 引用  testc ....如果后面还有套娃也可以.....套娃的套娃的套娃的套娃的套娃....

cad子插件项目

testa项目代码:

namespace testa
{
    public class MyCommands
    {
        [CommandMethod("testa")]
        public static void testa()
        {
            Document doc = Acap.DocumentManager.MdiActiveDocument;
            Editor ed;
            if (doc != null)
            {
                ed = doc.Editor;
                ed.WriteMessage("
自带函数testa.");
            }
        }

        [CommandMethod("gggg")]
        public void gggg()
        {
            Document doc = Acap.DocumentManager.MdiActiveDocument;
            Editor ed = doc.Editor;
            if (doc != null)
            { 
                ed.WriteMessage("
 **********gggg");  
                testb.MyCommands.TestBHello();
            }
        }
    }
}
View Code

testb项目代码:

namespace testb
{
    public class MyCommands
    {
        public static void TestBHello()
        {
            Document doc = Acap.DocumentManager.MdiActiveDocument;
            Editor ed;
            if (doc != null)
            {
                ed = doc.Editor;
                ed.WriteMessage("************testb的Hello");
                testc.MyCommands.TestcHello(); 
            }
        }

        [CommandMethod("testb")]
        public static void testb()
        {
            Document doc = Acap.DocumentManager.MdiActiveDocument;
            Editor ed;
            if (doc != null)
            {
                ed = doc.Editor;
                ed.WriteMessage("
自带函数testb.");
            }
        }
    }
}
View Code

testc项目代码:

namespace testc
{
    public class MyCommands
    {
        public static void TestcHello()
        {
            Document doc = Acap.DocumentManager.MdiActiveDocument;
            Editor ed;
            if (doc != null)
            {
                ed = doc.Editor;
                ed.WriteMessage("************testc的Hello");
            }
        }

        [CommandMethod("testc")]
        public static void testc()
        {
            Document doc = Acap.DocumentManager.MdiActiveDocument;
            Editor ed;
            if (doc != null)
            {
                ed = doc.Editor;
                ed.WriteMessage("
自带函数testc");
            }
        }
    }
}
View Code

必须更改版本号最后是*,否则无法重复加载(所有)

net framework要直接编辑项目文件.csproj,启用由vs迭代版本号:

<PropertyGroup>
  <Deterministic>False</Deterministic>
</PropertyGroup>

然后修改AssemblyInfo.cs

net standard只需要增加.csproj的这里,没有自己加一个:

<PropertyGroup>
    <AssemblyVersion>1.0.0.*</AssemblyVersion> 
    <FileVersion>1.0.0.0</FileVersion>
    <Deterministic>False</Deterministic>
</PropertyGroup>

  

cad主插件项目

概念

先说一下我的测试环境和概念,

我在[cad主插件]上面写了一个命令,这个命令调用了winform,winform让它接受拖拽dll文件,拿到dll的路径,然后链式加载..

这个时候需要直接启动cad,然后调用netload命令加载[cad插件]的dll.

      如果采用vs调试cad启动的话,那么我们本来也这么想的.....

      经过若海两天的Debug发现了: 不能在vs调试状态下运行cad!应该直接启动它! 

      猜想:这个时候令vs托管了cad的内存,令所有 Assembly.Load(byte) 都进入了托管内存上面,vs自动占用到 objDebug 文件夹下的dll.

      (不信你也可以试一下)      我开了个新文章写这个问题

启动cad之后用命令调用出winform,再利用拖拽testa.dll的方式就可以链式加载到所有的dll了..

再修改testa.dll重新编译,再拖拽到winform加载,

再修改testb.dll重新编译,再拖拽到winform加载,

再修改testc.dll重新编译,再拖拽到winform加载

.....如此如此,这般这般.....

winform拖拽这个函数太复杂了,但是搜一下基本能搞定....我就不贴代码了.

接收拖拽之后就有个testa.dll的path,再调用传给加载函数就好了,

调用方法

                        var ad = new AssemblyDependent(path);
                        var msg = ad.Load();

                        bool allyes = true;
                        foreach (var item in msg)
                        {
                            if (!item.LoadYes)
                            {
                                ed.WriteMessage(Environment.NewLine + "**" + item.Path +
                                    Environment.NewLine + "**此文件已加载过,重复名称,重复版本号,本次不加载!" +
                                    Environment.NewLine);
                                allyes = false;
                            }
                        }
                        if (allyes)
                        {
                            ed.WriteMessage(Environment.NewLine + "**链式加载成功!" + Environment.NewLine);
                        }

链式加载

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace JoinBoxCurrency
{
    public class AssemblyDependent
    {
        string _dllFile;
        /// <summary>
        /// cad程序域依赖_内存区(不可以卸载)
        /// </summary>
        private Assembly[] _cadAs;

        /// <summary>
        /// cad程序域依赖_映射区(不可以卸载)
        /// </summary>
        private Assembly[] _cadAsRef;

        /// <summary>
        /// 当前域加载事件
        /// </summary>
        public event ResolveEventHandler CurrentDomainAssemblyResolveEvent;

        /// <summary>
        /// 加载dll的和相关的依赖
        /// </summary>
        /// <param name="dllFile"></param>
        public AssemblyDependent(string dllFile)
        {
            _dllFile = dllFile;

            //cad程序集的依赖
            _cadAs = AppDomain.CurrentDomain.GetAssemblies();

            //映射区
            _cadAsRef = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();

            //运行时出错的话,就靠这个事件来解决
            if (CurrentDomainAssemblyResolveEvent != null)
            {
                AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainAssemblyResolveEvent;
            }
            else
            {
                AppDomain.CurrentDomain.AssemblyResolve += RunTimeCurrentDomain.DefaultAssemblyResolve;
            }
        }



        /// <summary>
        /// 返回的类型,描述加载的错误
        /// </summary>
        public class LoadDllMessage
        {
            public string Path;
            public bool LoadYes;

            public LoadDllMessage(string path, bool loadYes)
            {
                Path = path;
                LoadYes = loadYes;
            }
        }

        /// <summary>
        /// 字节加载
        /// </summary>
        /// <param name="_dllFile"></param>
        /// <returns></returns>
        public LoadDllMessage[] Load()
        {
            var loadYesList = new List<LoadDllMessage>();
            if (!File.Exists(_dllFile))
            {
                return loadYesList.ToArray();
            }

            //查询加载链之后再逆向加载,确保前面不丢失
            var allRefs = GetAllRefPaths(_dllFile);
            allRefs.Reverse();

            foreach (var path in allRefs)
            {
                //路径转程序集名
                string assName = AssemblyName.GetAssemblyName(path).FullName;
                //路径转程序集名
                Assembly assembly = _cadAs.FirstOrDefault((Assembly a) => a.FullName == assName);
                if (assembly == null)
                {

                    //为了实现debug时候出现断点,见链接
                    // https://www.cnblogs.com/DasonKwok/p/10510218.html
                    // https://www.cnblogs.com/DasonKwok/p/10523279.html

                    //实现字节加载 
                    var buffer = File.ReadAllBytes(path);
#if DEBUG
                    var dir = Path.GetDirectoryName(path);
                    var pdbName = Path.GetFileNameWithoutExtension(path) + ".pdb";
                    var pdbFullName = Path.Combine(dir, pdbName);
                    if (File.Exists(pdbFullName))
                    {
                        var pdbbuffer = File.ReadAllBytes(pdbFullName);
                        Assembly.Load(buffer, pdbbuffer);//就是这句会占用vs生成,可能这个问题是net strandard
                    }
                    else
                    {
                        Assembly.Load(buffer);
                    }
#else
                    Assembly.Load(buffer);
#endif   
                    loadYesList.Add(new LoadDllMessage(path, true));//加载成功
                }
                else
                {
                    loadYesList.Add(new LoadDllMessage(path, false));//版本号没变不加载
                }
            }
            return loadYesList.ToArray();
        }


        /// <summary>
        /// 获取加载链
        /// </summary>
        /// <param name="dll"></param>
        /// <param name="dlls"></param>
        /// <returns></returns>
        List<string> GetAllRefPaths(string dll, List<string> dlls = null)
        {
            dlls = dlls ?? new List<string>();
            //如果含有 || 不存在文件
            if (dlls.Contains(dll) || !File.Exists(dll))
            {
                return dlls;
            }
            dlls.Add(dll);

            //路径转程序集名
            string assName = AssemblyName.GetAssemblyName(dll).FullName;

            //在当前程序域的assemblyAs内存区和assemblyAsRef映射区找这个程序集名
            Assembly assemblyAs = _cadAs.FirstOrDefault((Assembly a) => a.FullName == assName);
            Assembly assemblyAsRef;

            //内存区有表示加载过
            //映射区有表示查找过但没有加载(一般来说不存在.只是debug会注释掉Assembly.Load的时候用来测试)
            if (assemblyAs != null)
            {
                assemblyAsRef = assemblyAs;
            }
            else
            {
                assemblyAsRef = _cadAsRef.FirstOrDefault((Assembly a) => a.FullName == assName);

                //内存区和映射区都没有的话就把dll加载到映射区,用来找依赖表
                assemblyAsRef = assemblyAsRef ?? Assembly.ReflectionOnlyLoad(File.ReadAllBytes(dll));
            }

            //遍历依赖,如果存在dll拖拉加载目录就加入dlls集合
            foreach (var assemblyName in assemblyAsRef.GetReferencedAssemblies())
            {
                //dll拖拉加载路径-搜索路径(可以增加到这个dll下面的所有文件夹?)
                string directoryName = Path.GetDirectoryName(dll);

                var path = directoryName + "\" + assemblyName.Name;
                var paths = new string[]
                {
                    path + ".dll",
                    path + ".exe"
                };
                foreach (var patha in paths)
                {
                    GetAllRefPaths(patha, dlls);
                }
            }
            return dlls;
        }
    }
}
View Code

运行域事件

而其中最重要的是这个事件,它会在运行的时候找已经载入内存上面的程序集.

AppDomain.CurrentDomain.AssemblyResolve += RunTimeCurrentDomain.DefaultAssemblyResolve;
using System;
using System.Linq;
using System.Reflection;

namespace JoinBoxCurrency
{
    public static class RunTimeCurrentDomain
    {
        #region  程序域运行事件 
        // 动态编译要注意所有的引用外的dll的加载顺序
        // cad2008若没有这个事件,会使动态命令执行时候无法引用当前的程序集函数
        // 跨程序集反射
        // 动态加载时,dll的地址会在系统的动态目录里,而它所处的程序集(运行域)是在动态目录里.
        // netload会把所处的运行域给改到cad自己的,而动态编译不通过netload,所以要自己去改.
        // 这相当于是dll注入的意思,只是动态编译的这个"dll"不存在实体,只是一段内存.

        /// <summary>
        /// 程序域运行事件
        /// </summary>   
        public static Assembly DefaultAssemblyResolve(object sender, ResolveEventArgs args)
        {
            var cad = AppDomain.CurrentDomain.GetAssemblies();

#if false
            /*获取名称一致,但是版本号不同的,调用最开始的版本*/
            //获取执行程序集的参数
             var ag = args.Name.Split(',')[0];
            //获取 匹配符合条件的第一个或者默认的那个
            // var load = cad.FirstOrDefault(a => a.GetName().FullName.Split(',')[0] == ag); 
#endif

            /*获取名称和版本号都一致的,调用它*/
            Assembly load = null;
            load = cad.FirstOrDefault(a => a.GetName().FullName == args.Name);
            if (load == null)
            {
                /*获取名称一致,但是版本号不同的,调用最后的可用版本*/
                var ag = args.Name.Split(',')[0];               
                //获取 最后一个符合条件的,
                //否则a.dll引用b.dll函数的时候,b.dll修改重生成之后,加载进去会调用第一个版本的b.dll            
                foreach (var item in cad)
                {
                    if (item.GetName().FullName.Split(',')[0] == ag)
                    {
                        //为什么加载的程序版本号最后要是*
                        //因为vs会帮你迭代这个版本号,所以最后的可用就是循环到最后的.
                        load = item;
                    }
                }
            }
 
            return load;
        }
        #endregion
    }
}
View Code

关于动态加载动态编译是有相通的部分的,而这个部分就是这个事件...

cad2008若没有这个事件,会使动态编译的命令,在执行时候无法引用当前的程序集函数...等于runtime error

netload会把所处的运行域给改到cad自己的,而动态编译不通过 netload,所以要自己去改.

调试

另见 https://www.cnblogs.com/JJBox/p/14050111.html

(完)

原文地址:https://www.cnblogs.com/JJBox/p/13833350.html