Skip to content

แยกส่วนโค้ดด้วย MVVM & IExternalEventHandler

ปัญหาคลาสสิคของคนที่เพิ่งเริ่มทำ UI ด้วย WPF หรือ WinForms คือการนำ โค้ดของ Revit API, โค้ดของฐานข้อมูล, และโค้ดของหน้าต่าง ไปกองรวมกันในไฟล์เดียว (MainWindow.xaml.cs)

เมื่อโปรเจ็กต์ใหญ่ขึ้น โค้ดจะอ่านยากมาก (เรียกกันว่า Spaghetti Code) ทางแก้คือ Design Pattern ที่ชื่อว่า MVVM (Model-View-ViewModel) ครับ

1. MVVM คืออะไร ย่อมาจากอะไร?

Section titled “1. MVVM คืออะไร ย่อมาจากอะไร?”
  • M (Model): ตัวแทนข้อมูล/ฐานข้อมูล
  • V (View): ไฟล์หน้าจอ (MainWindow.xaml) หน้าที่ของมันคือสวยงามอย่างเดียว ไม่มี Logic ข้างใน
  • VM (ViewModel): ผู้จัดการตัวกลางที่เอา Model มาประมวลผล แล้วส่งข้อมูลต่อไปโชว์ให้ View เห็น

ใน WPF เราจะใช้ระบบ Data Binding เพื่อผูกค่าระหว่างช่อง TextBox ใน View เข้ากับคุณสมบัติ (Property) ที่เราสร้างไว้ใน ViewModel ทันทีที่ผู้ใช้พิมพ์ ข้อมูลในตัวแปร ViewModel จะเปลี่ยนออโต้! ไม่ต้องโยงชื่อ txtPrefix.Text = ... อีกต่อไป

2. ความท้าทายระดับยาก: Context ของ Revit

Section titled “2. ความท้าทายระดับยาก: Context ของ Revit”

Revit มีกฎศักดิ์สิทธิ์อยู่ข้อนึงคือ API ทุกอย่างจะต้องถูกทำงานใน Main Thread ของ Revit เท่านั้น ถ้าคุณสร้างหน้าต่างลอย (Modeless Window) แล้วคุณมีปุ่มที่ผูกกับ ViewModel แล้วปุ่มนั้นคลิกเพื่อสั่งสร้างคาน Revit จะแครชทันที เพราะจังหวะที่กดปุ่ม Thread นั้นมันหลุดออกไปจาก Thread หลักของโปรแกรม!

เพื่อรับมือกับปัญหาที่ว่านี้ เราเลยต้องใช้สิ่งที่เรียกว่า IExternalEventHandler

3. ตัวรับสาร (IExternalEventHandler)

Section titled “3. ตัวรับสาร (IExternalEventHandler)”

เพื่อให้ UI ของคุณ “สั่งงาน” Revit ได้อย่างปลอดภัย คุณต้องทำเครื่อง “ฝากข้อความ”

ElementRenameHandler.cs
using Autodesk.Revit.UI;
namespace RevitToolkit;
// 1. คนรับหน้าที่ถือ Event
public class RevitEventHandler : IExternalEventHandler
{
// ตัวแปรข้อมูลที่ ViewModel จะส่งมาฝากไว้
public string UserPrefix { get; set; }
// ฟังก์ชันนี้แหละ ที่มั่นใจได้ 100% ว่าจะรันบน Main Thread
public void Execute(UIApplication app)
{
// ...เอา UserPrefix เข้าไปรัน FilteredElementCollector
// ...เปิด Transaction("Rename Beams")
TaskDialog.Show("Revit", $"กำลังทำงานใน Revit API อย่างปลอดภัย ด้วย Prefix: {UserPrefix}");
}
public string GetName() => "RevitEventHandler";
}

4. ViewModel (ผู้จัดการ)

Section titled “4. ViewModel (ผู้จัดการ)”

เมื่อผู้ใช้กดปุ่มบนหน้าจอโค้ดจะวิ่งมาที่ผู้จัดการ แล้วผู้จัดการจะ “เคาะประตูเรียก” Event Handler อีกที

MainWindowViewModel.cs
using Autodesk.Revit.UI;
namespace RevitToolkit;
public class MainViewModel
{
// ตัวแปรที่ผูกกับช่องกรอกของ XAML
public string InputPrefix { get; set; } = "C-";
// พระเอก 2 ตัว
private readonly RevitEventHandler _handler;
private readonly ExternalEvent _exEvent;
public MainViewModel()
{
_handler = new RevitEventHandler();
_exEvent = ExternalEvent.Create(_handler); // ลงทะเบียนช่องทางพิเศษกับ Revit
}
// สมมติว่ามี Binding Command จากปุ่มมาโผล่ตรงนี้
public void ExecuteRunButton()
{
// 1. ฝากข้อมูลเข้าไป
_handler.UserPrefix = InputPrefix;
// 2. เคาะประตูกดกริ่งให้ Revit รู้ว่า "เห้ย มีงานมาให้ทำนะ!"
// แล้ว Revit จะหาจังหวะที่ตัวเองว่าง หันมาหยิบฟังก์ชัน Execute() ของ _handler ไปรันเอง
_exEvent.Raise();
}
}

🔍 ไขความลับระบบ Modeless UI และ Revit Modeless Threading

Section titled “🔍 ไขความลับระบบ Modeless UI และ Revit Modeless Threading”

เพื่อรับมือกับระบบ Threading ที่เข้มงวดของ Revit เรามาเจาะลึก API ขั้นสูงสองตัวที่ช่วยประสานการคุยข้ามโลกกันครับ:

  • มันคืออะไร: อินเทอร์เฟซตัวรับสารข้าม Thread ของ Revit
  • หน้าที่ในโค้ด: ทำหน้าที่เตรียมฟังก์ชัน Execute(UIApplication app) เพื่อรองรับงานเขียนแก้ไขใดๆ โดยฟังก์ชันนี้จะได้รับการการันตีว่าจะ “ถูกหยิบไปรันอยู่บน Thread หลักที่ปลอดภัยของ Revit API เสมอ”
  • ทำไมต้องใช้: เมื่อสร้างปุ่มกดบน WPF โค้ดจะรันอยู่บน Thread ของหน้าต่าง UI ซึ่งแยกตัวเป็นเอกเทศจาก Revit หากเราสั่งงานตรงๆ Revit จะปฏิเสธและดีด Error โค้ดหยุดรันทันที การสืบทอดอินเทอร์เฟซนี้จึงเป็นการฝากจดหมายงานอย่างเป็นระบบส่งเข้าศูนย์กลาง
  • มันคืออะไร: วัตถุผู้ส่งสัญญาณเรียกการประมวลผล (Event Trigger Broker)
  • หน้าที่ในโค้ด: ลงทะเบียนสร้างเครื่องมือรับส่งสัญญาณขึ้นมาในระบบของ Revit โดยคำสั่งที่สำคัญคือ:
    • ExternalEvent.Create(handler): ลงทะเบียนจับคู่อีเวนต์เข้ากับคลาสรับสารย่อยของเรา
    • _exEvent.Raise(): สั่ง “กดกริ่งสัญญาณกระตุ้นเรียก” เพื่อสะกิดเตือน Revit ว่ามีงานใหม่เข้ามาในแถวคิว จากนั้น Revit จะหาจังหวะที่ตัวโปรแกรมว่างและหันมาประมวลผลคำสั่งในเมธอด Execute ของตัวรับสาร
  • ทำไมต้องใช้: ช่วยเป็นนายหน้าประสานคิวงานข้าม Thread ได้อย่างสมบูรณ์แบบ ทำให้ปลั๊กอินของเรามีหน้าต่างเครื่องมือลอยโชว์ข้อมูลเคียงข้างไปพร้อมๆ กับที่ผู้ใช้หมุนดูโมเดลหรือแก้ไขแบบในจอหลักได้อย่างพรีเมียมและปราศจากการ Crashes