Gunakan pola asinkron berbasis tugas (TAP) untuk menjalankan kode Revit API dari konteks eksekusi apa pun.
中文说明
Jika Anda pernah menemukan pengecualian Revit API yang mengatakan, "Tidak dapat mengeksekusi Revit API di luar konteks Revit API", biasanya ketika Anda ingin mengeksekusi kode Revit API dari jendela modeless, Anda mungkin memerlukan perpustakaan ini untuk menyelamatkan hidup Anda.
Solusi umum untuk pengecualian ini adalah dengan membungkus kode API Revit menggunakan IExternalEventHandler
dan mendaftarkan instance handler ke Revit terlebih dahulu untuk mendapatkan pemicu ( ExternalEvent
). Untuk mengeksekusi handler, cukup naikkan trigger dari mana saja untuk mengantri handler ke loop perintah Revit. Tapi ada masalah lain. Setelah menaikkan trigger, dalam konteks yang sama, Anda tidak tahu kapan handler akan dieksekusi dan tidak mudah untuk mendapatkan hasil yang dihasilkan dari handler tersebut. Jika Anda ingin mewujudkannya, Anda harus mengembalikan kontrol ke konteks panggilan secara manual.
Solusi ini terlihat sangat mirip dengan mekanisme "Promise" jika Anda familiar dengan JavaScript ES6. Sebenarnya, kita dapat mencapai semua logika di atas dengan memanfaatkan pola asinkron berbasis tugas (TAP) yang umumnya dikenal sebagai Task<T>
di .NET. Dengan mengadopsi Revit.Async, kode API Revit dapat dijalankan dari konteks apa pun, karena secara internal Revit.Async membungkus kode Anda secara otomatis dengan IExternalEventHandler
dan menghasilkan nilai kembalian ke konteks panggilan untuk membuat pemanggilan Anda lebih alami.
Jika Anda tidak terbiasa dengan pola asinkron berbasis tugas (TAP), berikut adalah beberapa materi berguna yang disediakan oleh Microsoft:
Berikut adalah diagram yang membandingkan mekanisme kejadian eksternal Revit API dengan Revit.Async dan tangkapan layar dari dua bagian utama:
Saya sering ditanya apakah Revit.Async menjalankan Revit API di thread latar belakang.
Mari kita perjelas. Jawabannya adalah TIDAK!!!!! Jangan salah paham dengan kata "Async".
Kata "Async" sebenarnya tidak bersalah di sini. .NET-lah yang menamai sekumpulan metode multithread dengan akhiran "Async" yang mengakibatkan kesalahpahaman umum.
Pertanyaan ini dapat dijelaskan mulai dari perbedaan Asynchronous Programming dan Multithread Programming.
Sepatah kata dari stackoverflow:
"Threading adalah tentang pekerja; asinkron adalah tentang tugas".
Analogi dari jawaban stackoverflow yang sama:
Anda sedang memasak di restoran. Pesanan datang untuk telur dan roti panggang.
Sinkron: Anda memasak telur, lalu memasak roti panggang.
Asynchronous, single threaded: Anda mulai memasak telur dan menyetel pengatur waktu. Anda mulai memasak roti panggang, dan menyetel pengatur waktu. Saat mereka berdua memasak, Anda membersihkan dapur. Saat pengatur waktu berbunyi, angkat telur dari api dan roti panggang dari pemanggang roti, lalu sajikan.
Asinkron, multithread: Anda mempekerjakan dua juru masak lagi, satu untuk memasak telur dan satu lagi untuk memasak roti panggang. Sekarang Anda mempunyai masalah dalam mengkoordinasikan para juru masak agar mereka tidak saling berkonflik di dapur saat berbagi sumber daya. Dan Anda harus membayarnya.
Alasan mengapa orang memiliki kesalahpahaman "asynchronous == multithread" adalah karena asynchronous memiliki peluang besar untuk hadir dengan multithread. Di sebagian besar aplikasi UI (STA), saat kita menggunakan multithread untuk menjalankan tugas latar belakang, hasil tugas tersebut perlu "kembali" ke thread UI untuk ditampilkan. Asynchronous mengambil bagiannya dalam fase "kembali".
Dalam aplikasi formulir windows, jika Anda ingin memperbarui UI dari thread pekerja, Anda perlu menggunakan metode Invoke
untuk mengantri Delegate
ke thread utama untuk melakukan pembaruan UI.
Dalam aplikasi WPF, jika Anda ingin memperbarui UI dari thread pekerja, Anda perlu menggunakan objek Dispatcher
untuk memasukkan Delegate
ke thread utama untuk melakukan pembaruan UI.
Di dunia Revit, hampir sama. Revit API digunakan untuk memperbarui model. Revit melakukan pembaruan model di thread utama dan mengharuskan semua API dipanggil di thread utama juga, menurut saya demi keamanan thread.
Jika Anda ingin memperbarui model dari thread pekerja, Anda perlu menggunakan objek ExternalEvent
untuk mengantri( Raise()
) instance IExternalEventHandler
ke thread utama untuk memanggil Revit API. Ini adalah pola asinkron yang disediakan Revit untuk menjadwalkan panggilan API baru.
Mengenai Revit.Async, ini hanyalah pembungkus pola asinkron di atas. Tujuan perpustakaan ini adalah untuk memberikan pengalaman unik untuk Revit API asinkron.
Pastinya TIDAK ada hal multithread di Revit.Async.
Dalam konteks Revit API apa pun yang valid, inisialisasi RevitTask sebelum Anda menggunakan fungsionalitas RevitTask apa pun.
RevitTask . Initialize ( app ) ;
Beberapa konteks Revit API yang valid adalah:
Fungsi utama Revit.Async diekspos oleh metode RevitTask.RunAsync()
. Ada beberapa kelebihan beban untuk metode 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 } " ) ;
}
}
Muak dengan antarmuka IExternalEventHandler
yang lemah? Gunakan antarmuka IGenericExternalEventHandler<TParameter,TResult>
sebagai gantinya. Ini memberi Anda kemampuan untuk menyampaikan argumen kepada penangan dan menerima hasil secara lengkap.
Selalu disarankan untuk mengambil dari kelas abstrak yang telah ditentukan sebelumnya; mereka dirancang untuk menangani bagian penyampaian argumen dan pengembalian hasil.
Kelas | Keterangan |
---|---|
AsyncGenericExternalEventHandler<TParameter, TResult> | Gunakan untuk menjalankan logika asinkron |
SyncGenericExternalEventHandler<TParameter, TResult> | Gunakan untuk menjalankan logika sinkronisasi |
[ 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 ;
}
}
Jangan ragu untuk menghubungi saya melalui [email protected] jika Anda memiliki masalah dalam menggunakan perpustakaan ini.