明经CAD社区

 找回密码
 注册

QQ登录

只需一步,快速开始

搜索
查看: 461|回复: 2

[图形系统] 基础篇-AOP和事件

[复制链接]
发表于 2024-8-30 02:23:20 | 显示全部楼层 |阅读模式
本帖最后由 你有种再说一遍 于 2024-8-31 21:09 编辑

AOP和事件
# AOP切面编程
c#之所以比Lisp强大,其中一个原因就是反射.
PS:反射我会啊,序列化就需要反射字段嘛,还有反射接口嘛...
反射之所以牛,那肯定不是做序列化这种简单的东西啦.

曾几何时,大家会冥冥之中觉得,想在函数执行时候加一个执行前/执行后的效果.
但是函数是很难修改的,函数体body被编译成MSIL中间语言了,没有经过一定汇编培训的话是困难重重.
并且想改的函数或许已经写了很多地方了,直接加入两个前后函数并不合适,未来还可能存在修改,又不要这两个前后函数了.

有什么是代码上面的技巧去实现呢?

## 方案一,装饰器模式:
无论是接口实现还是事件实现,装饰器模式它都有一定的问题.
缺点:接口需要实现一个新类,并且如果之前没有考虑到接口,那么还需要更换旧实现里面的函数.
优点:扩展调用链方便,并且还可以装饰器套装饰器.例如你需要同时支持json,xml,db写入配置,但是你已经写了json,那么就可以这样一层套一层.

```c#
// 接口
public interface IService{
    void Execute();
}
// 服务(原始对象)
public class Service : IService{
    public void Execute() {
        Console.WriteLine("Service Executed");
    }
}
// 装饰器(把原始对象生命周期纳入本类字段中)
public class ServiceDecorator : IService {
    private readonly IService _service;
    public ServiceDecorator(IService service) {
        _service = service;
    }
    public void Execute() {
        Console.WriteLine("开始,插入新方法");
        Console.WriteLine("展示返回值去控制是否回调原始方法,在修复bug代码时候就可以实现此功能");
       Console.WriteLine("输入字符a回调,否则屏蔽:");
        string input = Console.ReadLine();
        // 这样就可以屏蔽方法
        if (input == "a") {   
            _service.Execute(); //原始方法
        }
        Console.WriteLine("结束,插入新方法");
    }
}

// 调用:
class Program {
  static void Main(string[] args) {
      // 创建服务对象
      IService s = new Service();
      // 创建装饰器,并将服务对象作为参数传递给装饰器,这个方法IFox用在填充类了
      s = new ServiceDecorator(s);
      // 调用装饰后的执行方法(旧方法调用就不需要改动了)
      s.Execute();
      Console.ReadKey();
    }
}
```

## 方案二,AOP切面编程:
举个例子,反射全部命令,然后全部回调到一个S方法上面.那么这个S方法内部就可以写,执行前调用A方法,执行后调用B方法.

这有什么用呢?
可以在S方法内部: try{cmd()}catch{}
不就是把全部命令的错误都拦截了吗?

```c#
// 插件命令管理器类
public static class CommandManager {
    // 定义命令组名称
    private const string GroupName = "My";

    // 键为命令名称(全大写),值为方法名称
    private static Dictionary<string, string> _cmdMap = new();

    // 加载插件命令的方法,应放在ExtensionApplication接口的Initialize事件中调用
    public static void AddCommands() {
        // 权限验证
        if (true) return;
        _cmdMap.Clear();

        // 反射本程序集,获取所有带有CustomFlags特性的方法,并添加插件命令
        foreach (var info in typeof(CommandManager).GetMethods()) {
            object[] atts = info.GetCustomAttributes(typeof(CustomFlags), false);
            if (atts.Length <= 0) continue;

            // 获取CustomFlags特性
            var cu = (CustomFlags)atts[0];
            string cmdName = info.Name.ToUpper();
            _cmdMap.Add(cmdName,info.Name);

           // 添加cad命令栈(acad08没有此方法,采用动态编译跨程序域调用,或者直接通过_cmdMap在输入钩子调用)
            Utils.AddCommand(GroupName,
               cmdName,
               cmdName,
               cu.Parameter,
               OnCommand);
        }
    }

    // 统一将全部命令回调到此方法,实现AOP.
    private static void OnCommand() {
        // 将焦点设置到DWG视图
        Utils.SetFocusToDwgView();
        // 获取当前正在执行的命令名称(acad08可以采用键盘钩子拦截获取)
        string curCmd = Env.Editor.Document.CommandInProgress.ToUpper();
        // 反射获取方法并调用...这里没有缓存?
        MethodInfo methodInfo = typeof(CommandManager).GetMethod(_cmdMap[curCmd]);
        try{
          methodInfo?.Invoke(null,null);
        }catch{...}
    }

    // 卸载插件命令的方法,通常在插件过期后
    public static void RemoveCommands() {
        if (_cmdMap.Count == 0) return;

        // 通过反射移除所有添加的命令
        foreach (var key in _cmdMap.Keys) {
            Utils.RemoveCommand(GroupName, key);
        }
    }

    // 以下是自定义的插件命令示例
    [CustomFlags(CommandFlags.UsePickSet)] // 使用CustomFlags特性标记命令
    public static void cs1() {
        MessageBox.Show("Cs1");
    }

    [CustomFlags(CommandFlags.UsePickSet)] // 同上
    public static void cs2() {
        MessageBox.Show("Cs2");
    }
}

// 自定义CommandFlags特性
[AttributeUsage(AttributeTargets.Method)]
public class CustomFlags : Attribute {
    public CommandFlags Parameter { get; set; }
    public CustomFlags(CommandFlags commandFlags) {
        Parameter = commandFlags;
    }
}
```

c/c++上面其实只能埋钩子,使用函数指针作为钩子的触发点
```c
typedef void (*HookFunction)(void);
HookFunction hook1 = NULL;
HookFunction hook2 = NULL;
void trigger_hook() {
    // 开头的钩子函数
    if (hook1) {
        hook1();
    }
    // 执行原始函数...
    // 结束的钩子函数
    if (hook2) {
        hook2();
    }
}
// 外部实现函数指针传递到参数中
void set_hook1(HookFunction func) {
    hook1 = func;
}
void set_hook2(HookFunction func) {
    hook2 = func;
}
```

# 事件
acad事件是单线程的,所以我们可以相信命令前/命令后,两个独立事件的有序性.
而winform,WPF,键盘钩子,鼠标钩子,文件夹监控等等,这些事件很多是多线程的.
因此我们不能相信打开文档是谁先被哪个cad打开(延时不是一种好方法,而要采取信号量)
并且我们不能在跨线程打开图纸:
http://bbs.mjtd.com/thread-191035-1-1.html

## 封装手法
多个事件应该封装到每个命令类中,而不是每个命令都去改同一个全局事件,因为会破坏了开闭原则.
因为-=要考虑卸载顺序,因此我喜欢归纳+=-=到同一个方法内:
void LoadHelper(bool isLoad) {
  if (isLoad) { xx+=...}
  else { xx-=...}
}

## 单例事件
如果在命令函数中+=事件,那么重复执行命令会被多次+=,也就是说cad事件并不是单例事件(没错单线程也存在需要单例).
既然cad没做,它也不应该做,那么我们就需要自己处理了.

方案1:在命令方法中先-=再+=
第二次生效时候就是移除第一次的+=,确保事件存在.缺点就是只适用在单线程事件.

方案2:类字段实现
不写在命令函数内,而是写在类字段上面.

loadx重复加载dll的时候,实现覆盖功能,你会发现事件也是被执行两次,因为它们隶属于不同的程序域.
不过这方法是避免不了dll动态加载时候出现重复事件,方案1也是避免不了的,因为它也加载了.
若想避免,卸载执行时候遍历全部事件状态机去停止这个事件.(什么是状态机就接着往下看)
这个时候,谁是单例区域呢?那肯定是loadx所在的主程序域,直接在loadx工程上面提供管道通讯,实现跨程序域事件执行,那么进行dispose岂不是也是跨域?

方案3:自己封装单例事件
事件本质就是一个委托链,那么可以自己封装一次,add时候进行hashset,尤其是把cad基础的再封装一次,实现弱事件.
弱事件:它允许事件订阅者在不需要显式注销事件处理器的情况下,自动解除对事件的订阅.这在防止内存泄漏方面非常有用,尤其是在订阅者和发布者之间存在长生命周期依赖关系时.
使用WeakReference,卸载程序域时候,才能移除之后不会触发异常,并且WPF树形图内存不足自动释放图片等等.
我们还可以用一种叫中央事件处理器的方式集中全部事件的执行,本质也是AOP.

方案4:状态机字段
bool flag:
命令执行时候判断flag==true表示挂载事件了.

枚举字段 state:
state是多个状态的,如果为run就是正在执行中.
当卸载发生时,需要先停止鼠标键盘钩子多线程事件尤为有用.例如我选择了填充,触发选择集事件,里面关联了鼠标钩子,执行期间发生了卸载,令鼠标钩子失去写入目标的变量,产生了不可预估的错误.
既然单线程和多线程在处理事件上面的差异,那么我们如何统一一个步骤实现呢?
因此我们需要在命令类中需要加入一个枚举状态state(状态机),放在每个事件开头,卸载前设置stop,这样就堵住命令里全部事件的下一次执行,实现了安全卸载.

IFox拉伸填充事件:
https://gitee.com/inspirefunctio ... 4%BA%8B%E4%BB%B6.cs

# cad事件
## 否决命令
填充边界事件,有个用来否决命令执行的Acap.ManagerDocumentLockMode事件,
一定注意,只能e.Vote()后再发送异步命令,立马退出.
不能在e.Vote()后面再继续创建任何面板,因为这种插入任务将导致此事件再进入,进入之后再进入,无限递归,造成不可知的异常,可能是无效/爆栈/致命错误等信息弹出.
而发送异步命令相当于是一个异步队列,等到vote事件退出之后才执行.
而发送的命令不想污染命令历史,可以设置CommandFlags.NoHistory.
PS:特性面板,"op用户配置注册表"是会记录上次ctrl+1特性面板是打开的.

## 图形系统事件
永久反应器(事件)是cad驱动的,例如标注关联,填充边界关联,约束器等等.
临时则是我们自己驱动的,例如伪自定义墙体和墙体交碰的地方,自动断开.
差别在于,数据库没有保留一份表,需要遍历全图构造索引,所以用Arx的自定义图元更好,但是这是阻碍进步的方式吗?知道原理不就好了,遍历一遍很简单.
(完)
发表于 2024-8-30 09:02:32 | 显示全部楼层
惊总学术大佬,扎实的理论基础,膜拜中
您需要登录后才可以回帖 登录 | 注册

本版积分规则

小黑屋|手机版|CAD论坛|CAD教程|CAD下载|联系我们|关于明经|明经通道 ( 粤ICP备05003914号 )  
©2000-2023 明经通道 版权所有 本站代码,在未取得本站及作者授权的情况下,不得用于商业用途

GMT+8, 2024-11-22 19:32 , Processed in 0.164251 second(s), 22 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表