概要
Revit APIを使用する際、処理の待機時間を短縮したり、UIの応答性を向上させたりするために、非同期処理を活用したい場面があります。例えば下記のようなコードです。
class SyncIExternalEventHandler : IExternalEventHandler
{
UIApplication _uiApp;
UIDocument _uiDoc;
Document _doc;
/// <summary>
/// 過程を表示するためのWPF
/// </summary>
RevitAsyncWindow _window;
/// <summary>
/// 現在表示している全ての要素をいれる配列
/// </summary>
List<Element> _allElements;
/// <summary>
/// プロパティを持つ要素を入れるリスト
/// </summary>
List<Element> _havePropertyElements;
private readonly string originalPropertyName = "自作したプロパティ名";
public void Execute(UIApplication app)
{
_uiApp = app;
_uiDoc = _uiApp.ActiveUIDocument;
_doc = _uiDoc.Document;
//WPFを作成
_window = new RevitAsyncWindow();
_window.Show();
//全ての要素を収集
FilteredElementCollector collector = new FilteredElementCollector(_doc);
_allElements = collector.WhereElementIsNotElementType().ToList();
//UIを更新するタイミング
HashSet<int> checkPoints = new HashSet<int>(Enumerable.Range(1, 10).Select(i => i * (_allElements.Count / 10)));
//すべての要素を順番に見ていく
for (int i = 0; i < _allElements.Count; i++)
{
if (checkPoints.Contains(i))
{
int percent = (int)((float)i / _allElements.Count * 100);
//プログレスバーの更新処理
_window.UpdateProgressBar(percent);
}
//Elementからプロパティの値を抜き出す
var propertyValue = _allElements[i].LookupParameter(originalPropertyName);
//プロパティがない
if (propertyValue == null) continue;
//プロパティはあるが値が空の場合は処理しない
if (propertyValue.HasValue || propertyValue.AsString() != string.Empty)
{
//自作したプロパティをもつ要素をリストに入れる
_havePropertyElements.Add(_allElements[i]);
}
}
//WPFを閉じる
_window.Close();
}
public string GetName()
{
return "AsyncIExternalEventHandler";
//throw new NotImplementedException();
//これを残しておくと実行されないので注意
}
}
このコードは画面に表示されているすべての要素で特定のプロパティ値を調査し、調査の進捗をWPFのプログレスバーに表示する処理を行うためのコードです。しかし、同期的に実行されるため、プログレスバーが更新されません。また要素数が膨大な場合にはWPFの画面がフリーズし、調査の中断もできません。そこで本記事では非同期処理を活用し、UIのフリーズを防ぎながら快適な操作性を実現する方法を解説します。
環境
Revit2022
VS2019
方法
通常の非同期処理
C#玄人の方であれば「Task.Runなどを使用して非同期処理をすればよいのでは?」と考えるでしょう。しかし通常RevitAPIでは非同期処理を使用することはできません。 なぜなら、RevitAPIは単一のスレッドでのみ動作し、全てのAPI呼び出しはメインスレッドで実行する必要があるため外部から操作ができないからです。(多分)
RevitAPIの非同期処理
RevitAPIはメインスレッドからのみ処理を呼び出すことが求められているため、非同期処理を行うことはできません。そこで外部からの処理をメインスレッドに渡すことができるIExternalEventHandlerを使用します。これでRevitAPIの制限を回避しつつTask.Runの処理を実行できます。
Task.Runが利用できるのは、IExternalEventHandlerがバックグラウンドスレッドで行われている処理をRevitの許可が下りたタイミングでメインスレッドに戻して実行させるためです。(多分)
実装
IExternalEventHandlerを利用してTask.Runを行うコードがこちらになります。
/// <summary>
/// .addinに登録するクラス
/// </summary>
public class RevitButtonCommand : IExternalCommand
{
//Revitのボタンが押された時に実行される関数
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
//WPFを表示
RevitAsyncWindow window = new RevitAsyncWindow();
window.Show();
return Result.Succeeded;
}
}
/// <summary>
/// RevitAsyncWindow.xaml の相互作用ロジック
/// WPFのクラス
/// </summary>
public partial class RevitAsyncWindow : Window
{
private ExternalEvent m_ExEvent;
private AsyncIExternalEventHandler m_Handler;
public RevitAsyncWindow()
{
InitializeComponent();
//IExternalEventHandlerの準備
m_Handler = new AsyncIExternalEventHandler(this);
m_ExEvent = ExternalEvent.Create(m_Handler);
//IExternalEventHandlerの実行
m_ExEvent.Raise();
}
/// <summary>
/// プログレスバーを更新するための関数
/// </summary>
public void UpdateProgressBar(int value)
{
ProgressBar.Value = value;
}
}
//WPFから呼ばれるクラス
[Transaction(TransactionMode.Manual)]
public class AsyncIExternalEventHandler : IExternalEventHandler
{
UIApplication _uiApp;
UIDocument _uiDoc;
Document _doc;
/// <summary>
/// 過程を表示するためのWPF
/// </summary>
RevitAsyncWindow _window;
/// <summary>
/// 現在表示している全ての要素をいれる配列
/// </summary>
List<Element> _allElements;
/// <summary>
/// プロパティを持つ要素を入れるリスト
/// </summary>
List<Element> _havePropertyElements;
private readonly string originalPropertyName = "自作したプロパティ名";
public AsyncIExternalEventHandler(RevitAsyncWindow window)
{
_window = window;
}
public async void Execute(UIApplication app)
{
_uiApp = app;
_uiDoc = _uiApp.ActiveUIDocument;
_doc = _uiDoc.Document;
//非同期処理
await Task.Run(() =>
{
//全ての要素を収集
FilteredElementCollector collector = new FilteredElementCollector(_doc);
_allElements = collector.WhereElementIsNotElementType().ToList();
//UIを更新するタイミング
HashSet<int> checkPoints = new HashSet<int>(Enumerable.Range(1, 10).Select(i => i * (_allElements.Count / 10)));
//すべての要素を順番に見ていく
for (int i = 0; i < _allElements.Count; i++)
{
if (checkPoints.Contains(i))
{
int percent = (int)((float)i / _allElements.Count * 100);
//プログレスバーの更新処理
//UIの更新はUIスレッドで行わなければエラーになる
_window.Dispatcher.Invoke(() =>
{
_window.UpdateProgressBar(percent);
});
}
//Elementからプロパティの値を抜き出す
var propertyValue = _allElements[i].LookupParameter(originalPropertyName);
//プロパティがない
if (propertyValue == null) continue;
//プロパティはあるが値が空の場合は処理しない
if (propertyValue.HasValue || propertyValue.AsString() != string.Empty)
{
//自作したプロパティをもつ要素をリストに入れる
_havePropertyElements.Add(_allElements[i]);
}
}
});
//非同期処理が終了したら実行
//WPFを閉じる
_window.Close();
}
public string GetName()
{
return "AsyncIExternalEventHandler";
//throw new NotImplementedException();
//これを残しておくと実行されないので注意
}
}
処理を大まかに説明すると、
まず、Revit上でボタンを押すとRevitButtonCommandクラスのExecuteメソッドが実行され、WPFのウィンドウが表示されます。次に、WPFクラス内でIExternalEventHandlerの準備と実行を行い、そのIExternalEventHandlerを継承したクラス内で、すべての要素を調べる非同期処理を実行します。
Task.Runを使った非同期処理により、画面がフリーズすることなく、WPFのプログレスバーがスムーズに更新されます。
警告
Dispatcher.Invokeを使用してUIスレッドでUIの更新を行わないとエラーもしくは処理が終わらなくなるので注意してください(4敗)
終わりに
本記事ではRevitAPIを使用する際に非同期処理を活用する方法を解説しました。
「RevitAPI WPF 画面固まる」、「RevitAPI 非同期処理」
といった検索を行っている方の助けとなれば幸いです。
本記事のコードで非同期処理が可能になりますが、可能となった理由に自信がありません。だってアメリカ語の記事しかないんですもん。
もし間違っていたら、ご指摘いただけると嬉しいです。
おまけ
構成の簡略化
本記事のコードでは
Revitボタンで呼び出すクラス→WPFのクラス→IExternalEventHandlerを継承したクラス
という流れで処理を呼び出す構成にしています。
しかしこの構成はクラス呼び出しを2回行っているためコードが深くなり、可読性が低くなっています。「WPFのボタンを押したときに非同期処理を行う」という動作を行いたいならばこれでよいのですが、今回の場合は「Revitボタンで呼び出すクラス内で非同期処理を行ってWPFクラスに渡す」という流れの方がシンプルな構造となり、分かりやすくなります。
通常ではRevitボタンで呼び出すクラスのIExternalCommandのExecute関数は返り値があるためasyncを付けて非同期にできません。しかしRevit.asyncライブラリを使用することで非同期にすることができます。
//Revitのボタンを押したときに実行
public class RevitAsyncCommand : IExternalCommand
{
UIApplication _uiApp;
UIDocument _uiDoc;
Document _doc;
/// <summary>
/// 過程を表示するためのWPF
/// </summary>
RevitAsyncWindow _window;
/// <summary>
/// 現在表示している全ての要素をいれる配列
/// </summary>
List<Element> _allElements;
/// <summary>
/// プロパティを持つ要素を入れるリスト
/// </summary>
List<Element> _havePropertyElements;
private readonly string originalPropertyName = "自作したプロパティ名";
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
_uiApp = commandData.Application;
_uiDoc = _uiApp.ActiveUIDocument;
_doc = _uiDoc.Document;
//WPFの表示
_window = new RevitAsyncWindow();
_window.Show();
//非同期処理の準備
RevitTask.Initialize(_uiApp);
RevitTask.RunAsync(async () =>
{
//非同期処理
await Task.Run(() =>
{
//全ての要素を収集
FilteredElementCollector collector = new FilteredElementCollector(_doc);
_allElements = collector.WhereElementIsNotElementType().ToList();
//UIを更新するタイミング
HashSet<int> checkPoints = new HashSet<int>(Enumerable.Range(1, 10).Select(i => i * (_allElements.Count / 10)));
//すべての要素を順番に見ていく
for (int i = 0; i < _allElements.Count; i++)
{
if (checkPoints.Contains(i))
{
int percent = (int)((float)i / _allElements.Count * 100);
//プログレスバーの更新処理
//UIの更新はUIスレッドで行わなければエラーになる
_window.Dispatcher.Invoke(() =>
{
_window.UpdateProgressBar(percent);
});
}
//Elementからプロパティの値を抜き出す
var propertyValue = _allElements[i].LookupParameter(originalPropertyName);
//プロパティがない
if (propertyValue == null) continue;
//プロパティはあるが値が空の場合は処理しない
if (propertyValue.HasValue || propertyValue.AsString() != string.Empty)
{
//自作したプロパティをもつ要素をリストに入れる
_havePropertyElements.Add(_allElements[i]);
}
}
});
//非同期処理が終了したら実行
//WPFを閉じる
_window.Close();
});
return Result.Succeeded;
}
}
代替案
IExternalEventHandler もRevit.asyncも使いたくないという人はDoEventsを使用することでUIの更新ができます。しかしこの方法は非常に処理が遅くなるためお勧めはしません。
/// <summary>
/// UIをブロックしないためのおまじない
/// </summary>
public void WPFDoEvents()
{
DispatcherFrame frame = new DispatcherFrame();
var callback = new DispatcherOperationCallback(obj =>
{
((DispatcherFrame)obj).Continue = false;
return null;
});
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, callback, frame);
Dispatcher.PushFrame(frame);
}
その他参考サイト