Utilice el patrón asincrónico basado en tareas (TAP) para ejecutar el código API de Revit desde cualquier contexto de ejecución.
中文说明
Si alguna vez se ha encontrado con una excepción de la API de Revit que dice: "No se puede ejecutar la API de Revit fuera del contexto de la API de Revit", normalmente cuando desea ejecutar el código de la API de Revit desde una ventana no modal, es posible que necesite esta biblioteca para salvarle la vida.
Una solución común para esta excepción es empaquetar el código API de Revit usando IExternalEventHandler
y registrar la instancia del controlador en Revit con anticipación para obtener un activador ( ExternalEvent
). Para ejecutar el controlador, simplemente levante el disparador desde cualquier lugar para poner el controlador en cola en el bucle de comandos de Revit. Pero surge otro problema. Después de activar el disparador, dentro del mismo contexto, no tiene idea de cuándo se ejecutará el controlador y no es fácil generar algún resultado a partir de ese controlador. Si desea que esto suceda, debe devolver manualmente el control al contexto de llamada.
Esta solución se parece bastante al mecanismo de "Promise" si está familiarizado con JavaScript ES6. En realidad, podemos lograr toda la lógica anterior haciendo uso del patrón asincrónico basado en tareas (TAP), que generalmente se conoce como Task<T>
en .NET. Al adoptar Revit.Async, es posible ejecutar código API de Revit desde cualquier contexto, porque internamente Revit.Async envuelve su código automáticamente con IExternalEventHandler
y genera el valor de retorno al contexto de llamada para que su invocación sea más natural.
Si no está familiarizado con el patrón asincrónico basado en tareas (TAP), aquí tiene material útil proporcionado por Microsoft:
A continuación se muestra un diagrama que compara el mecanismo de eventos externos de la API de Revit con Revit.Async y capturas de pantalla de las dos partes principales:
Con frecuencia me preguntaban si Revit.Async ejecuta la API de Revit en un subproceso en segundo plano.
Aclarémoslo. La respuesta es NO!!!!! No se deje engañar por la palabra "Async".
La palabra "Async" es realmente inocente aquí. Es .NET quien nombra un montón de métodos multiproceso con terminación "Async" lo que resulta en un malentendido general.
Esta pregunta se puede explicar a partir de las diferencias entre programación asincrónica y programación multiproceso.
Unas palabras de stackoverflow:
"El threading se trata de trabajadores; la asincronía se trata de tareas".
Una analogía de la misma respuesta de stackoverflow:
Estás cocinando en un restaurante. Llega un pedido de huevos y tostadas.
Sincrónico: cocinas los huevos, luego cocinas las tostadas.
Asincrónico, de un solo subproceso: comienzas a cocinar los huevos y configuras un temporizador. Comienzas a cocinar la tostada y configuras un temporizador. Mientras ambos cocinan, tú limpias la cocina. Cuando suene el cronómetro, retiras los huevos del fuego y las tostadas de la tostadora y los sirves.
Asincrónico, multiproceso: contratas a dos cocineros más, uno para cocinar huevos y otro para cocinar tostadas. Ahora tienes el problema de coordinar a los cocineros para que no entren en conflicto en la cocina a la hora de compartir recursos. Y hay que pagarles.
La razón por la que la gente tiene el malentendido "asincrónico == multiproceso" es que lo asincrónico tiene una gran posibilidad de venir con múltiples subprocesos. En la mayoría de las aplicaciones de UI (STA), cuando usamos subprocesos múltiples para ejecutar una tarea en segundo plano, el resultado de esa tarea debe "volver" al subproceso de UI que se presentará. Asíncrono participa en la fase de "regreso".
En una aplicación de Windows Form, si desea actualizar la interfaz de usuario desde un subproceso de trabajo, debe utilizar el método Invoke
para poner en cola un Delegate
al subproceso principal para realizar las actualizaciones de la interfaz de usuario.
En una aplicación WPF, si desea actualizar la interfaz de usuario desde un subproceso de trabajo, debe utilizar el objeto Dispatcher
para poner en cola un Delegate
al subproceso principal para realizar las actualizaciones de la interfaz de usuario.
En el mundo de Revit, es casi lo mismo. La API de Revit se utiliza para actualizar los modelos. Revit realiza actualizaciones del modelo en el hilo principal y requiere que todas las API también se llamen en el hilo principal, creo que por seguridad del hilo.
Si desea actualizar los modelos desde un subproceso de trabajo, debe utilizar el objeto ExternalEvent
para poner en cola ( Raise()
) una instancia IExternalEventHandler
en el subproceso principal para llamar a la API de Revit. Este es el patrón asincrónico que proporciona Revit para programar nuevas llamadas API.
En cuanto a Revit.Async, es solo una envoltura del patrón asincrónico anterior. El objetivo de esta biblioteca es proporcionar una experiencia lista para usar para la API de Revit asíncrona.
Definitivamente NO hay ningún elemento multiproceso en Revit.Async.
En cualquier contexto de API de Revit válido, inicialice RevitTask antes de utilizar cualquier funcionalidad de RevitTask.
RevitTask . Initialize ( app ) ;
Algunos de los contextos válidos de la API de Revit son:
La funcionalidad principal de Revit.Async se expone mediante el método RevitTask.RunAsync()
. Existen varias sobrecargas para el método 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 } " ) ;
}
}
¿Estás harto de la débil interfaz IExternalEventHandler
? Utilice la interfaz IGenericExternalEventHandler<TParameter,TResult>
en su lugar. Le proporciona la posibilidad de pasar argumentos a un controlador y recibir el resultado una vez completado.
Siempre se recomienda derivar de clases abstractas predefinidas; están diseñados para manejar la parte de paso de argumentos y devolución de resultados.
Clase | Descripción |
---|---|
AsyncGenericExternalEventHandler<TParameter, TResult> | Úselo para ejecutar lógica asincrónica |
SyncGenericExternalEventHandler<TParameter, TResult> | Úselo para ejecutar la lógica de sincronización |
[ 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 ;
}
}
No dude en ponerse en contacto conmigo a través de [email protected] si tiene algún problema al utilizar esta biblioteca.