[OutSystems]Extension開発用メモ

OutSystemsのExtension開発の資料があまり世の中にないのでTipsを残しておきます。随時追加予定です。なお、環境はPEなためDBMSはSQL Serverです。

# Printデバッグ
昔はExtensionのデバッグはPrintデバッグしかなかった気がします。その場合、C#のコードの中に以下のように書くとGeneral Logに変数の値などが出力できます。

```C#
GenericExtendedActions.LogMessage(AppInfo.GetAppInfo().OsContext, $"nameの値は: {name}", "");
```

今ではIDE上でデバッグできるっぽいですが、まだ試していません。→ [2022/3/21追記]IDE上でデバッグするにはサーバーにVisual Studioをインストールする必要があるようです。PE民には厳しいですが、自分でサーバー持っている人にはそう難しくないように感じました。

https://youtu.be/B3mXzytlbQw

# ExtensionのActionのInput/Output Parameterに任意のレコードリストを指定する方法
Actionに任意のレコードリストを渡してテーブルに登録したり、逆にテーブルから取得した値を任意のレコードリストに出力してくれると汎用的に使えて嬉しいですよね。この場合はObject型をうまく使います（JSONに変換する方法もありますが）。

任意のレコードリストを出力するためには一工夫必要です。Service StudioではObject型を任意のレコードリストに変換できないので、Object型が参照渡しなことを利用し、Input Parameterにレコードリストをオブジェクト変換（ToObject()）したものを設定し、そのObject型の変数をC#のコードの中で加工します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/337041/d5950c2b-b8f1-6fbd-8466-1a0a649ec45a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/337041/6e750c06-7f27-40bd-011a-fb01e917834a.png)

```Object型の引数をレコード（Entity）リスト型に変換する方法.cs
IOSList recordList = (IOSList)ssRecordListObject;

recordList.StartIteration();
while (!recordList.Eof)
{
	var record = recordList.Current;
	Type t = record.GetType();			
	foreach (var p in t.GetProperties())  // Entity型の場合。Structureの場合はt.GetFields()を利用することになるはず
	{
		var name = p.Name;
		var value = p.GetValue(record);
		if (name.StartsWith("ss"))  // "ss"から始まるものが自分で定義したAttribute
		{
			GenericExtendedActions.LogMessage(AppInfo.GetAppInfo().OsContext, $"{name}: {value}", "");
		}
	}				
	recordList.Advance();
}
recordList.EndIteration();
```

# SQL（SELECT）
C#ほとんどわかっていないので方法が正しいかはわからないですが一応これで動きました。今回のサンプルは単一行を取得する前提ですが、複数行の場合でも戻り値をList型にして `while (reader.Read())` のループの中でAdd()すればいいはずです。

```C#
public void MssGetPhysicalName(string sseSpaceName, string ssEntityName, out string ssPhysicalName) {
	DatabaseProvider dbp = DatabaseAccess.ForRunningApplication();
	RequestTransaction trans = dbp.GetRequestTransaction();

	string ssPhysicalName = string.Empty;
    string sql = "SELECT PHYSICAL_TABLE_NAME FROM OSSYS_ENTITY";
    sql += " INNER JOIN OSSYS_ESPACE ON OSSYS_ESPACE.ID = OSSYS_ENTITY.ESPACE_ID";
    sql += " WHERE OSSYS_ENTITY.NAME = @entityName";
    sql += " AND OSSYS_ENTITY.IS_ACTIVE = 1";
    sql += " AND OSSYS_ESPACE.NAME = @eSpaceName";
    sql += " AND OSSYS_ESPACE.IS_ACTIVE = 1";

    Command command = trans.CreateCommand(sql);
    command.CreateParameter("eSpaceName", DbType.String, eSpaceName);
    command.CreateParameter("entityName", DbType.String, entityName);
	using (IDataReader reader = command.ExecuteReader())
	{
		while (reader.Read())
		{
			ssPhysicalName = reader.GetString(0);  // 複数行を期待するならList.Add()する
		}
	}
}
```

# [2020/4/2追加]SQL（INSERT）
任意のEntityにレコードを追加しようとすると、列毎の型を判定しながらSQL文を組み立て、バインド変数を追加することになります。また、通常のEntity Action（CreateUserなど）と仕様を合わせるならば戻り値にIdを設定することになりますが、これを実現するために`ExecuteScalar()`を呼び出します。今回は戻り値はInteger型の想定ですが、OutSystemsは他にLong Integer型、Text型のIdがあるため、より汎用的にするならばここも工夫する必要があります。

```C#
public void MssInsertUser(string ssPhysicalName, object ssRecordObject, out in ssId) {
	DatabaseProvider dbp = DatabaseAccess.ForRunningApplication();
	RequestTransaction trans = dbp.GetRequestTransaction();

    string sql1 = $"INSERT INTO {physicalName} (";
    string sql2 = string.Empty;
    string sql3 = " VALUES (";
    string sql4 = string.Empty;
    string sql5 = ");";

    IRecord record = (IRecord)ssRecordObject;
    Type t = record.GetType();
    
    var command = trans.CreateCommand();

    foreach (var p in t.GetProperties())
    {
        var name = p.Name;
        var value = p.GetValue(record);
        var type = p.PropertyType;

        if (name.StartsWith("ss") && !name.Equals("ssId"))
        {
            // SQL文を追加＆型毎に変換しパラメーターに追加
            sql2 = BuildSQLForCreate(sql2, name, true);
            sql4 = BuildSQLForCreate(sql4, name, false);

            if (type == typeof(Int64))
            {
                command.CreateParameter(name, DbType.Int64, value);
            }
            else if (type == typeof(Int32))
            {
                command.CreateParameter(name, DbType.Int32, value);
            }
            else if (type == typeof(String))
            {
                command.CreateParameter(name, DbType.String, value);
            }
            else if (type == typeof(Decimal))
            {
                command.CreateParameter(name, DbType.Decimal, value);
            }
            else if (type == typeof(Boolean))
            {
                command.CreateParameter(name, DbType.Boolean, value);
            }
            else if (type == typeof(DateTime))
            {
                command.CreateParameter(name, DbType.DateTime, value);
            }
            else if (type == typeof(Byte[]))
            {
                command.CreateParameter(name, DbType.Binary, value);
            }
            else
            {
                throw new Exception($"メソッド名: { MethodBase.GetCurrentMethod().Name }\n"
                        + $"サポートしていない型です。物理テーブル名: {ssPhysicalName}, 列名: {name}, 型: {type}");
            }
        }
    }

    // 挿入したレコードの主キーの値を取得するためのSQL文を追加
    sql2 += ") output INSERTED.ID";
    command.CommandText = sql1 + sql2 + sql3 + sql4 + sql5;

    // SQLの実行
    // 挿入したレコードの主キーの値を取得するため、ExecuteNonQueryではなく、ExecuteScalarを実行
    object modified = command.ExecuteScalar();
    ssId = Convert.ToInt32(modified);
}

// Insert文の組み立て
private string BuildSQLForCreate(string sql, string name, bool isColumnName)
{
    string prefix = "";
    if (!sql.Equals(string.Empty))
    {
        prefix = ", ";
    }
    if(isColumnName)
    {
        sql += prefix + name.Substring(2);
    }
    else
    {
        sql += prefix + $"@{name}";
    }
    return sql;
}
```

# [2020/4/2追加]SQL（UPDATE）
ソースコードはINSERTとほぼ同じなので割愛します。主な違いは以下です。

* SQL文の組み立て（SET 列名1 = @列名1, 列名2 = @列名2, ...）
* 主キーの列名と値を取り出し、WHERE句に設定

# [2020/4/2追加]SQL（複数レコードをまとめてINSERT&UPDATE）
これを実現するための部品をForgeにアップロードしました。
https://www.outsystems.com/forge/component-overview/12825/createorupdatewithcommonprocess

# リストのソート
```C#
public class ValueCompare : System.Collections.IComparer  // System.Collections.Generic.IComparerではないので注意
{
	int IComparer.Compare(Object x, Object y)
	{
		Type t = x.GetType();
		PropertyInfo firstProperty = null;
		foreach (var p in t.GetProperties())
		{
			if (p.Name.StartsWith("ss"))
			{
				firstProperty = p;
				break;
			}
		}
		return firstProperty.GetValue(x).ToString().CompareTo(firstProperty.GetValue(y).ToString());
	}
}

IOSList recordList = (IOSList)ssRecordListObject;
ValueCompare vc = new ValueCompare();
recordList.Sort(vc);
```

# [2020/4/2追加]Visual Studio上でのソリューションのビルド
基本的にExtension開発は以下の流れで行っていますが、Actionの引数の型を変えた場合などは1-Click Publishのコンパイルでエラーが発生することがあります。

1. Integration StudioのEdit Sorce Code .Netボタンを押してVisual Studioを起動
1. Visual Studioでソースコードを書いて保存し、Visual Studioを閉じる
1. Integration Studioで1-Click Publish

その場合は、Visual Studioでソースコードを書いて保存した後にVisual Studio上でソリューションのビルドを行うとうまく行くことが多いです。ただ、初めはApp.configが存在しないためソリューションのビルドが行えません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/337041/c67bad1f-d449-5bb4-b986-5568ed13a70f.png)

この場合は「追加」→「新しい項目」でアプリケーション構成ファイルを追加するとうまくいきました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/337041/d12851e5-0828-c448-e552-d2e497469697.png)

この後にソリューションのビルドを行い、Visual Studioを閉じ、以降は通常と同じ流れで1-Click Publishを行います。

# [2020/4/2追加]Actionのアイコン変更方法
通常のアイコンはpngファイルなどでも大丈夫なのですが、なぜかExtensionのアイコンはpngファイルはダメでicoファイルに変更したところうまくいきました。icoファイルは[こちら](https://ao-system.net/alphaicon/)のサイトを使ってpngファイルから変換し作成しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/337041/4d3fae6c-ad45-26cf-4f95-9d3cad0d3310.png)

# 参考サイト
* [OutSystemsのExtensionでRange Action作ってみた](https://qiita.com/shoji9x9/items/1e4653ba2d2bae02ef21)
* [Using generic types with integration studio](https://www.outsystems.com/forums/discussion/53523/using-generic-types-with-integration-studio/)