使用基于任务的异步模式 (TAP) 从任何执行上下文运行 Revit API 代码。
中文说明
如果您曾经遇到过 Revit API 异常,提示“无法在 Revit API 上下文之外执行 Revit API”,通常当您想要从无模式窗口执行 Revit API 代码时,您可能需要此库来挽救您的生命。
此异常的常见解决方案是使用IExternalEventHandler
包装 Revit API 代码,并提前将处理程序实例注册到 Revit 以获取触发器 ( ExternalEvent
)。要执行该处理程序,只需从任何位置触发触发器即可将处理程序排队到 Revit 命令循环中。但还有另一个问题。引发触发器后,在相同的上下文中,您不知道处理程序何时将被执行,并且从该处理程序生成一些结果并不容易。如果您确实想实现此目的,则必须手动将控制权交还给调用上下文。
如果你熟悉 JavaScript ES6,这个解决方案看起来与“Promise”的机制非常相似。实际上,我们可以利用基于任务的异步模式(TAP)来实现上述所有逻辑,在.NET中通常称为Task<T>
。通过采用 Revit.Async,可以从任何上下文运行 Revit API 代码,因为 Revit.Async 在内部自动使用IExternalEventHandler
包装您的代码,并向调用上下文生成返回值,使您的调用更加自然。
如果您不熟悉基于任务的异步模式 (TAP),以下是 Microsoft 提供的一些有用的材料:
下面是 Revit API 外部事件机制与 Revit.Async 的比较图以及两个主要部分的屏幕截图:
我经常被问到 Revit.Async 是否在后台线程中运行 Revit API。
让我们澄清一下。答案是否定的!!!!不要被“异步”这个词误导。
“异步”这个词在这里实际上是无辜的。 .NET 将一堆多线程方法命名为“Async”结尾,导致了普遍的误解。
这个问题可以从异步编程和多线程编程的区别来解释。
来自 stackoverflow 的一句话:
“线程是关于工人的;异步是关于任务的”。
来自同一 stackoverflow 答案的类比:
你正在一家餐馆做饭。一份鸡蛋和吐司的订单进来了。
同步:先煮鸡蛋,然后煮吐司。
异步、单线程:您开始煮鸡蛋并设置计时器。您开始烤面包,并设置计时器。当他们俩做饭时,你打扫厨房。当计时器响起时,您将鸡蛋从火上取下,将吐司从烤面包机中取出并享用。
异步、多线程:您再雇用两名厨师,一名负责煮鸡蛋,一名负责烤面包。现在你面临着协调厨师的问题,这样他们在共享资源时就不会在厨房里互相冲突。而且你必须付钱给他们。
人们之所以有“异步==多线程”的误解,是因为异步有很大的机会伴随着多线程而来。在大多数UI应用程序(STA)中,当我们使用多线程运行后台任务时,该任务的结果需要“返回”到UI线程才能呈现。异步参与“返回”阶段。
在 Windows 窗体应用程序中,如果要从工作线程更新 UI,则需要使用Invoke
方法将Delegate
排队到主线程以执行 UI 更新。
在 WPF 应用程序中,如果要从工作线程更新 UI,则需要使用Dispatcher
对象将Delegate
排队到主线程以执行 UI 更新。
在Revit世界中,几乎是一样的。 Revit API 用于更新模型。 Revit 在主线程上执行模型更新,并且它也需要在主线程上调用所有 API,我认为是为了线程安全。
如果要从工作线程更新模型,则需要使用ExternalEvent
对象将IExternalEventHandler
实例排队( Raise()
)到主线程以调用Revit API。这是 Revit 提供的用于安排新 API 调用的异步模式。
至于Revit.Async,它只是上述异步模式的包装。该库的目标是为异步 Revit API 提供开箱即用的体验。
Revit.Async 中绝对没有多线程。
在任何有效的 Revit API 上下文中,请在使用 RevitTask 的任何功能之前初始化 RevitTask。
RevitTask . Initialize ( app ) ;
一些有效的 Revit API 上下文包括:
Revit.Async 的主要功能由RevitTask.RunAsync()
方法公开。 RevitTask.RunAsync()
方法有多个重载。
Task RunAsync(Action action)
await RevitTask . RunAsync ( ( ) =>
{
// sync function without return value
} )
Task RunAsync(Action<UIApplication> action)
await RevitTask . RunAsync ( ( uiApp ) =>
{
// sync function without return value, with uiApp paramter to access Revit DB
} )
Task<T> RunAsync<T>(Func<T> func)
var result = await RevitTask . RunAsync ( ( ) =>
{
// sync function with return value
return 0 ;
} )
// result will be 0
Task<T> RunAsync<T>(Func<UIApplication, T> func)
var result = await RevitTask . RunAsync ( ( uiApp ) =>
{
// sync function with return value, with uiApp paramter to access Revit DB
return 0 ;
} )
// result will be 0
Task RunAsync(Func<Task> func)
await RevitTask . RunAsync ( async ( ) =>
{
// async function without return value
} )
Task RunAsync(Func<UIApplication, Task> func)
await RevitTask . RunAsync ( async ( uiApp ) =>
{
// async function without return value, with uiApp paramter to access Revit DB
} )
Task<T> RunAsync<T>(Func<Task<T>> func)
var result = await RevitTask . RunAsync ( async ( ) =>
{
// async function with return value, http request as an example
var httpResponse = await http . Get ( " server api url " ) ;
//
return httpResponse ;
} )
// result will be the http response
Task<T> RunAsync<T>(Func<UIApplication, Task<T>> func)
var result = await RevitTask . RunAsync ( async ( uiApp ) =>
{
// async function with return value, with uiApp paramter to access Revit DB, http request as an example
var httpResponse = await http . Get ( " server api url " ) ;
//
return httpResponse ;
} )
// result will be the http response
[ Transaction ( TransactionMode . Manual ) ]
public class MyRevitCommand : IExternalCommand
{
public static ExternalEvent SomeEvent { get ; set ; }
public Result Execute ( ExternalCommandData commandData , ref string message , ElementSet elements )
{
//Register MyExternalEventHandler ahead of time
SomeEvent = ExternalEvent . Create ( new MyExternalEventHandler ( ) ) ;
var window = new MyWindow ( ) ;
//Show modeless window
window . Show ( ) ;
return Result . Succeeded ;
}
}
public class MyExternalEventHandler : IExternalEventHandler
{
public void Execute ( UIApplication app )
{
//Running some Revit API code here to handle the button click
//It's complicated to accept argument from the calling context and return value to the calling context
var families = new FilteredElementCollector ( app . ActiveUIDocument . Document )
. OfType ( typeof ( Family ) )
. ToList ( ) ;
//ignore some code
}
}
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 )
{
//Running Revit API code directly here will result in a "Running Revit API outside of Revit API context" exception
//Raise a predefined ExternalEvent instead
MyRevitCommand . SomeEvent . Raise ( ) ;
}
}
[ Transaction ( TransactionMode . Manual ) ]
public class MyRevitCommand : IExternalCommand
{
public Result Execute ( ExternalCommandData commandData , ref string message , ElementSet elements )
{
//Always initialize RevitTask ahead of time within Revit API context
// version 1.x.x
// RevitTask.Initialze();
// version 2.x.x
RevitTask . Initialize ( commandData . Application ) ;
var window = new MyWindow ( ) ;
//Show modeless window
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 )
{
//.NET 4.5 supported keyword, use ContinueWith if using .NET 4.0
var families = await RevitTask . RunAsync (
app =>
{
//Run Revit API code here
//Taking advantage of the closure created by the lambda expression,
//we can make use of the argument passed into the Execute method.
//Let's assume it's a boolean indicating whether to filter families that is editable
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
接口?请改用IGenericExternalEventHandler<TParameter,TResult>
接口。它使您能够将参数传递给处理程序并接收完整结果。
始终建议从预定义的抽象类派生;它们旨在处理参数传递和结果返回部分。
班级 | 描述 |
---|---|
AsyncGenericExternalEventHandler<TParameter, TResult> | 用于执行异步逻辑 |
SyncGenericExternalEventHandler<TParameter, TResult> | 用于执行同步逻辑 |
[ Transaction ( TransactionMode . Manual ) ]
public class MyRevitCommand : IExternalCommand
{
public Result Execute ( ExternalCommandData commandData , ref string message , ElementSet elements )
{
//Always initialize RevitTask ahead of time within Revit API context
// version 1.x.x
// RevitTask.Initialze();
// version 2.x.x
RevitTask . Initialize ( commandData . Application ) ;
//Register SaveFamilyToDesktopExternalEventHandler ahead of time
RevitTask . RegisterGlobal ( new SaveFamilyToDesktopExternalEventHandler ( ) ) ;
var window = new MyWindow ( ) ;
//Show modeless window
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 your own handler
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 )
{
//write sync logic here
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] 与我联系。