你有种再说一遍 发表于 2024-8-30 02:23:20

基础篇-AOP和事件

本帖最后由 你有种再说一遍 于 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;
            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);
      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特性标记命令
    public static void cs1() {
      MessageBox.Show("Cs1");
    }

    // 同上
    public static void cs2() {
      MessageBox.Show("Cs2");
    }
}

// 自定义CommandFlags特性

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/inspirefunction/ifoxcad/blob/jing/tests/TestAcad08/%E6%8B%89%E4%BC%B8%E5%A1%AB%E5%85%85/02.%E6%8B%89%E4%BC%B8%E5%A1%AB%E5%85%85%E4%BA%8B%E4%BB%B6.cs

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

## 图形系统事件
永久反应器(事件)是cad驱动的,例如标注关联,填充边界关联,约束器等等.
临时则是我们自己驱动的,例如伪自定义墙体和墙体交碰的地方,自动断开.
差别在于,数据库没有保留一份表,需要遍历全图构造索引,所以用Arx的自定义图元更好,但是这是阻碍进步的方式吗?知道原理不就好了,遍历一遍很简单.
(完)

zjy2999 发表于 2024-8-30 08:27:39

曲高和寡!!

czb203 发表于 2024-8-30 09:02:32

惊总学术大佬,扎实的理论基础:lol,膜拜中
页: [1]
查看完整版本: 基础篇-AOP和事件