cad.net 自执行初始化接口IExtensionApplication

原因

继承IExtensionApplication初始化接口的派生类会加载后自动执行.
但是此接口是不能在同一个dll实现一次以上的,继承了多次也不会执行多次,会报错.

那么这给编写代码带来了一种不好的情况是,每次都要去修改这个派生类,
如果是一个小的测试功能,你又要去动前面的核心,
老板都说这个类OK了,结果你git日志上面还能看到对它的修改,这样就感觉很蛋疼.

违反了编程原则
开闭原则: 对拓展进行开放,对修改进行关闭.

所以我是这么想的,只实现一次IExtensionApplication初始化接口,
通过它反射仿初始化接口的派生类,然后实例化它们,
运行它们的 仿Initialize方法仿Terminate方法 ,从而绕开只能唯一继承的缺点.
当然,除了反射仿初始化接口还可以反射初始化特性.

原始接口功能

派生类执行顺序

1,成员变量会被JIT以线程安全的方式初始化.
2,运行构造函数.
3,运行 Initialize 方法.
4,关闭CAD前会运行 Terminate 方法.
此方法遇到强行关闭进程是不会运行的,所以是弱执行.
此方法和 Initialize 方法位于同一个实例化类,为了验证说法,请看:

// A:如果输出都是1,就是不同实例化.
// B:如果输出是1和2,就是同一个实例化.
// 测试证明是B,说明Acad是会缓存的.
public class TestClass : IExtensionApplication {
    int a = 1;
    public void Initialize() {
        Debug.WriteLine(a);
        a++;
    }
    public void Terminate() {
        Debug.WriteLine(a);
    }
}

但是我们实例化时仍然需要构建缓存,
因为我们存在同一个类下的多个方法写初始化特性,
为了避免实例化这个类多次,下面代码会说这个事情.

加载dll时机

CAD加载dll有两种方式,它们的触发接口时机不同,
需要共同保证(我下面代码就是帮大家完成这个工作)

01,通过注册表启动
CAD启动时候会从进程后台到界面完成,
接口派生类在执行时候是后台,
此时界面和文档并没有完成,执行上下文和UI同步上下文还没有成为同一个.
a: 文档函数是无法使用,会为null,也就不存在doc.Editor了.
b: 新建图纸会致命错误.
c: Autodesk.Windows.ComponentManager.Ribbon==null.
d: 设置CAD系统变量会出错.

因此必须用dm文档事件,等待文档出现,然后执行任务.
dm全局事件可以不取消订阅,doc和db事件则需要注意取消订阅.

02,通过netload加载启动
此时界面已经完成,可以尝试直接执行任务.

03,运行时/结束前
dm是永远不为null的
var dm = Acap.DocumentManager;
Debug.Assert(dm is not null, "不可思议的为空Acap.DocumentManager");
当关闭全部文档后: dm.Count == 0;
但是我们不需要这样判断,直接获取当前文档是一样的:
var doc = dm.MdiActiveDocument;
此时doc is null,而发送命令和提示信息需要doc.
例如发送开启文档ctrl+n就要改用win32API.
搜索qnew: https://www.cnblogs.com/JJBox/p/14187031.html

更多实现

南胜还反射特性提取每个命令,把它们放在cuix的菜单工具位置
南胜博客,自动生成cuix

封装

接口和特性

namespace JoinBox;

[Flags]
public enum Sequence : int {
    StartFirst = 1, // 进程开启,后台最先
    StartLast = 1 << 1, // 进程开启,后台最后
    StartOnce = 1 << 2, // 文档首次开启,单例(特性默认/打印信息/发送命令/必然拥有前台文档)
    StartDocs = 1 << 3, // 文档每次开启
    EndDocs = 1 << 4, // 文档每次关闭
    EndOnce = 1 << 5, // 文档最后关闭,不是单例
    EndFirst = 1 << 6, // 进程关闭,最先,此时已经没有前台
    EndLast= 1 << 7 // 进程关闭,最后
}

// 初始化接口,仿IExtensionApplication
public interface IAutoGo {
    // 控制加载顺序
    Sequence SequenceId();
    // 被cad加载时候会自动执行
    void Initialize();
    // 关闭cad的时候会自动执行
    void Terminate();
}
 
// 初始化特性,放在命令函数上用来初始化
[AttributeUsage(AttributeTargets.Class/*没有用*/
   | AttributeTargets.Method
   | AttributeTargets.Property/*没有用*/, 
   AllowMultiple = true)]
public class JAutoGo : Attribute {
    public Sequence SequenceId { get; }
    public JAutoGo(Sequence sq = Sequence.StartOnce) {
        SequenceId = sq;
    }
}

自动执行

#define parallel

namespace JoinBox;

// 执行器
public class Actuator : IEquatable<Actuator>, IComparable<Actuator> {
    public Sequence SequenceId { get; }
    MethodInfo _methodInfo;

    public Actuator(MethodInfo method, Sequence sequence) {
        SequenceId = sequence;
        _methodInfo = method;
    }

    public bool Equals(Actuator other) {
        if (other is null) return false;
        return (int)SequenceId == (int)other.SequenceId
            && _methodInfo.Equals(other._methodInfo);
    }
    public override bool Equals(object obj) {
        if (obj is not Actuator other) return false;
        return Equals(other);
    }
    public override int GetHashCode() {
        return (int)SequenceId;
    }

    // SortedSet/SortedList/SortedDictionary都是不能重复,
    // 而且要实现IComparable<T>接口,而且不是鸭子类型
    public int CompareTo(Actuator other) {
        if((int)SequenceId > (int)other.SequenceId)
            return 1;
        if((int)SequenceId < (int)other.SequenceId)
            return -1;
        return 0;
    }

    /// <summary>
    /// 运行方法
    /// </summary>
    public Actuator Run(object[]? args = null) {
        try {
        TypeCache.Invoke(_methodInfo, args);

/* 此处由用户自己保证
        // Acad年份获取 https://www.cnblogs.com/JJBox/p/18511807
        // 立即处理队列
        if (VersionTool.CurrentYear >= 2014){
            System.Windows.Forms.Application.DoEvents();
        }
        // 底层一样ed.UpdateScreen();
        Acap.UpdateScreen();
*/
        return this;
        } catch (System.Exception e) {
            Debugger.Break();
            Debug.WriteLine("Actuator.Run出错" + e.Message 
                + $"出错位置:{_methodInfo.ReflectedType?.FullName}.{_methodInfo.Name}");
            return this;
        }
    }
}

public static class TypeCache {

    // 如果同一个类中多个方法有初始化特性,
    // 那么这个类会被实例化n次,所以需要构造缓存.
    // 如果key是方法信息,另一个方法信息进来找不到key,但是它们是同一个类.
    // 所以key是类名才对,
    // 但是key是方法信息,可以快速命中同一个方法,
    // 因此双缓存.
    static ConcurrentDictionary<MethodInfo, object> _cache1 = new();
    static ConcurrentDictionary<string, object> _cache2 = new();

    public static void Clear() {
        _cache1.Clear();
        _cache2.Clear();
    }

    public static object? Invoke(MethodInfo methodInfo, object[]? args = null) {
        if (methodInfo.IsStatic) {
            // 静态调用,参数数量要匹配,为null.
            var pas = methodInfo.GetParameters();
            if (args is null) args = new object[pas.Length];
            return methodInfo.Invoke(null, args);
        }

        // 非静态调用,实例化类
        object instance;
        string? fullName = null;
        Type? reftype = null;
        bool flag = _cache1.TryGetValue(methodInfo, out instance);
        if (!flag) {
            // 类位置,扩展方法会为空
            reftype = methodInfo.ReflectedType;
            // 命名空间+类
            fullName = reftype?.FullName;
            if (fullName is null) return null;
            flag = _cache2.TryGetValue(fullName, out instance);
        }
        if (!flag) {
            if (fullName is null) return null;
            var type = reftype?.Assembly.GetType(fullName);
            if (type is null) return null;
            instance = Activator.CreateInstance(type);
            _cache1.TryAdd(methodInfo, instance);
            _cache2.TryAdd(fullName, instance);
        }
        return methodInfo.Invoke(instance, args);
    }
}//class

// 剔除重复
public static class ListExtensions {
    public static void ExceptWith<T>(this List<T> sourceList, T[] other) {
        for (int i = other.Length - 1; i >= 0; i--) {
            if (other.Contains(sourceList[i]))
                sourceList.RemoveAt(i);
        }
    }
}

public class AutoClass : IExtensionApplication {

    // 储存全部初始化和释放执行函数
    // C#的有序结构都是不能重复的
    // SortedSet/SortedList/SortedDictionary
    // 我们还是用List吧
    List<Actuator> _set = new();
    // 只反射本dll的程序集
    bool _constraint = true;

    // 被cad加载时候自动执行
    public void Initialize() {
        try {
        var dm = Acap.DocumentManager;
        Debug.Assert(dm is not null, "不可思议的为空Acap.DocumentManager");

        // 获取特性下面全部方法
        GetAttributeFunc();
        // 获取接口下面全部方法
        GetInterfaceFunc();

        // 执行任务,此时即使调用doc.Editor输出也是无效的
        var array = _set.Where(ac => ac.SequenceId == Sequence.StartFirst).ToArray();
        _set.ExceptWith(array);
        array.ForEach(ac => ac.Run());
        array = _set.Where(ac => ac.SequenceId == Sequence.StartLast).ToArray();
        _set.ExceptWith(array);
        array.ForEach(ac => ac.Run());

        // 文档每次开启,专用数组
        _startDocs = _set.Where(ac => ac.SequenceId == Sequence.StartDocs).ToArray();
        _set.ExceptWith(_startDocs);
        // 文档每次关闭,专用数组
        _endDocs = _set.Where(ac => ac.SequenceId == Sequence.EndDocs).ToArray();
        _set.ExceptWith(_endDocs);

        // 为了能够无论何种加载都能doc.Editor输出:
        // x01,通过注册表加载StartFirst/Last,有doc没有doc.Editor,所以不会输出.
        // 用订阅文档事件等待,其后会立即触发一次,文档事件内就可以发送打印了.
        // x02,通过netload命令加载,虽然有订阅文档事件,
        // 它会在下次创建文档(ctrl+n)触发,此时必然有doc.Editor需要直接触发.
        dm.DocumentCreated += DmCreated;
        dm.DocumentToBeDestroyed += DmDestroyed;
        if (IsNetload()) {
            var doc = dm.MdiActiveDocument;
            MyTask(doc);
        }

        // 开启线程等待editor就绪,会打印四次,不知道为什么.
        // ThreadHelper.NewlyThread(ed => MyTask(ed));

        } catch (System.Exception e) {
            Debugger.Break();
            Debug.WriteLine("AutoClass.Initialize出错::" + e.Message);
        }
    }

    // 用命令输入中判断,那么动态加载dll就判断失效?
    bool IsNetload() {
        var dm = Acap.DocumentManager;
        var doc = dm.MdiActiveDocument;
        var ed = doc?.Editor;
        return doc is not null && doc.CommandInProgress.ToUpper() == "NETLOAD"
          && ed is not null 
          && !(ed.IsDragging && ed.IsQuiescent 
              && ed.IsQuiescentForTransparentCommand
              && ed.MouseHasMoved 
              && ed.UseCommandLineInterface);
    }

    // 执行我们的任务,
    // 只需要运行一次,就弄个标记做单例,为1就不执行
    int _isOnceExecuted = 0;
    void MyTask(Document doc) {
        Debug.Assert(doc is not null, "不可思议的为空doc");
        if (Interlocked.CompareExchange(ref _isOnceExecuted, 1, 0) == 0) {
            var array = _set.Where(ac => ac.SequenceId == Sequence.StartOnce).ToArray();
            _set.ExceptWith(array);
            var docArgs = new object[]{doc};
            array.ForEach(ac => ac.Run(docArgs));
        }
    }

    // 文档开启
    Actuator[] _startDocs = [];
    void DmCreated(object sender, DocumentCollectionEventArgs e) {
        try {
        var doc = e.Document;
        var docArgs = new object[]{doc};
        // 首次执行比每次先
        MyTask(doc);
        // 每次执行
        _startDocs.ForEach(ac => ac.Run(docArgs));
        } catch (System.Exception ex) {
            Debugger.Break();
            Debug.WriteLine("AutoClass.DmCreated出错::" + ex.Message);
        }
    }

    // 文档关闭
    Actuator[] _endDocs = [];
    void DmDestroyed(object sender, DocumentCollectionEventArgs e) {
        try {
        var doc = e.Document;
        var docArgs = new object[]{doc};
        // 每次执行
        _endDocs.ForEach(ac => ac.Run(docArgs));
        // 最后关闭的文档,它不是单例,因为重复多次.
        var dm = Acap.DocumentManager;
        if (dm.Count == 1) {
            _set.Where(ac => ac.SequenceId == Sequence.EndOnce)
                .Select(ac => ac.Run(docArgs))
                .ToArray();     
        }

        } catch (System.Exception ex) {
            Debugger.Break();
            Debug.WriteLine("AutoClass.DmDestroyed出错::" + ex.Message);
        }
    }

    // 关闭cad的时候会自动执行
    public void Terminate() {
        try {
        var dm = Acap.DocumentManager;
        Debug.Assert(dm is not null, "不可思议的为空Acap.DocumentManager");
        dm.DocumentCreated -= DmCreated;
        dm.DocumentToBeDestroyed -= DmDestroyed;

        // 执行任务
        _set.Where(ac => ac.SequenceId == Sequence.EndFirst)
            .Select(ac => ac.Run())
            .ToList();
        _set.Where(ac => ac.SequenceId == Sequence.EndLast)
            .Select(ac => ac.Run())
            .ToList();

        // 释放缓存,类析构会在此之后.
        TypeCache.Clear();

        } catch (System.Exception e) {
             Debugger.Break();
             Debug.WriteLine("AutoClass.Terminate出错::" + e.Message);
        }
    }

    /// <summary>
    /// 遍历程序域下所有类型
    /// </summary>
    /// <param name="dllNameWithoutExtension">约束此文件反射,不含扩展名</param>
    public static IEnumerable<Type> AppDomainGetTypes(string? dllNameWithoutExtension = null) {
        try {

        // 01,过滤ass.IsDynamic,因为Acad2021报错:
        // System.NotSupportedException:动态程序集中不支持已调用的成员
        // 02,过滤AcInfoCenterConn.dll,通讯库,因为反射它会依赖其他造成报错:
        // ReflectionTypeLoadException
        // 03,WPF的报错 https://dev59.com/rWIi5IYBdhLWcg3w_QmH
        // Microsoft.Expression.Interactions程序集
        // 跳过全部微软的包就好了?

        const string str1 = "AcInfoCenterConn";
        const string str2 = "Microsoft";

#if parallel
        System.Diagnostics.Trace.WriteLine("这是一条Trace消息,此时是并行");
        var assemblies = AppDomain.CurrentDomain.GetAssemblies()
            .AsParallel()
            .WithDegreeOfParallelism(Environment.ProcessorCount)
            .Where(ass => !ass.IsDynamic)
            .Where(ass => !ass.GetName().Name.Contains(str2))
            .Where(ass => Path.GetFileNameWithoutExtension(ass.Location) != str1);

        // 约束在此dll中反射.
        if (dllNameWithoutExtension is not null) {
            assemblies = assemblies.Where(ass =>
                Path.GetFileNameWithoutExtension(ass.Location) 
                == dllNameWithoutExtension);
        }
        // 只反射公开
        var types = assemblies.SelectMany(ass => ass.GetExportedTypes());
#else
/*
        // 找名称
        var names = AppDomain.CurrentDomain.GetAssemblies()
            .Where(ass => !ass.IsDynamic
            && Path.GetFileNameWithoutExtension(ass.Location) == str1)
            .Select(ass => ass); // ass.GetName().Name 为什么是空?
        MessageBox.Show(string.Join("\n\r", names.ToString()));
*/
        // 串行测试
        var assemblies = AppDomain.CurrentDomain.GetAssemblies()
            .Where(ass => !ass.IsDynamic)
            .Where(ass => !ass.GetName().Name.Contains(str2))
            .Where(ass => Path.GetFileNameWithoutExtension(ass.Location) != str1);

        // 约束在此dll中反射.
        if (dllNameWithoutExtension is not null) {
            assemblies = assemblies.Where(ass =>
                Path.GetFileNameWithoutExtension(ass.Location)
                == dllNameWithoutExtension);
        }

        int error = 0;
        var types = assemblies.SelectMany(ass => {
            try {
                ++error;
                // return ass.GetTypes();
                return ass.GetExportedTypes(); // 只反射公开
            } catch(ReflectionTypeLoadException e) {
                Debug.WriteLine($"出错:App;计数{error};错误信息:{e}");
                foreach (Type type in e.Types) {
                    if (type is null) continue;
                    Debug.WriteLine($"可用类型:{type.FullName}");
                }
                Debugger.Break();
                return null;
            } catch(Exception e) {
                Debug.WriteLine($"出错:App2;计数{error};程序集:{ass}");
                Debug.WriteLine($"出错:App2;计数{error};错误信息:{e}");
                Debugger.Break();
                return null;
            }
        });
#endif
        return types;
        } catch (System.Exception e) {
            Debugger.Break();
            Debug.WriteLine($"出错:AppDomainGetTypes;错误信息:{e.Message}");
            return null;
      }
    }

    /// <summary>
    /// 收集接口下的函数
    /// </summary>
    /// <returns></returns>
    void GetInterfaceFunc() {
        // 约束只反射本dll的类
        string? dll = null;
        if (_constraint) {
            var ass = Assembly.GetExecutingAssembly();
            dll = Path.GetFileNameWithoutExtension(ass.Location);
        }
        var ts = AppDomainGetTypes(dll);

#if parallel
        System.Diagnostics.Trace.WriteLine("这是一条Trace消息,此时是并行");

        // 我要实例化派生类(跳过委托/抽象和静态)
        var acs = ts.Where(type => type.IsClass)
            .Where(type => !type.IsAbstract)
            .Where(type => type.GetInterfaces().FirstOrDefault(
                iface => iface.Name == nameof(IAutoGo)) is not null)
        // .SelectMany(type => type.GetMethods()); // 方法展开会错序.
            .Select(type => CreateActuator2(type))
            .ToArray();

        // 串行加入容器
        foreach(var ac in acs) {
            if (ac is null) continue;
            _set.Add(ac.Value.Init);
            _set.Add(ac.Value.Term);
        }
#else
        var types = ts.ToArray();
        for (int i = 0; i < types.Length; i++) {
            var type = types[i];
            if (type is null) continue;   
            // 我要实例化派生类(跳过委托/抽象和静态)
            if (!type.IsClass || type.IsAbstract) continue;
            var hasIface = type.GetInterfaces().FirstOrDefault(
                iface => iface.Name == nameof(IAutoGo));
            if (hasIface is null) continue;
            var ac = CreateActuator2(type);
            if(ac is null) continue;
            _set.Add(ac.Value.Init);
            _set.Add(ac.Value.Term);
        }
#endif
    }

    const string _sequenceId = nameof(Sequence) + "Id";
    const string _in = "Initialize";
    const string _te = "Terminate";

    (Actuator Init, Actuator Term)? CreateActuator2(Type type) {
        // 获取接口实现的成员函数,虽然它们只会出现一次,
        // 但万一别人写了重载呢,要参数数量是0才行.
        var mets = type.GetMethods();
        var im = mets.FirstOrDefault(m => m.Name == _in
            && m.GetParameters().Length == 0
            && !m.IsAbstract);
        if (im is null) return null;

        var tm = mets.FirstOrDefault(m => m.Name == _te
            && m.GetParameters().Length == 0
            && !m.IsAbstract);
        if (tm is null) return null;

        var sq = Sequence.StartOnce;
        var sm = mets.FirstOrDefault(m => m.Name == _sequenceId
            && m.GetParameters().Length == 0
            && !m.IsAbstract);
        if (sm is not null) {
            // 这里是多线程环境,缓存需要线程安全容器
            var obj = TypeCache.Invoke(sm);
            if(obj is not null) 
                sq = (Sequence)obj;
        }

        // 如果派生类写的是End需要映射回Start.
        if (sq == Sequence.EndFirst)
            sq = Sequence.StartFirst;
        else if (sq == Sequence.EndLast)
            sq = Sequence.StartLast;
        else if (sq == Sequence.EndDocs)
            sq = Sequence.StartDocs;
        else if (sq == Sequence.EndOnce)
            sq = Sequence.StartOnce;

        Sequence sq2;
        if (sq == Sequence.StartFirst)
            sq2 = Sequence.EndFirst;
        else if (sq == Sequence.StartLast)
            sq2 = Sequence.EndLast;
        else if (sq == Sequence.StartDocs)
            sq2 = Sequence.EndDocs;
        else if (sq == Sequence.StartOnce)
            sq2 = Sequence.EndOnce;
        else throw ArgumentException("出错类型");

        return (new Actuator(im, sq), new Actuator(tm, sq2));
    }
 
    // 根据特性创建类型,允许同一个方法多个特性
    Actuator[] CreateActuator(MethodInfo methodInfo) {
        return methodInfo.GetCustomAttributes(true)
            .OfType<JAutoGo>();
            .Select(att => new Actuator(methodInfo, att.SequenceId))
            .ToArray();       
    }

    /// <summary>
    /// 收集特性下的函数
    /// </summary>
    void GetAttributeFunc() {
        // 特性会出现在同一个类中的多个方法,
        // 特性下的方法要public,否则就被编译器优化掉了.

        // 约束只反射本dll的类
        string? dll = null;
        if (_constraint) {
            var ass = Assembly.GetExecutingAssembly();
            dll = Path.GetFileNameWithoutExtension(ass.Location);
        }
        var ts = AppDomainGetTypes(dll);

#if parallel
        System.Diagnostics.Trace.WriteLine("这是一条Trace消息,此时是并行");
        var acs = ts.SelectMany(type => type.GetMethods())
            .SelectMany(m => CreateActuator(m))
            .ToArray();
        // 串行加入
        acs.ForEach(ac => _set.Add(ac));
#else
        var types = ts.ToArray();
        for (int i = 0; i < types.Length; i++) {
            var mets = types[i].GetMethods();
            for (int j = 0; j < mets.Length; j++) {
                CreateActuator(mets[j]).ForEach(ac => _set.Add(ac));
            }
        }
#endif
    }
}

线程等待

本文并没有使用,只是测试过程尝试插入任务到主线程.

1,同步上下文System.Threading.SynchronizationContext
因为UI的线程亲和性的原因,必须要创建线程才能修改,
它Post会安全插入UI线程中任务队列,不需要阻塞UI线程.

但是我希望等到某个时机才执行任务.
a,当前没有执行命令中的时机:

那么为什么不发送命令呢?
因为发送命令前需要检查doc是不是空.
那么为什么不用文档事件呢?
因为才发现文档事件可以.

b,初始化后静止的时机:

此时可以嵌入文档栏,但是不能等到界面完成,
因为完成之后插入的其他控件计算是失败的.
但是这个任务似乎也可以在文档事件内进行,
因为文档事件虽然初始化了很多东西,但是没有完成界面计算.

(所以说基本上不需要这个Post任务了????)

2,同步上下文System.Threading.SynchronizationContext
只是为了访问到变量,之后依然有并发问题,
就好像控制台开启两个线程,本身就能访问变量,但依然有并发问题.

a:需要资源的锁,
但是Acad符号表记录我锁什么?没法锁.

b:如果主线程仍然对于资源做出修改呢,
岂不是仍然需要阻塞主线程?
所以说插入任务时机的重要性,要在读取前插入任务.

正确流程:
插入任务T1(读写资源A)--主线程读资源A--主线程写资源A--插入任务T2...
错误流程:
主线程读资源A--插入任务T1(读写资源A)--主线程写资源A--插入任务T2...

那么如何保证呢?
在消息循环内进行判断是否是事务中!!

3,Send和Post有区别,
Send无法使用editor.Getpoint等交互函数,
交互函数也是另外的线程切入,而Send则屏蔽了其他插入.

4,消息循环
a:空闲事件System.Windows.Forms.Application.Idle
b:空闲事件高版本自带Acap.Idle
c:WPF的定时器DispatcherTimer
d:子类化NativeWindow

5,低版本处理
之前测试: 直接运行Acad08 4a会失效,而debug有效,
怀疑1,没有文档首次出现才订阅Idle事件,
此时System.Windows.Forms.Application没有初始化,
自然没有Idle事件,不过也不报错...
怀疑2,之前没有用volatile读取,导致一直是空的上下文.
怀疑3,之前整个流程出错了.
如果几个怀疑都不行,改为子类化NativeWindow拦截是肯定行.

public static class JAutoTask {
    /// <summary>
    /// 线程上下文的缓存,一旦不为空就一直不是空的<br/>
    /// 使用时出现对话框(例如新建图纸)上下文会是null<br/>
    /// 若为null就直接发送任务,会卡死,所以使用前循环判断<br/>
    /// </summary>
    static System.Threading.SynchronizationContext? _mainContext = null;

    // 线程队列(这个容器无序啊!)
    static ConcurrentQueue<Action> _postQueue = new();
    static ConcurrentQueue<Action> _sendQueue = new();

    /// <summary>
    /// 把任务发送到主线程,异步
    /// </summary>
    public static void Post(Action action) {
        if(action is null) throw new ArgumentNullException("Post委托参数为空");
        _postQueue.Enqueue(action);
    }

    /// <summary>
    /// 把任务发送到主线程,同步
    /// </summary>
    public static void Send(Action action) {
        if(action is null) throw new ArgumentNullException("Send委托参数为空");
        _sendQueue.Enqueue(action);
    }

    // 执行队列任务,并移除已执行的任务
    static void Run() {
        var context = Volatile.Read(ref _mainContext);
        if (context is null) 
            throw new ArgumentNullException("上下文为空");

        Action postAction;
        while (_postQueue.Count > 0 && _postQueue.TryDequeue(out postAction)) {
            context.Post(state => {
                try { postAction.Invoke(); } catch { }
            }, null);
        }

        Action sendAction;
        while (_sendQueue.Count > 0 && _sendQueue.TryDequeue(out sendAction)) {
            context.Send(state => {
                try { sendAction.Invoke(); } catch { }
            }, null);
        }
    }

#if NET45_OR_GREATER
    [MethodImpl(MethodImplOptions.AggressiveInlining)]  
#endif
    static System.Threading.SynchronizationContext? TrySetContext() {
        var context = System.Threading.SynchronizationContext.Current;
        Interlocked.CompareExchange(ref _mainContext, context, null);
        return Volatile.Read(ref _mainContext);
    }

    // 放在文档首次出现,form显示后才是前台线程上下文.
    public static void Init() {
        var context = Volatile.Read(ref _mainContext);
        context ??= TrySetContext();
        if(context is null) {
            // 等待线程上下文首次出现,没有就刷新主界面
            Refresh(Acap.MainWindow.Handle);       
            System.Diagnostics.Trace.WriteLine("1.初始化设置上下文失败");
        }

        // 消息循环中插入任务
        System.Windows.Forms.Application.Idle += (sender, e) => {
            var context = Volatile.Read(ref _mainContext);
            context ??= TrySetContext();
            if(context is null) {
                // 等待线程上下文首次出现,没有就刷新主界面
                Refresh(Acap.MainWindow.Handle);       
                System.Diagnostics.Trace.WriteLine("2.消息循环设置上下文失败");
                return;
            }

            // 写入数据库的话,需要阻止:
            // 1,文档锁跳过(必跳过)
            // 2,一个事务中或者嵌套事务运行中不要插入,事务数/事务栈顶不是空(必跳过).
            // 3,一个命令中有多个事务,
            // 不要插在两个事务缝隙之中,要跳过命令输入中.
            // 透明命令呢?就是能够套入另一个命令中,画直线的时候能够缩放.
            // 根据特性,发送到透明命令队列.
            // 4,C++/.NET/VB/Lisp执行中要跳过,
            // Lisp可以靠事件获取执行时机,发送到透明Lisp队列.
            // 其他呢? 
            // 别人也开线程Post然后直接id.Open的话这种怎么办呢?
            // 插进去咯,谁叫它不用事务/文档锁,不然无法约定互斥.
            // 文档锁flag执行任务必然跳过锁定,是不是执行任务我也加自动锁呢?
            // 5,多个命令的连续编组
            // 是否跳过活动编组,仿发送命令这个不需要,
            // 是否约定一个每个任务都需要编组?由用户控制.

            // 那么不是写入数据库呢?需要设计不同的任务队列.
            // 1,其他form(例如登陆对话框)比Acad主界面先弹出?
            // 如果是,那么UI上下文将捕获这个form的,那就必须跳过焦点不是主界面的.
            // 2,嵌入文档栏需要在Acad主界面完成前,
            // 如果跳过doc is null,岂不是不能嵌入了?通过无界面队列完成.
            // 3,用户交互中,你总不能断开别人GetPoint/GetString/输入命令中吧..

            var dm = Acap.DocumentManager;
            // 订阅事件为了获取是否Lisp/cmd中
            // 订阅事件之后跳过,下次循环再执行任务
            if(Interlocked.CompareExchange(ref _isOnce, 1, 0) == 0) {
                dm.DocumentCreated += DmCreated;
                dm.DocumentToBeDestroyed += DmDestroyed;
                return;
            }

            var doc = dm.MdiActiveDocument;
            if(doc is null) return;
            var lockmode = doc.LockMode();
            if(!lockmode.HasFlag(DocumentLockMode.None)) return;
            if(!lockmode.HasFlag(DocumentLockMode.NotLocked)) return;
            if(doc.TransactionManager.TopTransaction is not null) return;

            // 活动的编组
            // if((int)Acap.GetSystemVariable("Undoctl") == 8) return;

            // 关键字输入/ESC可能是""吗?还要靠事件上面的判断?
            if(doc.CommandInProgress != "") return;
            _docFlagsMap.TryGetValue(doc, out RunFlag ffff);
            if (ffff.HasFlag(RunFlag.LispRuning) ||
                ffff.HasFlag(RunFlag.CmdRuning)) return;
            Run();
        };

        // 低版本是这个报错吗?
        if(!System.Windows.Forms.Application.MessageLoop) {
            throw new("3.消息循环还没初始化");
        }
    }

    enum RunFlag : int {
        None = 0,
        CmdEnded = 1,
        CmdRuning = 1 << 1,
        CmdCancelled = 1 << 2,
        LispEnded = 1 << 3,
        LispRuning = 1 << 4,
        LispCancelled = 1 << 5,
    }

    // 首次订阅事件
    static int _isOnce = 0;
    // 每个文档的是否正在运行Lisp/cmd的标记
    static Dictionary<Document, RunFlag> _docFlagsMap = new();

    static void DmCreated(object sender, DocumentCollectionEventArgs e) {
        var doc = e.Document;
        if(_docFlagsMap.ContainsKey(doc)) return;
        _docFlagsMap.Add(doc, RunFlag.None);
        SubscribeToEvents(doc, false); // 订阅
    }
    static void DmDestroyed(object sender, DocumentCollectionEventArgs e) {
        var doc = e.Document;
        if(_docFlagsMap.ContainsKey(doc)) {
            SubscribeToEvents(doc, true); // 取消订阅
            _docFlagsMap.Remove(doc);
        }
    }

    // 订阅文档事件判断是否执行任务中,例如Lisp
    static void SubscribeToEvents(Document doc, bool isUn){
        if(isUn){
            doc.LispWillStart -= Doc_LispWillStart;
            doc.LispEnded -= Doc_LispEnded;
            doc.LispCancelled -= Doc_LispCancelled;
            doc.CommandWillStart -= Doc_CommandWillStart;
            doc.CommandEnded -= Doc_CommandEnded;
            doc.CommandCancelled -= Doc_CommandCancelled;
            return;
        }
        doc.LispWillStart += Doc_LispWillStart;
        doc.LispEnded += Doc_LispEnded;
        doc.LispCancelled += Doc_LispCancelled;
        doc.CommandWillStart += Doc_CommandWillStart;
        doc.CommandEnded += Doc_CommandEnded;
        doc.CommandCancelled += Doc_CommandCancelled;
    }

    static void Doc_CommandWillStart(object sender, CommandEventArgs e) {
        var doc = (Document)sender;
        _docFlagsMap[doc] &= ~RunFlag.CmdEnded;
        _docFlagsMap[doc] &= ~RunFlag.CmdCancelled;
        if(!_docFlagsMap[doc].HasFlag(RunFlag.CmdRuning))
            _docFlagsMap[doc] |= RunFlag.CmdRuning;
        
    }
    static void Doc_CommandEnded(object sender, CommandEventArgs e) {
        var doc = (Document)sender;
        _docFlagsMap[doc] &= ~RunFlag.CmdRuning;
        _docFlagsMap[doc] &= ~RunFlag.CmdCancelled;
        if(!_docFlagsMap[doc].HasFlag(RunFlag.CmdEnded))
            _docFlagsMap[doc] |= RunFlag.CmdEnded;
    }
    static void Doc_CommandCancelled(object sender, CommandEventArgs e) {
        var doc = (Document)sender;
        _docFlagsMap[doc] &= ~RunFlag.CmdRuning; 
        // 结束和取消
        if(!_docFlagsMap[doc].HasFlag(RunFlag.CmdEnded))
            _docFlagsMap[doc] |= RunFlag.CmdEnded;
        if(!_docFlagsMap[doc].HasFlag(RunFlag.CmdCancelled))
            _docFlagsMap[doc] |= RunFlag.CmdCancelled;
    }

    static void Doc_LispWillStart(object sender, LispWillStartEventArgs e) {
        var doc = (Document)sender;
        _docFlagsMap[doc] &= ~RunFlag.LispEnded;
        _docFlagsMap[doc] &= ~RunFlag.LispCancelled;
        if(!_docFlagsMap[doc].HasFlag(RunFlag.LispRuning))
            _docFlagsMap[doc] |= RunFlag.LispRuning;
    }
    static void Doc_LispEnded(object sender, EventArgs e) {
        var doc = (Document)sender;
        _docFlagsMap[doc] &= ~RunFlag.LispRuning;
        _docFlagsMap[doc] &= ~RunFlag.LispCancelled;
        if(!_docFlagsMap[doc].HasFlag(RunFlag.LispEnded))
            _docFlagsMap[doc] |= RunFlag.LispEnded;
    }
    static void Doc_LispCancelled(object sender, EventArgs e) {
        var doc = (Document)sender;
        _docFlagsMap[doc] &= ~RunFlag.LispRuning;
        // 结束和取消
        if(!_docFlagsMap[doc].HasFlag(RunFlag.LispEnded))
            _docFlagsMap[doc] |= RunFlag.LispEnded;
        if(!_docFlagsMap[doc].HasFlag(RunFlag.LispCancelled))
            _docFlagsMap[doc] |= RunFlag.LispCancelled;
    }


    // 刷新窗口
#if NET45_OR_GREATER
    [MethodImpl(MethodImplOptions.AggressiveInlining)]  
#endif
    static bool Refresh(IntPtr handle) {
        [DllImport("user32.dll", SetLastError = true)]
        static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
        const int SWP_NOMOVE = 0x0002;
        const int SWP_NOSIZE = 0x0001;
        return SetWindowPos(handle, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
    }
}

调用

public class TestNewlyThread {
    [JAutoGo]
    public void CmdStartOnce(Document doc) {
        JAutoTask.Init();
        NewlyThread(ed => ed.WriteMessage("文档首次打开位置初始化"));
    }

    // 进程最开始初始化呢?...上下文有没有错误呢?如果有就需要等了.
    [JAutoGo(Sequence.StartFirst)]
    public void CmdStartFirst() {
        JAutoTask.Init();
        NewlyThread(ed => ed.WriteMessage("进程最开始初始化"));
    }

    static Editor? GetEditorValid() {
        var dm = Acap.DocumentManager;
        var doc = dm.MdiActiveDocument;
        var ed = doc?.Editor;
        if (ed is not null 
            // && Acap.IsQuiescent
            // && Utils.IsEditorReady
            // && ed.CurrentViewportObjectId != ObjectId.Null
            && ed.IsQuiescent) {
            return ed;
        }
        return null;
    }

    // 新建一个线程,忙等待
    public static void NewlyThread(Action<Editor> action){
        // 直接执行
        var ed = GetEditorValid();
        if(ed is not null) action.Invoke(ed);

        // 异步执行
        int loopState = 0;
        new Thread(() => {
            while (Interlocked.CompareExchange(ref loopState, 0, 0) == 0) {
                JAutoTask.Post(() => {
                    var ed = GetEditorValid();
                    if(ed is null) return;
                    // 设置为1结束循环,排除其他Post任务执行
                    if (Interlocked.CompareExchange(ref loopState, 1, 0) == 0)
                        action.Invoke(ed);
                });
                if (Interlocked.CompareExchange(ref loopState, 0, 0) == 0)
                    Thread.Sleep(100);
            }
        }).Start();
    }
}

调用

接口例子

派生类继承 IAutoGo 接口,然后实现下面三个方法.

namespace JoinBox;

public class Test : IAutoGo {
    public Sequence SequenceId() {
        // 最先,最好只使用一次,写注册表+设置信任路径
        // 其余使用 Last 或者 Once
        return Sequence.StartFirst;
    }

    // 构造函数
    public Test() {

    }

    public void Initialize() {
        var dm = Acap.DocumentManager;
        var doc = dm.MdiActiveDocument;
        doc?.Editor.WriteMessage("接口初始化是StartFirst 注册表加载不会打印输出\n\r");
    }

    // 关闭时候执行
    // 此处用于释放安全退出,例如卸载注册表/跨进程通讯
    // 用户直接关闭CAD进程,此处是不会执行的,所以是弱执行
    public void Terminate() {
        // CAD主窗口已经回收(打印无效)
        var dm = Acap.DocumentManager;
        var doc = dm.MdiActiveDocument;
        doc?.Editor.WriteMessage("执行释放");
        MessageBox.Show("执行释放");
        // VS输出窗口打印(打印成功)
        Debug.WriteLine("执行释放");
    }
}

特性例子

如果你觉得接口很麻烦,那么就用特性吧,
存在启动的两种模式问题已经被分解成不同的参数上面处理.

namespace JoinBox;

public class TestClass {
    [JAutoGo(Sequence.StartFirst)]
    [JAutoGo(Sequence.StartLast)]
    public void ProcessInitialize() {
        Debug.WriteLine("用于进程初始化时候执行");
        var dm = Acap.DocumentManager;
        var doc = dm.MdiActiveDocument;
        doc?.Editor.WriteMessage("特性初始化是StartFirst/Last 注册表加载不会打印输出\n\r");
    }

    // 无论何种加载方式,此处doc肯定不为空,可以打印输出
    [JAutoGo]
    public void DocumentFirstOpen(Document doc) {
        Debug.WriteLine("JAutoGo文档首次开启" + doc.Name);
        doc.Editor.WriteMessage("无论何种加载方式都能输出: 惊惊博客https://www.cnblogs.com/JJBox \n\r");
    }

    [JAutoGo(Sequence.StartDocs)]
    public void DocumentEveryOpen(Document doc) {
        Debug.WriteLine("文档每次开启" + doc.Name);
    }

    [JAutoGo(Sequence.EndDocs)]
    public void DocumentEveryClosed(Document doc) {
        Debug.WriteLine("文档每次关闭" + doc.Name);
    }

    [JAutoGo(Sequence.EndOnce)]
    public void DocumentLastClosed(Document doc) {
        Debug.WriteLine("文档最后关闭" + doc.Name);
    }

    [JAutoGo(Sequence.EndLast)]
    public void ProcessTerminate() {
        // CAD主窗口已经回收(打印无效)
        var dm = Acap.DocumentManager;
        var doc = dm.MdiActiveDocument;
        doc?.Editor.WriteMessage("进程关闭时");
        MessageBox.Show("进程关闭时");
        // VS输出窗口打印(打印成功)
        Debug.WriteLine("进程关闭时");
    }

}

定义命令和检测重复

因为命令定义有几种方式:
1,命令特性定义
如果你在同一个dll中写了两个名称相同的命令,
Acad只会Editor上面输出异常信息"无法加载程序集...",
而没有提示冲突的命令名称.
我们利用初始化运行找它出来,
不过我不建议大家用这个了,因为还可以用其他定义方式.
2,自定义命令特性
然后通过Commands.Add(),不过Acad08没有这个函数.
自定义命令特性AOP切面
3,动态编译
本质上也是利用命令特性,只是Acad08的补充方案.
4,空闲事件
通过键盘钩子拦截命令栏(输入法切换工程),
检测空格和回车键认为发送命令(注意用户输入字符串中的空格要跳过),
再调用JAutoTask.Post发送到空闲事件上面.

namespace JoinBox;

public class CmdCheckHelper {
// 命令和类名
struct CommandInfo {
    public string GlobalName;
    public string FullName;
}

[JAutoGo]
public void CmdCheck(Document doc) {
    // 获取所有相关命令信息,反射程序集全部dll
    var commandInfos = AutoClass.AppDomainGetTypes()
       .SelectMany(type => 
           type.GetMethods()
               .SelectMany(method =>
               method.GetCustomAttributes(true)
                   .OfType<CommandMethodAttribute>()
                   .Select(att => new CommandInfo {
                       GlobalName = att.GlobalName,
                       FullName = type.FullName + "." + method.Name
    }))).ToList();

    // 命令分组并统计重复,记录类名
    var map = commandInfos.GroupBy(info => info.GlobalName)
        .Where(group => group.Count() > 1)  // 排除不重复的
        .ToDictionary(group => group.Key, 
            group => (group.First().FullName, group.Count()));

    if (map.Count() == 0) return;

    doc.Editor.WriteMessage("多个DLL中存在重复命令:");
    foreach (var kvp in map) {
        doc.Editor.WriteMessage($"命令:{kvp.Key},重复次数:{kvp.Value.Item2},位于:{kvp.Value.Item1}\n");
    }
}
}

注册表加载

namespace JoinBox;

public class JAutoRegisterHelper {

    // 获取AutoCAD的Applications键
    // Acad2012版 RegistryProductRootKey属性
    // Acad2014版 MachineRegistryProductRootKey属性
    // string _key = HostApplicationServices.Current.MachineRegistryProductRootKey;
#if NET35
    string _key = HostApplicationServices.Current.RegistryProductRootKey; //这里浩辰读出来是""
#else
    string _key = HostApplicationServices.Current.UserRegistryProductRootKey;
#endif
    const string _applications = "Applications";
    const string _appName = nameof(JAutoRegister);

[JAutoGo(Sequence.StartFirst)]
[CommandMethod(nameof(JAutoRegister))]
public void JAutoRegister() {
    // 信任加载
    // Acad年份获取 https://www.cnblogs.com/JJBox/p/18511807
    if (VersionTool.CurrentYear >= 2014){
        Acap.SetSystemVariable("secureload", 0); // Acad2014加载Lisp不警告 
    }
    Acap.SetSystemVariable("acadlspasdoc", 1); // 将acad.lsp文件加载到每一个文档

    using RegistryKey regAcadProdKey = Registry.CurrentUser.OpenSubKey(_key);
    if(regAcadProdKey is null) return;
    using RegistryKey regAcadAppKey = regAcadProdKey.OpenSubKey(_applications, true);
    if(regAcadAppKey is null) return;

    // 检查键是否存在,如果存在则更新路径,那就不检查得了
    // string[] subKeys = regAcadAppKey.GetSubKeyNames();
    // var k = subKeys.FirstOrDefault(a=> a == _appName);
    // if (k is not null) return;

    // 获取本模块的位置
    string dllFile = Assembly.GetExecutingAssembly().Location;
    // 注册插件
    using RegistryKey reg = regAcadAppKey.CreateSubKey(_appName);
    // 描述,可选
    reg.SetValue("DESCRIPTION", _appName, RegistryValueKind.String);
    // 控制加载时机
    // 01,检测到代理对象时加载插件
    // 02,启动时加载插件
    // 04,启动一个命令时加载插件
    // 08,用户或其它插件请求时加载插件
    // 16,不加载插件
    // 32,透明加载插件
    reg.SetValue("LOADCTRLS", 2, RegistryValueKind.DWord);
    // 加载完整路径和文件名
    reg.SetValue("LOADER", dllFile, RegistryValueKind.String);
    // 键值1是.NET程序集文件
    reg.SetValue("MANAGED", 1, RegistryValueKind.DWord);
}

[CommandMethod(nameof(JAutoUnRegister))]
public void JAutoUnRegister() {
    using RegistryKey regAcadProdKey = Registry.CurrentUser.OpenSubKey(_key);
    if(regAcadProdKey is null) return;
    using RegistryKey regAcadAppKey = regAcadProdKey.OpenSubKey(_applications, true);
    if(regAcadAppKey is null) return;
    var names = regAcadAppKey.GetSubKeyNames();
    if (names.Contains(_appName)) {
        regAcadAppKey.DeleteSubKeyTree(_appName);
    }
}
}
posted @ 2019-05-11 20:49  惊惊  阅读(2649)  评论(2编辑  收藏  举报