使用Rx响应式编程管理CAD二次开发中的事件

Rx 简介

Rx 的全称是 Reactive Extensions,是一套用于处理事件流和异步数据流的编程模型。

在传统事件编程中,我们通常分别订阅事件、编写事件处理方法,并在适当的时候解除订阅。Rx 则把事件、定时器、异步结果等随时间产生的数据统一表示为数据流,再使用类似 LINQ 的方式进行处理。

可以把 Rx 简单理解为:

使用可组合的方式处理随时间不断产生的数据。

Rx学习成本较低,为了方便C#开发者使用,微软将大部分方法设计的和Linq非常相似,可以非常容易的入手。

安装起来也非常简单,只需要在nuget里引用System.Reactive包即可

核心概念

Rx 中最常见的几个概念如下:

  • IObservable<T>:表示可以被订阅的数据流。
  • Subscribe():订阅数据流并接收数据。
  • OnNext:数据流产生了一个新数据。
  • OnError:数据流发生错误并结束。
  • OnCompleted:数据流正常结束。
  • IDisposable:表示一次订阅,调用 Dispose() 可以取消订阅。

Rx 的主要作用

Rx 比较适合处理以下问题:

  • 监听连续发生的事件。
  • 对事件进行过滤、转换和组合。
  • 合并多个不同来源的数据流。
  • 控制事件触发频率。
  • 管理临时事件订阅的生命周期。
  • 在对象不再使用时统一取消订阅,减少事件未解绑导致的内存泄漏。

Rx 常用操作符包括:

操作符作用
Select转换数据
Where过滤数据
Take只接收指定数量的数据
Merge合并多个数据流
CombineLatest组合多个数据流的最新值
Throttle等待事件暂时停止后再处理
DistinctUntilChanged忽略与上一次相同的数据

简单示例

下面的代码创建一个数据流,并订阅其中的数据:

IObservable<int> numbers = Observable.Range(1, 5);

IDisposable subscription = numbers.Subscribe(value =>
{
    Console.WriteLine(value);
});

subscription.Dispose();

Subscribe() 返回一个 IDisposable。当不再需要接收数据时,调用 Dispose() 即可结束本次订阅。

Rx 在CAD二次开发中的应用

Rx在CAD中适合处理事件,可用于管理临时事件,借助Rx中的类可以对事件更好的管理和拆除。

下面进行一些举例

例子1:使用Rx订阅PointMonitor事件。

在前面的文章中我们讲过Editor.PointMonitor事件。

使用鼠标事件替代Jig实现预览

其实cad中有很多其他类似的事件,如果你每次用的时候都要定义一个类似PointMonitorEventManager的容器封装一下,其实是件非常麻烦的事情,如果你的使用频率不高,那么使用Rx简化是一个不错的选择。也可以选择使用Rx进行封装。

Rx中将事件注册成数据流的方法有很多,由于CAD中的事件不遵循微软的 EventArgs<T> 标准,所以我们可以采用名字的方式来注册。

using var iDisposable = Observable.FromEventPattern(ed, nameof(ed.PointMonitor))  
    .Select(x => (PointMonitorEventArgs)x.EventArgs)  
    .Subscribe(args =>  
    {  
        // var pt = args.Context.ComputedPoint;  
    }

上述代码中在Subscribe订阅的时候,事件被注册,在iDisposable被释放时事件被拆除。

例子2:使用Rx订阅CommandWillStart事件

先上代码


    private static IDisposable? documentManagerSubscribe;

    public static void Init()
    {
        documentManagerSubscribe = Observable
            .FromEventPattern(Acap.DocumentManager, nameof(Acap.DocumentManager.DocumentActivated))
            .Select(x => x.EventArgs)
            .OfType<DocumentCollectionEventArgs>()
            .Select(x => x.Document)
            .Subscribe(DocumentActivatedHandler);

        DocumentActivatedHandler(Acap.DocumentManager.MdiActiveDocument);
    }

    private static readonly SerialDisposable _serialDisposable = new();
    
    private static void DocumentActivatedHandler(Document? doc)
    {
        if(doc is null)
            return;
        _serialDisposable.Disposable = Observable.FromEventPattern(doc,nameof(doc.CommandWillStart))
            .Select(x=>(CommandEventArgs)x.EventArgs)
            .Publish()
            .RefCount()
            .Subscribe(x =>
            {
                try
                {
                    var command = x.GlobalCommandName;
                    // do something
                }
                catch (Exception ex)
                {
                    Log.Error("CommandWillStart事件发生错误" + ex.Message);
                }
            });
    }

在Init里对DocumentManager文档管理器事件进行订阅,使其在文档被激活为活动文档时,执行DocumentActivatedHandler方法。
DocumentActivatedHandler方法中对文档的CommandWillStart事件进行订阅,并在事件触发时执行某些操作。

documentManagerSubscribe变量的作用是保持事件的生命周期,在此变量销毁时事件将被Rx自动拆除,所以将其放在静态变量中持久化。

SerialDisposable是Rx中的一个单IDisposable接口管理容器,每次对其Disposable属性进行赋值时,它将自动将旧的对象进行释放(如果有),并将其赋上新值,这样非常符合我们的使用场景,有新的文档变成了活动文档,那么刚刚的活动文档就变成了非活动文档,正好通过此特性将旧文档上的事件进行拆除,这里不需要判断旧文档是否还在,Rx会帮你处理。

这里注意到,订阅CommandWillStart事件时使用了PublishRefCount,他们起到的作用是:如果在程序的不同地方多次订阅了同一事件,那么Rx只会实际上在对象上订阅一次,再通过委托进行二次分发。