작업 기반 비동기 패턴(TAP)을 사용하여 모든 실행 컨텍스트에서 Revit API 코드를 실행합니다.
중국어 설명
일반적으로 모덜리스 창에서 Revit API 코드를 실행하려고 할 때 "Revit API 컨텍스트 외부에서 Revit API를 실행할 수 없습니다"라는 Revit API 예외가 발생한 경우 생명을 구하기 위해 이 라이브러리가 필요할 수 있습니다.
이 예외에 대한 일반적인 해결 방법은 IExternalEventHandler
사용하여 Revit API 코드를 래핑하고 핸들러 인스턴스를 Revit에 미리 등록하여 트리거( ExternalEvent
)를 가져오는 것입니다. 핸들러를 실행하려면 어디에서나 트리거를 올려 핸들러를 Revit 명령 루프에 대기시키면 됩니다. 하지만 또 다른 문제가 발생합니다. 동일한 컨텍스트 내에서 트리거를 발생시킨 후에는 핸들러가 언제 실행될지 알 수 없으며 해당 핸들러에서 생성된 일부 결과를 얻는 것도 쉽지 않습니다. 이를 실현하려면 제어권을 호출 컨텍스트에 수동으로 양보해야 합니다.
JavaScript ES6에 익숙하다면 이 솔루션은 "Promise" 메커니즘과 매우 유사해 보입니다. 실제로 .NET에서 일반적으로 Task<T>
로 알려진 작업 기반 비동기 패턴(TAP)을 사용하여 위의 모든 논리를 달성할 수 있습니다. Revit.Async를 채택하면 내부적으로 Revit.Async가 IExternalEventHandler
를 사용하여 자동으로 코드를 래핑하고 호출 컨텍스트에 반환 값을 생성하여 호출을 보다 자연스럽게 만들기 때문에 모든 컨텍스트에서 Revit API 코드를 실행할 수 있습니다.
TAP(작업 기반 비동기 패턴)에 익숙하지 않은 경우 Microsoft에서 제공하는 몇 가지 유용한 자료를 참조하세요.
다음은 Revit API 외부 이벤트 메커니즘을 Revit.Async와 비교하는 다이어그램과 두 주요 부분의 스크린샷입니다.
Revit.Async가 백그라운드 스레드에서 Revit API를 실행하는지에 대한 질문을 자주 받았습니다.
그것을 명확히합시다. 대답은 NO입니다!!!!! "비동기"라는 단어에 속지 마십시오.
여기서 "Async"라는 단어는 실제로 결백합니다. 일반적인 오해를 불러일으키는 "Async"로 끝나는 여러 멀티스레드 메서드 이름을 지정하는 것은 .NET입니다.
이 질문은 비동기 프로그래밍과 멀티스레드 프로그래밍의 차이점부터 설명할 수 있습니다.
stackoverflow의 한마디:
"스레딩은 작업자에 관한 것이고 비동기성은 작업에 관한 것입니다."
동일한 stackoverflow 답변의 비유:
당신은 식당에서 요리를 하고 있습니다. 계란과 토스트 주문이 들어옵니다.
동기식: 계란을 요리한 다음 토스트를 요리합니다.
비동기식 단일 스레드: 계란 요리를 시작하고 타이머를 설정합니다. 토스트 요리를 시작하고 타이머를 설정합니다. 둘 다 요리하는 동안 당신은 부엌을 청소합니다. 타이머가 울리면 계란을 불에서 꺼내고 토스터에서 토스트를 꺼내 서빙하세요.
비동기식, 멀티스레드: 두 명의 요리사를 더 고용합니다. 한 명은 계란을 요리하고 다른 한 명은 토스트를 요리합니다. 이제 요리사들이 자원을 공유할 때 주방에서 서로 충돌하지 않도록 조정하는 문제가 생겼습니다. 그리고 당신은 그들에게 돈을 지불해야합니다.
사람들이 "비동기 == 멀티스레드"라고 오해하는 이유는 비동기식이 멀티스레드와 함께 나타날 가능성이 크기 때문입니다. 대부분의 UI 애플리케이션(STA)에서 멀티스레드를 사용하여 백그라운드 작업을 실행할 때 해당 작업의 결과는 UI 스레드로 "돌아가서" 표시되어야 합니다. 비동기식은 "돌아가기" 단계에 참여합니다.
Windows 양식 애플리케이션에서 작업자 스레드에서 UI를 업데이트하려면 Invoke
메서드를 사용하여 Delegate
기본 스레드에 대기열에 추가하여 UI 업데이트를 수행해야 합니다.
WPF 애플리케이션에서 작업자 스레드에서 UI를 업데이트하려면 Dispatcher
개체를 사용하여 Delegate
기본 스레드에 대기열에 넣어 UI 업데이트를 수행해야 합니다.
Revit 세계에서는 거의 동일합니다. Revit API는 모델을 업데이트하는 데 사용됩니다. Revit은 메인 스레드에서 모델 업데이트를 수행하며 스레드 안전을 위해 모든 API를 메인 스레드에서도 호출해야 한다고 생각합니다.
작업자 스레드에서 모델을 업데이트하려면 Revit API를 호출하기 위해 기본 스레드에 대한 IExternalEventHandler
인스턴스를 큐( Raise()
)에 대한 ExternalEvent
객체를 사용해야 합니다. 이는 새 API 호출을 예약하기 위해 Revit이 제공하는 비동기 패턴입니다.
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]으로 연락해 주세요.