这是一个基于.NET任务异步模型(TAP),对Revit API外部事件机制(ExternalEvent)的增强库。使用这个库可以让你更加自然地基于Revit API书写代码,而不必被Revit API的执行上下文所困扰。
如果你曾经在Revit API的开发过程中,遇到过"Cannot execute Revit API outside of Revit API context"这样的异常,这个异常抛出的一个典型场景是当你尝试在非模态窗体中调用Revit API,那么这个库也许能够帮到你。
上面这个异常,对Revit API不熟悉的新开发者会感到困惑,他们可能并不理解ExternalEvent.Raise()的真正含义。ExternalEvent.Raise()方法,并不马上执行你写在IExternalEventHandler.Execute()方法中的代码,而是把预先注册好的IExternalEventHandler实例,添加到Revit内部的任务队列中,Revit会以单线程的方式,循环地从任务队列顶部抓取一个IExternalEventHandler实例,执行Execute方法。换言之,ExternalEvent.Raise()只是发起了一个异步任务。
但是Revit API提供的IExternalEventHandler接口过于简单(方法签名void Execute(UIApplication app)
),使得基于此接口的业务实现,很难动态地获取参数,也很难返回某一次执行的结果,必须要借助第三方的数据转存才能实现业务逻辑的串联,这使得本来连贯的业务开发,变得支离破碎。
如果你熟悉JavaScript ES6提供的Promise异步以及浏览器异步渲染机制,或者你理解.NET中的Task异步任务以及桌面STA应用的异步渲染机制,你就会发现,Revit提供的ExternalEvent与上述两种异步机制何其相似,我们完全可以基于Revit提供的异步能力,结合.NET基于任务的异步模型,提供一套更加简单易用的异步调用机制,以取代羸弱的ExternalEvent。
Revit.Async这个库,正是对这套异步机制的一种实现,重点解决外部事件传参以及外部事件结果的回调,使得开发者可以更加自然地基于Revit API书写代码,而不必被Revit API的执行上下文所困扰。Revit.Async这个库内部,会自动将待执行的方法,委托给内部定义的特定外部事件,Raise这个事件之后,立即向调用方返回一个用于接收事件回调的Task,调用方只需要await这个Task,即可在外部事件处理完成之后,获取结果并继续剩下的其他业务逻辑。
这里同样提供两张截图:
如果你对基于任务的异步编程模型(TAP)还不太熟悉,这里有两篇微软官方提供的资料,相信可以帮助你更好地理解.NET异步机制。 https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap
我经常被问到一个问题,Revit.Async是否会使用多线程运行Revit API。
很可惜,并不是这样的,千万不要被名字里的"Async"误导了。
实际上,"Async"真的是躺枪了,由于.NET在一大票多线程方法的命名上使用了"Async"后缀,导致了一个看到"Async"就想到“多线程”的普遍认知。
关于这个问题的解释,需要从“异步编程”与“多线程编程”的区别展开。
引用来自stackoverflow的一句话:
"Threading is about workers; asynchrony is about tasks". (线程关心的是任务并行度,异步关心的是任务的分配)
同样一个回答还给出了一个比喻来说明问题:
假设你是一个餐馆的厨师,这个餐馆提供煎蛋和吐司两种菜品:
同步:先做煎蛋,然后做吐司
单线程异步:开始做煎蛋,并设置定时器,马上开始做吐司,同样设置定时器。两个菜品都在制作的过程中的时候,你可以借机打扫厨房的卫生,或者打两把王者。煎蛋的定时器响了,你就去把煎蛋装盘送餐,吐司的定时器响了,你就去把吐司装盘送餐
多线程异步: 你忙不过来于是招了另外两个厨子,一个专门负责煎蛋,另一个则负责吐司。现在新的问题来了,你得更好地协调两个厨子的工作,让他俩不会因为争抢厨具而引发冲突导致效率下降,而且你还得给他们发工资
很多开发者产生“异步==多线程”这种误解的另一个原因在于,往往多线程编程都会伴随着异步编程。在大部分的UI应用开发中,当我们使用多线程执行一些数据获取、复杂计算等任务时,我们希望将产生的结果“返回”到主线程上来更新UI显示的内容,而这个“返回”正是异步编程发挥作用的地方。
例如在Windows Form应用开发中,如果你想要在一个后台线程中更新UI界面,你需要使用Invoke
方法把一个Delegate
发送到主线程排队执行。
又例如在WPF应用开发中,如果你想要在一个后台线程中更新UI界面,你需要使用Dispatcher
对象把一个Delegate
发送到主线程排队执行。
在Revit中这个过程也一样。Revit API可以用来修改模型,而Revit本身都是在主线程上执行模型的修改操作的,同时也要求Revit API必须在主线程上执行,我理解这一定程度上是对线程安全的考量。
如果你想要在一个后台线程中更新Revit模型,你需要使用ExternalEvent
对象的Raise()
方法把一个IExternalEventHandler
发送到主线程排队执行,IExternalEventHandler
里封装了Revit API的调用。而这也正是Revit提供的异步编程模型。
回到Revit.Async这个库,它仅仅只是对上述Revit异步编程模型的一个封装,它的目的也只是给开发者提供一个开箱即用的异步编程体验,它无法逾越Revit API的调用限制。
所以,Revit.Async里没有一星半点多线程的东西。
在使用RevitTask的任何功能之前,请务必在一个有效的Revit API上下文中执行RevitTask的初始化动作。
RevitTask.Initialize(app);
以下是一些常见的有效Revit API上下文:
- IExternalCommand.Execute method
- IExternalApplication.OnStartup method
- IExternalEventHandler.Execute method
- Revit API event handlers
- IUpdater
Revit.Async的核心功能都是通过RevitTask.RunAsync()
方法对外暴露的。
RevitTask.RunAsync()
方法有多个重载来支持各种调用方式。
Task RunAsync(Action action)
await RevitTask.RunAsync(() =>
{
// 在这里执行同步代码,不返回任何结果
})
Task RunAsync(Action<UIApplication> action)
await RevitTask.RunAsync((uiApp) =>
{
// 在这里执行同步代码,不返回任何结果,通过uiApp参数访问Revit模型数据库
})
Task<T> RunAsync<T>(Func<T> func)
var result = await RevitTask.RunAsync(() =>
{
// 在这里执行同步代码,返回一个结果
return 0;
})
// result 为 0
Task<T> RunAsync<T>(Func<UIApplication, T> func)
var result = await RevitTask.RunAsync((uiApp) =>
{
// 在这里执行同步代码,返回一个结果,通过uiApp参数访问Revit模型数据库
return 0;
})
// result 为 0
Task RunAsync(Func<Task> func)
await RevitTask.RunAsync(async () =>
{
// 在这里执行异步代码,不返回任何结果
})
Task RunAsync(Func<UIApplication, Task> func)
await RevitTask.RunAsync(async (uiApp) =>
{
// 在这里执行异步代码,不返回任何结果,通过uiApp参数访问Revit模型数据库
})
Task<T> RunAsync<T>(Func<Task<T>> func)
var result = await RevitTask.RunAsync(async () =>
{
// 在这里执行同步代码,返回一个结果,以HTTP请求为例
var httpResponse = await http.Get("server api url");
//
return httpResponse;
})
// result 为 http 请求的响应体
Task<T> RunAsync<T>(Func<UIApplication, Task<T>> func)
var result = await RevitTask.RunAsync(async (uiApp) =>
{
// 在这里执行同步代码,返回一个结果,通过uiApp参数访问Revit模型数据库, 以HTTP请求为例
var httpResponse = await http.Get("server api url");
//
return httpResponse;
})
// result 为 http 请求的响应体
[Transaction(TransactionMode.Manual)]
public class MyRevitCommand : IExternalCommand
{
public static ExternalEvent SomeEvent { get; set; }
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
//提前注册包含业务逻辑的外部事件
SomeEvent = ExternalEvent.Create(new MyExternalEventHandler());
var window = new MyWindow();
//打开非模态窗体
window.Show();
return Result.Succeeded;
}
}
public class MyExternalEventHandler : IExternalEventHandler
{
public void Execute(UIApplication app)
{
//在这里执行Revit API调用,以响应窗体中按钮的点击事件
//想要在这里获取一些参数,同时在执行完之后返回一些结果,将会非常地麻烦
var families = new FilteredElementCollector(app.ActiveUIDocument.Document)
.OfType(typeof(Family))
.ToList();
//忽略掉一些其他业务代码
}
}
public class MyWindow : Window
{
public MyWindow()
{
InitializeComponents();
}
private void InitializeComponents()
{
Width = 200;
Height = 100;
WindowStartupLocation = WindowStartupLocation.CenterScreen;
var button = new Button
{
Content = "Button",
Command = new ButtonCommand(),
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
};
Content = button;
}
}
public class ButtonCommand : ICommand
{
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
//直接在按钮响应中执行Revit API代码,将会得到一个异常,告诉你现在不是在Revit API的执行上下文中,无法执行Revit API
//常规做法是Raise一个包含Revit API业务逻辑的外部事件
MyRevitCommand.SomeEvent.Raise();
}
}
[Transaction(TransactionMode.Manual)]
public class MyRevitCommand : IExternalCommand
{
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
//总是提前在Revit API的执行上下文中,初始化RevitTask
// version 1.x.x
// RevitTask.Initialze();
// version 2.x.x
RevitTask.Initialize(commandData.Application);
var window = new MyWindow();
//打开非模态窗体
window.Show();
return Result.Succeeded;
}
}
public class MyWindow : Window
{
public MyWindow()
{
InitializeComponents();
}
private void InitializeComponents()
{
Width = 200;
Height = 100;
WindowStartupLocation = WindowStartupLocation.CenterScreen;
var button = new Button
{
Content = "Button",
Command = new ButtonCommand(),
CommandParameter = true,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
};
Content = button;
}
}
public class ButtonCommand : ICommand
{
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public async void Execute(object parameter)
{
//await 是.NET 4.5 的关键字, 如果是基于.NET 4.0的,请使用ContinueWith
var families = await RevitTask.RunAsync(
app =>
{
//在这里书写Revit API代码
//这里利用了Lambda表达式创建的闭包上下文,
//使得我们可以访问按钮点击事件传入的参数,以及所有的局部变量
//假设点击按钮传入的是个bool值,用来指示是否过滤出可编辑的族
if(parameter is bool editable)
{
return new FilteredElementCollector(app.ActiveUIDocument.Document)
.OfType(typeof(Family))
.Cast<Family>()
.Where(family => editable ? family.IsEditable : true)
.ToList();
}
return null;
});
MessageBox.Show($"Family count: {families?.Count ?? 0}");
}
}
IExternalEventHandler
这个接口太弱了,Revit.Async对外提供一个增强的泛型接口IGenericExternalEventHandler<TParameter,TResult>
,这个接口提供了向外部事件传参并且接收外部事件回调的基础能力,这也是Revit.Async得以实现的核心能力。
强烈建议直接从内部预定义的两个抽象基类开始派生你自己的外部事件,因为这两个基类实现了处理传参和回调的必要逻辑。
基类 | 描述 |
---|---|
AsyncGenericExternalEventHandler<TParameter, TResult> |
用来封装异步代码 |
SyncGenericExternalEventHandler<TParameter, TResult> |
用来封装同步代码 |
[Transaction(TransactionMode.Manual)]
public class MyRevitCommand : IExternalCommand
{
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
//总是提前在Revit API的执行上下文中,初始化RevitTask
// version 1.x.x
// RevitTask.Initialze();
// version 2.x.x
RevitTask.Initialize(commandData.Application);
//提前注册外部事件
RevitTask.RegisterGlobal(new SaveFamilyToDesktopExternalEventHandler());
var window = new MyWindow();
//打开非模态窗体
window.Show();
return Result.Succeeded;
}
}
public class MyWindow : Window
{
public MyWindow()
{
InitializeComponents();
}
private void InitializeComponents()
{
Width = 200;
Height = 100;
WindowStartupLocation = WindowStartupLocation.CenterScreen;
var button = new Button
{
Content = "Save Random Family",
Command = new ButtonCommand(),
CommandParameter = true,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
};
Content = button;
}
}
public class ButtonCommand : ICommand
{
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public async void Execute(object parameter)
{
var savePath = await RevitTask.RunAsync(
async app =>
{
try
{
var document = app.ActiveUIDocument.Document;
var randomFamily = await RevitTask.RunAsync(
() =>
{
var families = new FilteredElementCollector(document)
.OfClass(typeof(Family))
.Cast<Family>()
.Where(family => family.IsEditable)
.ToArray();
var random = new Random(Environment.TickCount);
return families[random.Next(0, families.Length)];
});
//Raise外部事件,传入参数,await这个异步任务,接收回调结果
return await RevitTask.RaiseGlobal<SaveFamilyToDesktopExternalEventHandler, Family, string>(randomFamily);
}
catch (Exception)
{
return null;
}
});
var saveResult = !string.IsNullOrWhiteSpace(savePath);
MessageBox.Show($"Family {(saveResult ? "" : "not ")}saved:\n{savePath}");
if (saveResult)
{
Process.Start(Path.GetDirectoryName(savePath));
}
}
}
public class SaveFamilyToDesktopExternalEventHandler :
SyncGenericExternalEventHandler<Family, string>
{
public override string GetName()
{
return "SaveFamilyToDesktopExternalEventHandler";
}
protected override string Handle(UIApplication app, Family parameter)
{
//在这里写同步代码逻辑
var document = parameter.Document;
var familyDocument = document.EditFamily(parameter);
var desktop = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
var path = Path.Combine(desktop, $"{parameter.Name}.rfa");
familyDocument.SaveAs(path, new SaveAsOptions {OverwriteExistingFile = true});
return path;
}
}
- 识别当前上下文环境,以便确定是将代码封送到外部事件,还是直接执行
- 支持外部事件取消
在使用过程中,遇到任何问题,可以通过 [email protected] 联系我,或者提issue