使用基於任務的非同步模式 (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] 與我聯繫。