Verwenden Sie das aufgabenbasierte asynchrone Muster (TAP), um Revit-API-Code aus jedem Ausführungskontext auszuführen.
中文说明
Wenn Sie jemals auf eine Revit-API-Ausnahme gestoßen sind, die besagt: „Revit-API kann nicht außerhalb des Revit-API-Kontexts ausgeführt werden“, typischerweise wenn Sie Revit-API-Code aus einem modalen Fenster ausführen möchten, benötigen Sie diese Bibliothek möglicherweise, um Ihr Leben zu retten.
Eine gängige Lösung für diese Ausnahme besteht darin, den Revit-API-Code mit IExternalEventHandler
zu umschließen und die Handler-Instanz vorab bei Revit zu registrieren, um einen Auslöser ( ExternalEvent
) zu erhalten. Um den Handler auszuführen, drücken Sie einfach an einer beliebigen Stelle den Auslöser, um den Handler in die Revit-Befehlsschleife einzureihen. Aber es gibt noch ein anderes Problem. Nachdem Sie den Trigger im selben Kontext ausgelöst haben, wissen Sie nicht, wann der Handler ausgeführt wird, und es ist nicht einfach, von diesem Handler ein Ergebnis zu generieren. Wenn Sie dies erreichen möchten, müssen Sie die Kontrolle manuell an den aufrufenden Kontext zurückgeben.
Diese Lösung sieht dem Mechanismus von „Promise“ ziemlich ähnlich, wenn Sie mit JavaScript ES6 vertraut sind. Tatsächlich können wir die gesamte oben genannte Logik erreichen, indem wir das aufgabenbasierte asynchrone Muster (TAP) verwenden, das in .NET allgemein als Task<T>
bekannt ist. Durch die Übernahme von Revit.Async ist es möglich, Revit-API-Code aus jedem Kontext auszuführen, da Revit.Async Ihren Code intern automatisch mit IExternalEventHandler
umschließt und den Rückgabewert an den aufrufenden Kontext zurückgibt, um Ihren Aufruf natürlicher zu gestalten.
Wenn Sie mit dem aufgabenbasierten asynchronen Muster (TAP) nicht vertraut sind, finden Sie hier nützliches Material von Microsoft:
Hier ist ein Diagramm, das den externen Ereignismechanismus der Revit-API mit Revit.Async vergleicht, sowie Screenshots der beiden Hauptteile:
Ich wurde häufig gefragt, ob Revit.Async die Revit-API in einem Hintergrundthread ausführt.
Lassen Sie es uns klären. Die Antwort ist NEIN!!!!! Lassen Sie sich nicht durch das Wort „Async“ in die Irre führen.
Das Wort „Async“ ist hier eigentlich unschuldig. Es ist .NET, das eine Reihe von Multithread-Methoden mit der Endung „Async“ benennt, was zu dem allgemeinen Missverständnis führt.
Diese Frage kann ausgehend von den Unterschieden zwischen asynchroner Programmierung und Multithread-Programmierung erklärt werden.
Ein Wort von Stackoverflow:
„Beim Threading geht es um Arbeiter; bei der Asynchronität geht es um Aufgaben.“
Eine Analogie aus derselben Stackoverflow-Antwort:
Sie kochen in einem Restaurant. Eine Bestellung für Eier und Toast geht ein.
Synchron: Sie kochen die Eier, dann backen Sie den Toast.
Asynchron, Single-Threaded: Sie starten das Kochen der Eier und stellen einen Timer ein. Sie beginnen mit dem Toastbacken und stellen einen Timer ein. Während beide kochen, putzen Sie die Küche. Wenn die Timer ablaufen, nehmen Sie die Eier vom Herd, nehmen das Toast aus dem Toaster und servieren es.
Asynchron, Multithreading: Sie stellen zwei weitere Köche ein, einen zum Eierkochen und einen zum Toasten. Nun steht man vor dem Problem, die Köche so zu koordinieren, dass es in der Küche nicht zu Konflikten untereinander bei der Ressourcenteilung kommt. Und du musst sie bezahlen.
Der Grund für das Missverständnis „asynchron == multithread“ liegt darin, dass asynchron eine große Chance hat, mit Multithread einherzugehen. Wenn wir in den meisten UI-Anwendungen (STA) Multithread zum Ausführen einer Hintergrundaufgabe verwenden, muss das Ergebnis dieser Aufgabe zum Präsentieren an den UI-Thread „zurückgehen“. Asynchronous übernimmt seinen Teil in der „Zurück“-Phase.
Wenn Sie in einer Windows Form-Anwendung die Benutzeroberfläche von einem Arbeitsthread aus aktualisieren möchten, müssen Sie Invoke
Methode verwenden, um einen Delegate
in die Warteschlange für den Hauptthread zu stellen, um die UI-Aktualisierungen durchzuführen.
Wenn Sie in einer WPF-Anwendung die Benutzeroberfläche von einem Arbeitsthread aus aktualisieren möchten, müssen Sie das Dispatcher
Objekt verwenden, um einen Delegate
in die Warteschlange für den Hauptthread zu stellen, um die UI-Aktualisierungen durchzuführen.
In der Revit-Welt ist es fast dasselbe. Zur Aktualisierung der Modelle wird die Revit-API verwendet. Revit führt Modellaktualisierungen im Haupt-Thread durch und erfordert aus Gründen der Thread-Sicherheit meiner Meinung nach, dass alle APIs auch im Haupt-Thread aufgerufen werden.
Wenn Sie die Modelle von einem Arbeitsthread aus aktualisieren möchten, müssen Sie das ExternalEvent
Objekt verwenden, um eine IExternalEventHandler
-Instanz in die Warteschlange ( Raise()
) für den Hauptthread zu stellen, um die Revit-API aufzurufen. Dies ist das asynchrone Muster, das Revit zum Planen neuer API-Aufrufe bereitstellt.
Bei Revit.Async handelt es sich lediglich um einen Wrapper um das oben genannte asynchrone Muster. Das Ziel dieser Bibliothek besteht darin, ein sofort einsatzbereites Erlebnis für die asynchrone Revit-API bereitzustellen.
Es gibt definitiv KEINE Multithread-Funktion in Revit.Async.
Initialisieren Sie RevitTask in einem gültigen Revit-API-Kontext, bevor Sie eine Funktionalität von RevitTask verwenden.
RevitTask . Initialize ( app ) ;
Einige der gültigen Revit-API-Kontexte sind:
Die Hauptfunktionalität von Revit.Async wird durch die Methode RevitTask.RunAsync()
verfügbar gemacht. Es gibt mehrere Überladungen für RevitTask.RunAsync()
-Methode.
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 } " ) ;
}
}
Haben Sie genug von der schwachen IExternalEventHandler
Schnittstelle? Verwenden Sie stattdessen die Schnittstelle IGenericExternalEventHandler<TParameter,TResult>
. Es bietet Ihnen die Möglichkeit, Argumente an einen Handler zu übergeben und bei Abschluss ein Ergebnis zu erhalten.
Es wird immer empfohlen, von den vordefinierten abstrakten Klassen abzuleiten; Sie sind für die Verarbeitung der Argumentübergabe und der Ergebnisrückgabe konzipiert.
Klasse | Beschreibung |
---|---|
AsyncGenericExternalEventHandler<TParameter, TResult> | Zum Ausführen asynchroner Logik verwenden |
SyncGenericExternalEventHandler<TParameter, TResult> | Zum Ausführen der Synchronisierungslogik verwenden |
[ 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 ;
}
}
Wenn Sie Probleme bei der Verwendung dieser Bibliothek haben, können Sie mich jederzeit unter [email protected] kontaktieren.