- 积分
- 10875
- 明经币
- 个
- 注册时间
- 2015-8-18
- 在线时间
- 小时
- 威望
-
- 金钱
- 个
- 贡献
-
- 激情
-
|
本帖最后由 你有种再说一遍 于 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的自定义图元更好,但是这是阻碍进步的方式吗?知道原理不就好了,遍历一遍很简单.
(完) |
|