いくつかのカスタムコンポーネントを作っていたら、共通の値を参照したい場面がでてきました。
Grasshopperの仕組みで考えると、設定用のコンポーネントを用意して、そこから各コンポーネントへ接続して値を渡す構成はできそうです。
でも、それってスマートじゃないですよね。コンポーネントをたくさん使う場面では接続が増えて画面がごちゃごちゃしそうです。
共通の設定を1つ用意して、他のコンポーネントから参照できないか試してみました
シングルトンパターンで設定を共有
設定クラスをシングルトンパターンで実装して、他のコンポーネントから参照してみます。
設定(CommonSettings)クラス
まずは、設定を保存するクラスをシングルトンパターンで用意します。これを使って複数のカスタムコンポーネントから同じ設定クラスのインスタンスを参照します。
この例ではテスト用に文字型と数値型のプロパティを1つずつ用意しています。
internal class CommonSettings
{
// A variable for keeping only one instance in a singleton pattern
private static CommonSettings _instance;
/// <summary>
/// String Property
/// </summary>
public string StrValue { get; set; }
/// <summary>
/// Numeric Property
/// </summary>
public double DblValue { get; set; }
/// <summary>
/// Constructor
/// </summary>
private CommonSettings()
{
StrValue = "default_value";
DblValue = 3.14;
}
/// <summary>
/// Returns a common instance using the singleton pattern.
/// </summary>
public static CommonSettings Instance
{
get
{
if (_instance == null)
{
_instance = new CommonSettings();
}
return _instance;
}
}
}
設定コンポーネント
設定を変更するカスタムコンポーネントを用意します。上述の設定クラス(CommonSettingsクラス)を参照して、値を更新する処理を行います。
public class SettingsComponent : GH_Component
{
// The common settings instance.
private CommonSettings _settings;
/// <summary>
/// Initializes a new instance of the SettingsComponent class.
/// </summary>
public SettingsComponent()
: base("Common Settings", "Nickname",
"Description",
"MyGHA", "Settings")
{
// Retrieve the common settings instance
_settings = CommonSettings.Instance;
}
/// <summary>
/// Registers all the input parameters for this component.
/// </summary>
protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
{
pManager.AddTextParameter("String Value", "S", "String value", GH_ParamAccess.item);
pManager.AddNumberParameter("Double Value", "D", "Double value", GH_ParamAccess.item);
// options
pManager[0].Optional = true;
pManager[1].Optional = true;
}
/// <summary>
/// Registers all the output parameters for this component.
/// </summary>
protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
}
/// <summary>
/// This is the method that actually does the work.
/// </summary>
/// <param name="DA">The DA object is used to retrieve from inputs and store in outputs.</param>
protected override void SolveInstance(IGH_DataAccess DA)
{
// Retrieve values from the common settings
string sValue = _settings.StrValue;
double dValue = _settings.DblValue;
if(!DA.GetData(0, ref sValue))return;
if(!DA.GetData(1, ref dValue))return;
// Update common settings
if (_settings.StrValue != sValue || _settings.DblValue != dValue)
{
_settings.StrValue = sValue;
_settings.DblValue = dValue;
ExpireSolution(true);
}
}
...
設定を参照するコンポーネント
設定コンポーネントで設定した値を参照する側のコンポーネントを用意します。この例では、単純に取得した値をOutputへ出力します。
public class TestSettingComponent : GH_Component
{
// The common settings instance.
private CommonSettings _settings;
/// <summary>
/// Initializes a new instance of the TestSettingComponent class.
/// </summary>
public TestSettingComponent()
: base("Test Settings", "Nickname",
"Description",
"MyGHA", "Settings")
{
// Retrieve the common settings instance
_settings = CommonSettings.Instance;
}
/// <summary>
/// Registers all the input parameters for this component.
/// </summary>
protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
{
pManager.AddGeometryParameter("Geometry", "G", "Geometry to extract settings from", GH_ParamAccess.item);
// options
pManager[0].Optional = true;
}
/// <summary>
/// Registers all the output parameters for this component.
/// </summary>
protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
pManager.AddTextParameter("String Value", "S", "String value", GH_ParamAccess.item);
pManager.AddNumberParameter("Double Value", "D", "Double value", GH_ParamAccess.item);
}
/// <summary>
/// This is the method that actually does the work.
/// </summary>
/// <param name="DA">The DA object is used to retrieve from inputs and store in outputs.</param>
protected override void SolveInstance(IGH_DataAccess DA)
{
// Get values from common settings
string sValue = _settings.StrValue;
double dValue = _settings.DblValue;
// Output the retrieved values
DA.SetData(0, sValue);
DA.SetData(1, dValue);
}
...
動作を確認
で、早速試してみると。。。
共通設定の参照はうまく行くできたものの、即反映される訳ではないので動きがいまいちです。
動画では、SolveInstance()メソッドを再実行するためにGeometryコンポーネントをつなぎ直しています。
値を受け取って処理をはじめる段階で設定が参照できていればいいので、まあ、いいと言えば良いのですが。。。、できれば即座に反映されて欲しいですよね。
イベント処理で更新
即座に値を反映させるため、共通設定のクラス(CommonSettings)へ値の更新イベント処理を追加し、参照するコンポーネントではそのイベントを購読して処理してみます。
こんどはいい感じです。設定を変更すると、他のコンポーネントに即座に反映されるようになりました。(つながってないコンポーネント間で値が反映されるので、慣れないとちょっと気持ち悪いかもです)
変更点
コードの変更点をまとめると、
設定クラス(CommonSettings)
にイベント (SettingsChanged
) を追加。プロパティ(設定値)が変更されたらイベントを発火させる。- 各コンポーネントでは、このイベントを購読して処理を実行する(
ExpireSolution
を呼び出す)
これでCommonSettings
が更新されると、他のコンポーネントがその変更を検知して処理を行うことができます。
以下、変更したコードです。
CommonSettingsクラス
internal class CommonSettings
{
public event EventHandler SettingsChanged;
protected virtual void OnSettingsChanged()
{
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
private static CommonSettings _instance;
private string strValue;
/// <summary>
/// String Property
/// </summary>
public string StrValue
{
get=>strValue;
set
{
if (strValue != value)
{
strValue = value;
OnSettingsChanged();
}
;
}
}
private double dblValue;
/// <summary>
/// Numeric Property
/// </summary>
public double DblValue
{
get => dblValue;
set
{
if (dblValue != value)
{
dblValue = value;
OnSettingsChanged();
}
}
}
/// <summary>
/// Constructor
/// </summary>
private CommonSettings()
{
strValue = "default_value";
dblValue = 3.14;
}
/// <summary>
/// Returns a common instance using the singleton pattern.
/// </summary>
public static CommonSettings Instance
{
get
{
if (_instance == null)
{
_instance = new CommonSettings();
}
return _instance;
}
}
}
TestSettingComponentクラス(カスタムコンポーネント)
public class TestSettingComponent : GH_Component
{
// The common settings instance.
private CommonSettings _settings;
/// <summary>
/// Initializes a new instance of the TestSettingComponent class.
/// </summary>
public TestSettingComponent()
: base("Test Settings", "Nickname",
"Description",
"MyGHA", "Settings")
{
_settings = CommonSettings.Instance;
// Register event handler (subscribe to change event in CommonSettings)
_settings.SettingsChanged += OnSettingsChanged;
}
/// <summary>
/// Processing of common setting change event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnSettingsChanged(object sender, EventArgs e)
{
// Immediately execute ExpireSolution() when common settings are changed.
ExpireSolution(true);
}
/// <summary>
/// Process when a component is removed from the document
/// </summary>
/// <param name="document"></param>
public override void RemovedFromDocument(GH_Document document)
{
// Unregistering the event handler
_settings.SettingsChanged -= OnSettingsChanged;
base.RemovedFromDocument(document);
}
/// <summary>
/// Registers all the input parameters for this component.
/// </summary>
protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
{
pManager.AddGeometryParameter("Geometry", "G", "Geometry to extract settings from", GH_ParamAccess.item);
// options
pManager[0].Optional = true;
}
/// <summary>
/// Registers all the output parameters for this component.
/// </summary>
protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
pManager.AddTextParameter("String Value", "S", "String value", GH_ParamAccess.item);
pManager.AddNumberParameter("Double Value", "D", "Double value", GH_ParamAccess.item);
}
/// <summary>
/// This is the method that actually does the work.
/// </summary>
/// <param name="DA">The DA object is used to retrieve from inputs and store in outputs.</param>
protected override void SolveInstance(IGH_DataAccess DA)
{
// Get values from common settings
string sValue = _settings.StrValue;
double dValue = _settings.DblValue;
// Output the retrieved values
DA.SetData(0, sValue);
DA.SetData(1, dValue);
}
まとめ
まとめというか、今後の事を考えてのメモ。
- 基本的にはシングルトンパターンとイベント処理を組み合わせるとスマートに処理できそうです。
- この例だと、参照側コンポーネントで値を更新することも可能なので、そのあたりは更に工夫が必要。自分で作っているんだから自己責任ですが、うっかり変更して被害がでるのは避けたい。
- 設定用のコンポーネントが複数配置された紛らわしい。1個に制限する方法はあるんだろうか?
- .GHドキュメントを複数開くと、おそらく同じCommonSettingsのインスタンスが共有されてしまうので、なにか管理する仕組みが必要。DocumentIDで区別できそうだけど、.ghファイルってコピーしても変わらない。つまり同じDocumentIDのファイルが出来上がるので、プログラムから区別付かないので、どうしたものか?
- 実際に複数のコンポーネントに組み込んで、ちゃんと動くか検証はいるよね?
動作環境
以下の環境で動作を確認しています。
- Windows11 Pro(64bit, 23H2)
- Rhinoceros 8 SR10
- Visual Studio Professional 2022(Version 17.11.0)