通知のプレースホルダの改修案
通知のカスタムフォーマットでできないことのうち、次の 2 つを本体の改修で補う案です。ここにあるのは本体の標準機能ではありません。
- リンク先のテーブルの項目(
[ClassA~1234,ClassB]の形式)を本文・件名に出す - 書式行の
Prefix・Delimiterなどに書いた{Url}・{UserName}などを展開する
現行の仕組みは 通知のカスタムフォーマットとリマインダーの内部動作 を参照してください。
前提にした現行実装(1.5.8.1)
- 本文は
NoticeBodyが書式を 1 行ずつNotificationColumnFormatの JSON として読み、Nameから項目が特定できた行はToNoticeで値を出し、それ以外の行だけReplacedContextValuesで{Url}など 4 つを置換する(ResultModel.cs) - 項目の特定は
SiteSettings.IncludedColumnsで、角括弧の中身と自サイトのColumnsの項目名の完全一致(SiteSettings.cs) - 件名は
ReplacedDisplayValuesで[項目名]を表示値に置き換える(ResultModel.cs) NoticeBody・ReplacedDisplayValues・ReplacedContextValuesは CodeDefiner のテンプレート(App_Data/Definitions/Definition_Code/のModel_Notice_Body.txt・Model_ReplacedDisplayValues_Body.txt・Model_ReplacedContextValues_Body.txt)から Issues・Results・Wikis の各モデルに生成される。モデルの.csを直接直しても CodeDefiner の実行で上書きされる
リンク先の項目を表す ClassA~1234,ClassB の形式は、SiteSettings.GetColumn が , を含む名前を見つけたときに AddJoinedColumn でリンク先の SiteSettings から項目を作る経路で解決されます(SiteSettings.cs)。IncludedColumns はこの経路を通らないため、通知ではリンク先の項目を解決できません。
1. リンク先の項目を出す
案の比較
| 案 | 内容 | 変更範囲 | 性能 | 既存への影響 |
|---|---|---|---|---|
| A | IncludedColumns に Context 付きの版を足し、自サイトで見つからず , を含む名前なら GetColumn で解決する | 小 | 結合項目 1 つごとにリンク先を 1 回読む | 引数なしの版を残せば通知以外に影響しない |
| B | IncludedColumns は変えず、NoticeBody の中で見つからなかったときだけ GetColumn にフォールバックする | 中 | A と同じ | 件名(ReplacedDisplayValues)にも同じ処理を別に書く必要がある |
| C | 書式の結合項目を先に集め、ss.Join() でリンク先を JOIN した SQL で一括取得する | 大 | JOIN で 1 回 | 通知の SQL が変わるため回帰のリスクが高い |
変更が小さく、件名と本文を同じ入口で扱える 案 A を軸にするのが現実的です。
案 A の変更点
| ファイル | 変更 |
|---|---|
Libraries/Settings/SiteSettings.cs | IncludedColumns(Context context, string value, bool labelText = false) を足す。自サイトで見つからず、名前に , を含むときだけ GetColumn(context, columnName) を呼ぶ。既存の IncludedColumns(value, labelText) はこちらを context: null で呼ぶだけにする |
Model_Notice_Body.txt | ss.IncludedColumns(data.Format?.Name) を Context 付きに変え、項目が結合項目(column.Joined)なら専用の処理で値を出す |
Model_ReplacedDisplayValues_Body.txt | 件名の置換も Context 付きの IncludedColumns に変える |
IncludedColumns の変更イメージ
public List<Column> IncludedColumns(
Context context,
string value,
bool labelText = false)
{
var columns = new List<Column>();
if (!value.IsNullOrEmpty())
{
foreach (Match match in value.RegexMatches(@"(?<=\[).+?(?=\])"))
{
var isValue = false;
var columnName = match.Value;
if (!labelText && columnName.StartsWith("@"))
{
isValue = true;
columnName = columnName.Substring(1);
}
var column = labelText
? Columns.FirstOrDefault(o => o.LabelText == columnName)
: Columns.FirstOrDefault(o => o.ColumnName == columnName);
// 追加: 自サイトに無く、結合項目の形式なら GetColumn で解決する
if (column == null && context != null && columnName.Contains(','))
{
column = GetColumn(context: context, columnName: columnName);
}
if (column != null)
{
column.OutputType = isValue
? Column.OutputTypes.Value
: Column.OutputTypes.DisplayValue;
columns.Add(column);
}
}
}
return columns.DistinctBy(column => column.ColumnName).ToList();
}結合項目の値の取り方
項目が解決できても、リンク先のレコードの値は通知を出すモデルに読み込まれていません。NoticeBody の項目行の分岐に、結合項目用の処理を足します。
- 結合項目の名前(
ColumnNameInfo)から、リンク先のSiteSettings(ss.JoinedSsHash)とリンク元の項目(ClassA)を得る - リンク元の項目の値からリンク先のレコード ID を得る。値が空なら何も出さない
- リンク先の種類(Issues・Results・Wikis)に応じてモデルを読み、項目の表示値を
ToNoticeに渡す
値を取る処理のイメージ
private string GetJoinedColumnNotice(
Context context,
SiteSettings ss,
Column column,
NotificationColumnFormat notificationColumnFormat,
bool update)
{
var columnNameInfo = new ColumnNameInfo(column.ColumnName);
var joinedSs = ss.JoinedSsHash.Get(columnNameInfo.SiteId);
if (joinedSs == null) return string.Empty;
// リンク元の項目(例: ClassA)の値 = リンク先のレコード ID
var linkColumnName = columnNameInfo.TableAlias
.Split('-').Last().Split('~').First();
var linkedRecordId = PropertyValue(
context: context,
column: ss.GetColumn(context: context, columnName: linkColumnName))
?.ToLong() ?? 0;
if (linkedRecordId == 0) return string.Empty;
switch (joinedSs.ReferenceType)
{
case "Results":
var resultModel = new ResultModel(
context: context,
ss: joinedSs,
resultId: linkedRecordId);
return resultModel.ToDisplay(
context: context,
ss: joinedSs,
column: joinedSs.GetColumn(
context: context,
columnName: columnNameInfo.Name),
mine: resultModel.Mine(context: context))
.ToNotice(
context: context,
saved: string.Empty,
column: column,
notificationColumnFormat: notificationColumnFormat,
updated: false,
update: update);
// Issues・Wikis も同様
default:
return string.Empty;
}
}注意点
| 観点 | 内容 |
|---|---|
| 権限 | 一覧の結合項目はリンク先の閲覧権限を確かめてから出している(GridData.cs)。通知でも同じ確認を入れないと、読めないレコードの値が通知に載る |
| 性能 | 結合項目 1 つごとにリンク先を 1 回読む。同じリンク先の項目が複数あるときは、NoticeBody の先頭で書式を走査してリンク先ごとにまとめて読むとよい |
| 変更前後 | 更新時の「変更前 => 変更後」を出すには、リンク先の変更前の値も要る。まずは現在の値だけを出し、差分は後回しにする |
JoinedSsHash | 通知の時点、特にプロセスの通知で JoinedSsHash が用意されているかを確かめる |
| 多段のリンク | ClassA~1234-ClassB~5678,ClassC のような多段は、中間のテーブルを順にたどる必要がある。対応するかを決めておく |
確認したいケースは、単一のリンク、リンク先が Issues・Results・Wikis のそれぞれ、リンク元が空、リンク先が削除済み、閲覧権限なし、件名での使用、プロセスの通知、更新時の差分表示です。
なお、リンク項目そのもの([ClassA])は、改修しなくても選択肢の表示名(リンク先のタイトル)に変換されて出ます。
2. 書式行の {Url} などを展開する
{"Name":"[Title]","Prefix":"{Url} "} の {Url} は、項目行が ReplacedContextValues を通らないため展開されません。1 行に {"Name":"[Title]"}{Url} と続けて書く方法は JSON として読めなくなり、項目行になりません。
案の比較
| 案 | 内容 | Prefix などの {Url} | 同じ行の JSON + 文字列 | 変更量 | 副作用 |
|---|---|---|---|---|---|
| A | NoticeBody の戻り値全体に ReplacedContextValues をかける | 展開される | 解決しない(JSON がそのまま出る) | テンプレートの 1 行 | 項目の値に {Url} などの文字列が入っていると、それも置換される |
| B | 項目行の分岐に入る前に、NotificationColumnFormat の文字列プロパティだけを置換する | 展開される | 解決しない | NotificationColumnFormat.cs にメソッド 1 つ + テンプレート | 項目の値には影響しない |
| C | 行の先頭の JSON オブジェクトを括弧の対応で切り出し、後ろの文字列を別に置換する | 展開されない(A か B と併用) | 解決する | テンプレートにパーサーを足す | 括弧の解析に誤りが入りやすい |
案 B を勧めます。置換の対象が書式の設定値だけに限られ、利用者が入力した項目の値は変わりません。Prefix で {Url} が使えるようになれば、1 行に JSON と文字列を続ける書き方は要らなくなります。急ぐ場合は 1 行で済む案 A でもかまいません(項目の値に {Url} などの文字列そのものが入る場面はまれです)。
案 B の変更イメージ
// NotificationColumnFormat.cs に追加
public void ReplaceVariables(System.Func<string, string> replacer)
{
if (replacer == null) return;
Prefix = Prefix != null ? replacer(Prefix) : null;
Delimiter = Delimiter != null ? replacer(Delimiter) : null;
Allow = Allow != null ? replacer(Allow) : null;
StartBracket = StartBracket != null ? replacer(StartBracket) : null;
EndBracket = EndBracket != null ? replacer(EndBracket) : null;
DeletePrefixSymbol = DeletePrefixSymbol != null ? replacer(DeletePrefixSymbol) : null;
DeleteSuffixSymbol = DeleteSuffixSymbol != null ? replacer(DeleteSuffixSymbol) : null;
AddPrefixSymbol = AddPrefixSymbol != null ? replacer(AddPrefixSymbol) : null;
AddSuffixSymbol = AddSuffixSymbol != null ? replacer(AddSuffixSymbol) : null;
}// Model_Notice_Body.txt の項目行の分岐(else)の先頭に追加
data.Format?.ReplaceVariables(
v => ReplacedContextValues(context: context, value: v));案 A の場合は、NoticeBody の最後の return body.ToString(); を return ReplacedContextValues(context: context, value: body.ToString()); に変えます(Model_Notice_Body.txt)。項目行以外の行はすでに置換済みなので、2 回かけても結果は変わりません。
プレフィックス(通知の「プレフィックス」欄)は Notification.Send で件名に連結されるだけなので、ここで {Url} などを使いたい場合は、Send の呼び出し元で件名と同じ置換をかけてから渡す変更が別に要ります。