Utilisez le modèle asynchrone basé sur les tâches (TAP) pour exécuter le code de l'API Revit à partir de n'importe quel contexte d'exécution.
中文说明
Si vous avez déjà rencontré une exception de l'API Revit indiquant : "Impossible d'exécuter l'API Revit en dehors du contexte de l'API Revit", généralement lorsque vous souhaitez exécuter du code de l'API Revit à partir d'une fenêtre non modale, vous aurez peut-être besoin de cette bibliothèque pour vous sauver la vie.
Une solution courante pour cette exception consiste à encapsuler le code de l'API Revit à l'aide IExternalEventHandler
et à enregistrer l'instance de gestionnaire dans Revit à l'avance pour obtenir un déclencheur ( ExternalEvent
). Pour exécuter le gestionnaire, activez simplement le déclencheur n'importe où pour mettre le gestionnaire en file d'attente dans la boucle de commandes Revit. Mais il y a un autre problème. Après avoir déclenché le déclencheur, dans le même contexte, vous n'avez aucune idée du moment où le gestionnaire sera exécuté et il n'est pas facile d'obtenir un résultat généré par ce gestionnaire. Si vous souhaitez que cela se produise, vous devez céder manuellement le contrôle au contexte appelant.
Cette solution ressemble beaucoup au mécanisme de « Promesse » si vous êtes familier avec JavaScript ES6. En fait, nous pouvons réaliser toute la logique ci-dessus en utilisant un modèle asynchrone basé sur les tâches (TAP), généralement connu sous le nom de Task<T>
dans .NET. En adoptant Revit.Async, il est possible d'exécuter le code de l'API Revit à partir de n'importe quel contexte, car en interne, Revit.Async encapsule automatiquement votre code avec IExternalEventHandler
et renvoie la valeur de retour au contexte appelant pour rendre votre invocation plus naturelle.
Si vous n'êtes pas familier avec le modèle asynchrone basé sur les tâches (TAP), voici quelques informations utiles fournies par Microsoft :
Voici un diagramme comparant le mécanisme d'événements externes de l'API Revit avec Revit.Async et des captures d'écran des deux parties principales :
On m'a souvent demandé si Revit.Async exécutait l'API Revit dans un fil d'arrière-plan.
Clarifions-le. La réponse est NON !!!!! Ne vous laissez pas tromper par le mot « Async ».
Le mot « Async » est en fait innocent ici. C'est .NET qui nomme un tas de méthodes multithread avec la terminaison "Async", ce qui entraîne un malentendu général.
Cette question peut être expliquée à partir des différences entre la programmation asynchrone et la programmation multithread.
Un mot de stackoverflow :
"Le threading concerne les travailleurs ; l'asynchronie concerne les tâches".
Une analogie avec la même réponse stackoverflow :
Vous cuisinez dans un restaurant. Une commande arrive pour des œufs et des toasts.
Synchrone : vous faites cuire les œufs, puis vous faites cuire les toasts.
Asynchrone, mono-thread : vous démarrez la cuisson des œufs et réglez une minuterie. Vous démarrez la cuisson des toasts et réglez une minuterie. Pendant qu’ils cuisinent tous les deux, vous nettoyez la cuisine. Lorsque les minuteries sonnent, retirez les œufs du feu et les toasts du grille-pain et servez-les.
Asynchrone, multithread : vous embauchez deux cuisiniers supplémentaires, un pour cuire les œufs et un pour cuire les toasts. Vous avez maintenant le problème de coordonner les cuisiniers afin qu'ils n'entrent pas en conflit les uns avec les autres dans la cuisine lors du partage des ressources. Et il faut les payer.
La raison pour laquelle les gens ont le malentendu "asynchrone == multithread" est que l'asynchrone a de grandes chances de venir avec le multithread. Dans la plupart des applications d'interface utilisateur (STA), lorsque nous utilisons le multithread pour exécuter une tâche en arrière-plan, le résultat de cette tâche doit "revenir" au thread d'interface utilisateur pour être présenté. Asynchrone participe à la phase de « retour en arrière ».
Dans une application Windows Form, si vous souhaitez mettre à jour l'interface utilisateur à partir d'un thread de travail, vous devez utiliser la méthode Invoke
pour mettre en file d'attente un Delegate
vers le thread principal afin d'effectuer les mises à jour de l'interface utilisateur.
Dans une application WPF, si vous souhaitez mettre à jour l'interface utilisateur à partir d'un thread de travail, vous devez utiliser l'objet Dispatcher
pour mettre en file d'attente un Delegate
vers le thread principal afin d'effectuer les mises à jour de l'interface utilisateur.
Dans le monde Revit, c'est presque pareil. L'API Revit est utilisée pour mettre à jour les modèles. Revit effectue des mises à jour de modèle sur le thread principal et nécessite que toutes les API soient également appelées sur le thread principal, pour des raisons de sécurité des threads, je pense.
Si vous souhaitez mettre à jour les modèles à partir d'un thread de travail, vous devez utiliser l'objet ExternalEvent
pour mettre en file d'attente ( Raise()
) une instance IExternalEventHandler
vers le thread principal pour appeler l'API Revit. Il s'agit du modèle asynchrone fourni par Revit pour planifier de nouveaux appels d'API.
Quant à Revit.Async, il s'agit simplement d'un wrapper autour du modèle asynchrone ci-dessus. L'objectif de cette bibliothèque est de fournir une expérience prête à l'emploi pour l'API Revit asynchrone.
Il n'y a certainement AUCUN élément multithread dans Revit.Async.
Dans tout contexte d'API Revit valide, initialisez RevitTask avant d'utiliser une fonctionnalité de RevitTask.
RevitTask . Initialize ( app ) ;
Certains des contextes d'API Revit valides sont :
La fonctionnalité principale de Revit.Async est exposée par la méthode RevitTask.RunAsync()
. Il existe plusieurs surcharges pour la méthode 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 } " ) ;
}
}
Marre de la faible interface IExternalEventHandler
? Utilisez plutôt l’interface IGenericExternalEventHandler<TParameter,TResult>
. Il vous offre la possibilité de transmettre un argument à un gestionnaire et de recevoir le résultat une fois terminé.
Il est toujours recommandé de dériver des classes abstraites prédéfinies ; ils sont conçus pour gérer la partie de transmission des arguments et de renvoi des résultats.
Classe | Description |
---|---|
AsyncGenericExternalEventHandler<TParameter, TResult> | Utiliser pour exécuter une logique asynchrone |
SyncGenericExternalEventHandler<TParameter, TResult> | Utiliser pour exécuter la logique de synchronisation |
[ 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 ;
}
}
N'hésitez pas à me contacter par [email protected] si vous rencontrez un problème lors de l'utilisation de cette bibliothèque.