Используйте асинхронный шаблон на основе задач (TAP) для запуска кода API Revit из любого контекста выполнения.
中文说明
Если вы когда-либо сталкивались с исключением Revit API, говорящим: «Невозможно выполнить Revit API вне контекста Revit API», обычно, когда вы хотите выполнить код Revit API из немодального окна, вам может понадобиться эта библиотека, чтобы спасти свою жизнь.
Обычное решение для этого исключения — обернуть код Revit API с помощью IExternalEventHandler
и заранее зарегистрировать экземпляр обработчика в Revit, чтобы получить триггер ( ExternalEvent
). Чтобы выполнить обработчик, просто поднимите триггер из любого места, чтобы поставить обработчик в очередь в командный цикл Revit. Но возникает еще одна проблема. После срабатывания триггера в том же контексте вы понятия не имеете, когда будет выполнен обработчик, и получить результат, сгенерированный этим обработчиком, непросто. Если вы действительно хотите, чтобы это произошло, вам придется вручную вернуть элемент управления вызывающему контексту.
Это решение очень похоже на механизм «Обещания», если вы знакомы с JavaScript ES6. На самом деле, мы можем реализовать всю вышеописанную логику, используя асинхронный шаблон на основе задач (TAP), который в .NET обычно известен как Task<T>
. Приняв Revit.Async, можно запускать код Revit API из любого контекста, поскольку внутри Revit.Async автоматически оборачивает ваш код с помощью IExternalEventHandler
и возвращает возвращаемое значение в вызывающий контекст, чтобы сделать ваш вызов более естественным.
Если вы не знакомы с асинхронным шаблоном на основе задач (TAP), вот несколько полезных материалов по нему, предоставленных Microsoft:
Ниже представлена диаграмма, сравнивающая механизм внешних событий Revit API с Revit.Async, а также снимки экрана двух основных частей:
Меня часто спрашивали о том, запускает ли Revit.Async Revit API в фоновом потоке.
Давайте проясним это. Ответ НЕТ!!!!! Не вводите себя в заблуждение словом «Асинхронный».
Слово «Async» здесь на самом деле невинно. Именно .NET называет кучу многопоточных методов с окончанием «Async», что приводит к общему недопониманию.
Этот вопрос можно объяснить, исходя из различий между асинхронным программированием и многопоточным программированием.
Слово из stackoverflow:
«Поточность — это рабочие процессы, асинхронность — это задачи».
Аналогия из того же ответа stackoverflow:
Вы готовите в ресторане. Поступает заказ на яйца и тосты.
Синхронно: вы готовите яйца, затем готовите тост.
Асинхронный, однопоточный: вы начинаете готовить яйца и устанавливаете таймер. Вы начинаете готовить тосты и устанавливаете таймер. Пока они оба готовят, вы убираетесь на кухне. Когда таймеры сработают, вы снимаете яйца с огня, тосты из тостера и подаете их.
Асинхронный, многопоточный: вы нанимаете еще двух поваров: одного для приготовления яиц, а другого для приготовления тостов. Теперь у вас есть проблема координации работы поваров, чтобы они не конфликтовали друг с другом на кухне при разделении ресурсов. И вы должны им заплатить.
Причина, по которой люди недопонимают «асинхронность == многопоточность», заключается в том, что асинхронность имеет большие шансы прийти вместе с многопоточностью. В большинстве приложений пользовательского интерфейса (STA), когда мы используем многопоточность для запуска фоновой задачи, результат этой задачи должен «вернуться» в поток пользовательского интерфейса для представления. Асинхронный режим принимает участие в фазе «возврата».
В приложении формы Windows, если вы хотите обновить пользовательский интерфейс из рабочего потока, вам необходимо использовать метод Invoke
, чтобы поставить Delegate
в очередь в основной поток для выполнения обновлений пользовательского интерфейса.
Если в приложении WPF вы хотите обновить пользовательский интерфейс из рабочего потока, вам необходимо использовать объект Dispatcher
для постановки Delegate
в очередь в основной поток для выполнения обновлений пользовательского интерфейса.
В мире Revit все почти то же самое. Revit API используется для обновления моделей. Revit выполняет обновления модели в основном потоке и требует, чтобы все API вызывались и в основном потоке, я думаю, в целях безопасности потоков.
Если вы хотите обновить модели из рабочего потока, вам необходимо использовать объект ExternalEvent
для постановки в очередь ( Raise()
) экземпляра IExternalEventHandler
в основной поток для вызова Revit API. Это асинхронный шаблон, который Revit предоставляет для планирования новых вызовов API.
Что касается Revit.Async, то это всего лишь оболочка приведенного выше асинхронного шаблона. Цель этой библиотеки — предоставить готовые возможности для асинхронного API Revit.
В 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], если у вас возникнут проблемы с использованием этой библиотеки.