Revit.Async
这是一个基于.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,即可在外部事件处理完成之后,获取结果并继续剩下的其他业务逻辑。
这里同样提供两张截图:
Revit API
Revit.Async
如果你对基于任务的异步编程模型(TAP)还不太熟悉,这里有两篇微软官方提供的资料,相信可以帮助你更好地理解.NET异步机制。
https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/task-asynchronous-programming-model
示例
常规做法 ( 不使用 Revit.Async )
[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();
}
}
使用Revit.Async
[Transaction(TransactionMode.Manual)]
public class MyRevitCommand : IExternalCommand
{
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
//总是提前在Revit API的执行上下文中,初始化RevitTask
RevitTask.Initialze();
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
RevitTask.Initialize();
//提前注册外部事件
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;
}
}