ใช้รูปแบบอะซิงโครนัสตามงาน (TAP) เพื่อรันโค้ด Revit API จากบริบทการดำเนินการใดๆ
中文说明
หากคุณเคยพบข้อยกเว้น Revit API ที่ระบุว่า "ไม่สามารถเรียกใช้ Revit API นอกบริบท Revit API ได้" โดยทั่วไปเมื่อคุณต้องการรันโค้ด Revit API จากหน้าต่างที่ไม่มีโหมด คุณอาจต้องใช้ไลบรารีนี้เพื่อช่วยชีวิตคุณ
วิธีแก้ปัญหาทั่วไปสำหรับข้อยกเว้นนี้คือการล้อมโค้ด Revit API โดยใช้ IExternalEventHandler
และลงทะเบียนอินสแตนซ์ตัวจัดการไปที่ Revit ล่วงหน้าเพื่อรับทริกเกอร์ ( ExternalEvent
) หากต้องการรันตัวจัดการ เพียงเพิ่มทริกเกอร์จากที่ใดก็ได้เพื่อจัดคิวตัวจัดการไปยังลูปคำสั่ง Revit แต่มีปัญหาอื่นมาอีก หลังจากเพิ่มทริกเกอร์ ในบริบทเดียวกัน คุณไม่รู้ว่าเมื่อใดที่ตัวจัดการจะถูกดำเนินการ และมันไม่ง่ายเลยที่จะได้ผลลัพธ์ที่สร้างจากตัวจัดการนั้น ถ้าคุณต้องการให้สิ่งนี้เกิดขึ้น คุณจะต้องให้การควบคุมกลับไปยังบริบทการโทรด้วยตนเอง
โซลูชันนี้ดูค่อนข้างคล้ายกับกลไกของ "Promise" หากคุณคุ้นเคยกับ JavaScript ES6 จริงๆ แล้ว เราสามารถบรรลุตรรกะข้างต้นทั้งหมดได้โดยการใช้รูปแบบอะซิงโครนัสตามงาน (TAP) ซึ่งโดยทั่วไปเรียกว่า Task<T>
ใน .NET ด้วยการนำ Revit.Async มาใช้ คุณจะสามารถเรียกใช้โค้ด Revit API จากบริบทใดก็ได้ เนื่องจาก Revit.Async ภายในจะล้อมโค้ดของคุณโดยอัตโนมัติด้วย IExternalEventHandler
และให้ค่าส่งคืนไปยังบริบทการเรียกเพื่อทำให้การเรียกใช้ของคุณเป็นธรรมชาติมากขึ้น
หากคุณไม่คุ้นเคยกับรูปแบบอะซิงโครนัสตามงาน (TAP) ต่อไปนี้เป็นเนื้อหาที่เป็นประโยชน์บางส่วนที่ Microsoft ให้ไว้:
นี่คือแผนภาพเปรียบเทียบกลไกเหตุการณ์ภายนอก Revit API กับ Revit.Async และภาพหน้าจอของสองส่วนหลัก:
ฉันถูกถามบ่อยๆ ว่า Revit.Async รัน Revit API ในเธรดพื้นหลังหรือไม่
มาชี้แจงกันดีกว่า คำตอบคือ ไม่!!!!! อย่าเข้าใจผิดกับคำว่า "Async"
คำว่า "Async" นั้นไร้เดียงสาจริงๆ .NET เป็นผู้ที่ตั้งชื่อวิธีมัลติเธรดจำนวนมากโดยลงท้ายด้วย "Async" ซึ่งส่งผลให้เกิดความเข้าใจผิดโดยทั่วไป
คำถามนี้สามารถอธิบายได้โดยเริ่มจากความแตกต่างระหว่างการเขียนโปรแกรมแบบอะซิงโครนัสและการเขียนโปรแกรมแบบมัลติเธรด
คำพูดจาก stackoverflow:
"การเธรดเป็นเรื่องของคนงาน ส่วนความไม่ตรงกันเป็นเรื่องของงาน"
ความคล้ายคลึงจากคำตอบ stackoverflow เดียวกัน:
คุณกำลังทำอาหารในร้านอาหาร ออเดอร์มาทั้งไข่และขนมปังปิ้ง
ซิงโครนัส: คุณปรุงไข่แล้วปรุงขนมปังปิ้ง
อะซิงโครนัส เธรดเดียว: คุณเริ่มปรุงไข่และตั้งเวลา คุณเริ่มทำขนมปังปิ้งและตั้งเวลา ขณะที่ทั้งคู่กำลังทำอาหาร คุณก็ทำความสะอาดห้องครัว เมื่อหมดเวลา คุณนำไข่ออกจากเตาและขนมปังปิ้งออกจากเครื่องปิ้งขนมปังแล้วเสิร์ฟ
แบบอะซิงโครนัสแบบมัลติเธรด: คุณจ้างพ่อครัวเพิ่มอีกสองคน คนหนึ่งปรุงไข่ และอีกหนึ่งคนทำขนมปังปิ้ง ตอนนี้คุณมีปัญหาในการประสานงานพ่อครัวเพื่อไม่ให้ขัดแย้งกันในครัวเมื่อแบ่งปันทรัพยากร และคุณต้องจ่ายเงินให้พวกเขา
เหตุผลที่ผู้คนเกิดความเข้าใจผิดเกี่ยวกับ "อะซิงโครนัส == มัลติเธรด" ก็คือ อะซิงโครนัสมีโอกาสมากที่จะมาพร้อมกับมัลติเธรด ในแอปพลิเคชัน UI ส่วนใหญ่ (STA) เมื่อเราใช้มัลติเธรดเพื่อรันงานเบื้องหลัง ผลลัพธ์ของงานนั้นจะต้อง "ย้อนกลับ" ไปที่เธรด UI จึงจะนำเสนอได้ อะซิงโครนัสมีส่วนร่วมในระยะ "ย้อนกลับ"
ในแอปพลิเคชันแบบฟอร์ม windows หากคุณต้องการอัปเดต UI จากเธรดของผู้ปฏิบัติงาน คุณต้องใช้วิธี Invoke
เพื่อจัดคิว Delegate
ไปยังเธรดหลักเพื่อดำเนินการอัปเดต UI
ในแอปพลิเคชัน WPF หากคุณต้องการอัปเดต UI จากเธรดของผู้ปฏิบัติงาน คุณต้องใช้ออบเจ็กต์ Dispatcher
เพื่อจัดคิว Delegate
ไปยังเธรดหลักเพื่อดำเนินการอัปเดต UI
ในโลกของ Revit มันแทบจะเหมือนกันเลย Revit API ใช้เพื่ออัปเดตโมเดล Revit ทำการอัปเดตโมเดลบนเธรดหลัก และจำเป็นต้องเรียกใช้ API ทั้งหมดบนเธรดหลักด้วย ฉันคิดว่าเพื่อความปลอดภัยของเธรด
หากคุณต้องการอัปเดตโมเดลจากเธรดของผู้ปฏิบัติงาน คุณต้องใช้วัตถุ ExternalEvent
เพื่อจัดคิว ( Raise()
) อินสแตนซ์ IExternalEventHandler
ไปยังเธรดหลักเพื่อเรียก Revit API นี่คือรูปแบบอะซิงโครนัสที่ Revit จัดเตรียมไว้เพื่อกำหนดเวลาการเรียก API ใหม่
สำหรับ 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] หากคุณมีปัญหาใด ๆ ในการใช้ห้องสมุดนี้