Use o padrão assíncrono baseado em tarefa (TAP) para executar o código da API do Revit a partir de qualquer contexto de execução.
中文说明
Se você já encontrou uma exceção da API do Revit dizendo: "Não é possível executar a API do Revit fora do contexto da API do Revit", normalmente quando você deseja executar o código da API do Revit a partir de uma janela sem janela restrita, você pode precisar desta biblioteca para salvar sua vida.
Uma solução comum para essa exceção é agrupar o código da API do Revit usando IExternalEventHandler
e registrar a instância do manipulador no Revit antecipadamente para obter um acionador ( ExternalEvent
). Para executar o manipulador, basta acionar o gatilho de qualquer lugar para enfileirar o manipulador no loop de comando do Revit. Mas surge outro problema. Depois de acionar o gatilho, dentro do mesmo contexto, você não tem ideia de quando o manipulador será executado e não é fácil obter algum resultado gerado a partir desse manipulador. Se você quiser que isso aconteça, será necessário devolver manualmente o controle ao contexto de chamada.
Esta solução é bastante semelhante ao mecanismo de "Promise" se você estiver familiarizado com JavaScript ES6. Na verdade, podemos alcançar toda a lógica acima fazendo uso do padrão assíncrono baseado em tarefas (TAP), geralmente conhecido como Task<T>
em .NET. Ao adotar o Revit.Async, é possível executar o código da API do Revit a partir de qualquer contexto, porque internamente o Revit.Async agrupa seu código automaticamente com IExternalEventHandler
e gera o valor de retorno para o contexto de chamada para tornar sua invocação mais natural.
Se você não está familiarizado com o padrão assíncrono baseado em tarefas (TAP), aqui está algum material útil fornecido pela Microsoft:
Aqui está um diagrama comparando o mecanismo de eventos externos da API do Revit com o Revit.Async e capturas de tela das duas partes principais:
Fui frequentemente questionado sobre se o Revit.Async executa a API do Revit em um thread em segundo plano.
Vamos esclarecer isso. A resposta é NÃO!!!!! Não se deixe enganar pela palavra "Async".
A palavra "Async" é realmente inocente aqui. É o .NET quem nomeia vários métodos multithread com finalização "Async" que resulta no mal-entendido geral.
Esta questão pode ser explicada a partir das diferenças entre Programação Assíncrona e Programação Multithread.
Uma palavra do stackoverflow:
“Threading tem a ver com trabalhadores; assincronia tem a ver com tarefas”.
Uma analogia da mesma resposta stackoverflow:
Você está cozinhando em um restaurante. Chega um pedido de ovos e torradas.
Síncrono: você cozinha os ovos e depois cozinha a torrada.
Assíncrono, de thread único: você inicia o cozimento dos ovos e define um cronômetro. Você começa a cozinhar a torrada e ajusta um cronômetro. Enquanto os dois cozinham, você limpa a cozinha. Quando o cronômetro dispara você tira os ovos do fogo e as torradas da torradeira e serve.
Assíncrono, multithread: você contrata mais dois cozinheiros, um para cozinhar ovos e outro para fazer torradas. Agora você tem o problema de coordenar os cozinheiros para que não entrem em conflito na cozinha na hora de compartilhar recursos. E você tem que pagá-los.
A razão pela qual as pessoas têm o mal-entendido "assíncrono == multithread" é que o assíncrono tem uma grande chance de vir com o multithread. Na maioria dos aplicativos de UI (STA), quando usamos multithread para executar uma tarefa em segundo plano, o resultado dessa tarefa precisa "voltar" ao thread de UI a ser apresentado. O assíncrono participa da fase de "voltar".
Em um aplicativo de formulário do Windows, se você deseja atualizar a UI a partir de um thread de trabalho, você precisa usar o método Invoke
para enfileirar um Delegate
no thread principal para executar as atualizações da UI.
Em um aplicativo WPF, se você deseja atualizar a UI a partir de um thread de trabalho, você precisa usar o objeto Dispatcher
para enfileirar um Delegate
no thread principal para executar as atualizações da UI.
No mundo Revit, é quase a mesma coisa. A API do Revit é usada para atualizar os modelos. O Revit executa atualizações de modelo no thread principal e exige que todas as APIs sejam chamadas no thread principal também, para segurança do thread, eu acho.
Se desejar atualizar os modelos de um thread de trabalho, você precisará usar o objeto ExternalEvent
para enfileirar ( Raise()
) uma instância IExternalEventHandler
no thread principal para chamar a API do Revit. Este é o padrão assíncrono que o Revit fornece para agendar novas chamadas de API.
Quanto ao Revit.Async, é apenas um wrapper em torno do padrão assíncrono acima. O objetivo desta biblioteca é fornecer uma experiência pronta para uso para a API assíncrona do Revit.
Definitivamente NÃO há coisa multithread no Revit.Async.
Em qualquer contexto válido da API do Revit, inicialize o RevitTask antes de usar qualquer funcionalidade do RevitTask.
RevitTask . Initialize ( app ) ;
Alguns dos contextos válidos da API do Revit são:
A principal funcionalidade do Revit.Async é exposta pelo método RevitTask.RunAsync()
. Existem diversas sobrecargas para o 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 } " ) ;
}
}
Farto da interface IExternalEventHandler
fraca? Use a interface IGenericExternalEventHandler<TParameter,TResult>
. Ele fornece a capacidade de passar argumentos para um manipulador e receber o resultado após a conclusão.
É sempre recomendado derivar das classes abstratas predefinidas; eles são projetados para lidar com a passagem de argumentos e a parte de retorno de resultados.
Aula | Descrição |
---|---|
AsyncGenericExternalEventHandler<TParameter, TResult> | Use para executar lógica assíncrona |
SyncGenericExternalEventHandler<TParameter, TResult> | Use para executar a lógica de sincronização |
[ 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 ;
}
}
Sinta-se à vontade para entrar em contato comigo pelo telefone [email protected] se tiver algum problema ao usar esta biblioteca.