MDataTable 을 미리보기에서 실제로 돌린다 — 태그만 채우면 반쯤 빈 것을 못 본다
"태그와 MDataTable도 환자 선택하면 해당 정보로 조회되는지 확인가능해야" 한다는 요구.
확인해 보니 미리보기가 DataTableViewModel 을 아예 건너뛰고 있었다(PrintService:87,143).
서식의 값 상당수가 태그가 아니라 이 관으로 오는데(운영 실측 Rows 형 1,624건)
그쪽은 미리보기에 존재하지 않았다.
## 레거시 구조를 먼저 확인했다
TK_PREVIEW 는 meLoadMode 를 Runtime 으로 바꾸고 TestPatientSetting() 으로
환자 선택 창을 띄운다(frmSheetDesigner.vb:452-462). 고르는 것만으로는 화면이 안 바뀐다 —
moPatInfoBiz 는 필드에 담기기만 하고(:634-674), 서식을 다시 열어야
ucLoadSheetBase(moWrkInfoBiz, moPatInfoBiz, …, EN_LoadType.Edit) 로 로드되면서
그때 태그와 MDataTable 이 실행된다(:216). 탭마다 별도 인스턴스라
환자를 바꿔도 이미 열린 탭은 옛 환자를 계속 들고 있다.
SheetMe 는 1단계다. UsePatient 가 해석기를 갈고 즉시 다시 그린다.
창이 하나뿐이라 잔상도 없다. 이 차이는 의도한 것이다.
## 치환 엔진(QuerySubstitution)
clsMDataTable.ConvertQuery 를 옮겼다. 한 칸이라도 다르면 미리보기가 운영과
다른 SQL 을 돌린다 — "미리보기에서는 나왔는데 실제로는 안 나온다"가 되고
사람이 확인했다고 믿고 넘어가므로 미리보기가 없는 것보다 나쁘다.
처음에 갈래 하나를 반대로 만들었다. 대문자 Item 처럼 정규식이 안 맞는 경우를
"토큰이 남는다"로 단정했는데, GetPropertyInfo 는 정규식 실패에 ""를 돌려주고(:118)
호출부가 그걸 빈 문자열로 치환한다(:66-70) — 즉 지워진다.
토큰이 남는 갈래는 <b>접두어 불일치</b>뿐이다(oBaseObj 가 Nothing 이라 Replace 를 안 한다).
소스를 읽어 고쳤다. 두 갈래를 섞으면 안 되는 이유는 하나는 ORA 구문오류가 되고
다른 하나는 조건이 사라진 SQL 이 조용히 도는 것이라서다.
SQL 에서는 같아지는 것들도 사람에게는 갈라서 말한다 —
Unknown(아직 안 옮긴 속성) / Empty(값이 빔) / NotAVariable(접두어 틀림).
셋은 고칠 곳이 전부 다르다.
## 짐작하지 않는 변수원(PatientQueryVariableSource)
DataRow 접근형이 이 기능의 대부분을 실어 준다 — PatInfDR.item("아무컬럼") 은
SELECT * 결과 사전을 그대로 조회하면 되므로 <b>내가 컬럼을 알 필요가 없다.</b>
짐작할 것이 없으니 조용히 틀릴 일도 없다.
스칼라도 대부분 그 다섯 행의 컬럼 하나라, 컬럼 이름만 적고 있으면 값·없으면 모른다고
답한다. 존재 여부는 DB 가 판정한다. UDF 산출값(Age·Sex)은 넣지 않았다.
## 값이 안 나올 때를 위한 창
종이에는 "[MDataTable1.ALGYON — 조회 결과가 0행입니다]" 한 줄만 나온다.
그것으로는 쿼리가 틀렸는가·치환이 빈 값이 됐는가·이 환자에게 자료가 없는가를 못 가른다.
그래서 데이터소스 창이 <b>치환을 마친 SQL</b>을 그대로 보여 준다.
값이 안 나왔을 때 봐야 하는 것은 결과가 아니라 무엇을 물었는가다.
레거시에는 이걸 볼 수단이 없었다 — 런타임이 빈 catch 로 삼켜 "빈칸"만 남았다.
진단 화면 표본도 <b>실패 상태</b>로 찍는다. 성공 화면만 회귀 대상으로 두면
정작 사람이 오래 들여다보는 화면이 검사에서 빠진다.
## 안전
- 실행은 OracleQueryWorkbench.Trial 을 그대로 쓴다 — SELECT/WITH 문이 이미 거기 있다.
문을 두 군데 두면 한쪽이 느슨해진다.
- 치환 값은 레거시처럼 원문 그대로 박힌다(그래야 같은 SQL 이다). 따옴표가 섞인 값은
QuotedValues 로 드러내고 실행은 SELECT/WITH 문이 막는다.
- 데이터소스당 한 번만 실행하고 캐시한다 — 배선 40개면 왕복 40회가 된다.
- 환자를 바꾸면 러너를 새로 만든다. 재사용하면 태그만 바뀌고 표는 옛 환자가 남는데
그건 아무도 눈치채지 못한다. 서식을 갈아탈 때도 다시 만든다(옛 서식의 쿼리를 쓰게 된다).
## 아직 안 되는 것
Select 형의 필터(DataTable.Select 메모리 문법)는 옮기지 않았다.
무시하고 행 번호만 쓰면 다른 행의 값이 조용히 찍히므로, 값을 내지 않고 사유를 말한다.
운영 다수파인 Rows 형(76%)은 된다.
## 게이트
- dotnet test 323/323 (치환 판정 10건 신규)
- --edit-smoke 실패 0 (배선 판정 7건 추가, 대조군 포함)
- --db-patient ①~⑱ 전건 통과. ⑯ 치환한 SQL 이 실제로 1행을 뽑고,
⑱ 대조군은 환자 없이 같은 쿼리가 "WHERE ComNum = AND ... = ''" 로 깨진다 —
이 대조가 없으면 ⑯ 은 "쿼리가 원래 환자와 무관했다"와 구분되지 않는다
- --dialog-shots FAIL 0 (08c-datasource-result 추가)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
- --db-smoke 1,271건 diff 0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f50a7cff4b
commit
9bff3dd5f6
@@ -0,0 +1,193 @@
|
||||
using SheetMe.Core.Catalog;
|
||||
using SheetMe.Core.Models;
|
||||
using SheetMe.Data.Stores;
|
||||
|
||||
namespace SheetMe.Designer.Services;
|
||||
|
||||
/// <summary>한 MDataTable 의 실행 결과 — 또는 왜 실행하지 못했는지</summary>
|
||||
/// <param name="Name">데이터소스 컨트롤 이름(배선이 이 이름으로 찾는다)</param>
|
||||
/// <param name="Sql">치환을 마친 SQL — 사람이 눈으로 확인해야 하는 그 문장</param>
|
||||
/// <param name="Columns">결과 컬럼</param>
|
||||
/// <param name="Rows">결과 행</param>
|
||||
/// <param name="Error">실패 사유. null 이면 성공</param>
|
||||
/// <param name="Unknown">치환하지 못한 변수들 — 값이 빈 이유가 여기 있을 수 있다</param>
|
||||
public sealed record MDataTableResult(
|
||||
string Name, string Sql,
|
||||
IReadOnlyList<string> Columns, IReadOnlyList<string[]> Rows,
|
||||
string? Error, IReadOnlyList<string> Unknown)
|
||||
{
|
||||
public bool Ok => Error is null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 미리보기에서 MDataTable 을 <b>실제로 돌린다</b> — 레거시 <c>ucLoadSheetBase</c> 가
|
||||
/// 서식을 로드할 때 하던 일이다.
|
||||
///
|
||||
/// <b>왜 필요한가.</b> 서식의 값 상당수는 태그가 아니라 이 경로로 온다. 태그만 채워 보면
|
||||
/// "미리보기는 되는데 실제 서식은 반쯤 빈" 상태를 확인할 수 없다.
|
||||
///
|
||||
/// <b>실행은 읽기만.</b> <see cref="OracleQueryWorkbench.Trial"/> 을 그대로 쓴다 —
|
||||
/// SELECT/WITH 만 통과시키는 문이 이미 거기 있고, 이 접속은 운영에 붙을 수 있다.
|
||||
/// 문을 두 군데 두면 한쪽이 느슨해진다.
|
||||
///
|
||||
/// <b>치환 값이 SQL 에 원문 그대로 박힌다</b>(레거시가 그렇다 — 그래야 같은 SQL 이 된다).
|
||||
/// 따옴표가 섞인 값은 <see cref="QuerySubstitutionResult.QuotedValues"/> 로 드러내고
|
||||
/// 실행은 SELECT/WITH 문이 막는다.
|
||||
///
|
||||
/// <b>한 번만 돌린다.</b> 컨트롤마다 돌리면 배선 40개짜리 서식에 왕복 40회가 된다 —
|
||||
/// 데이터소스 단위로 캐시한다. 환자를 바꾸면 러너를 새로 만든다(같은 러너를 재사용하면
|
||||
/// 옛 환자의 표가 남는다).
|
||||
/// </summary>
|
||||
public sealed class MDataTableRunner : IDataFieldResolver
|
||||
{
|
||||
#region Member Fields
|
||||
private readonly IQueryVariableSource variables;
|
||||
private readonly OracleQueryWorkbench? workbench;
|
||||
private readonly Dictionary<string, MDataTableResult> cache = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>이름 → 그 데이터소스의 쿼리 원문</summary>
|
||||
private readonly Dictionary<string, string> queries = new(StringComparer.OrdinalIgnoreCase);
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
/// <param name="document">데이터소스와 그 쿼리를 여기서 모은다</param>
|
||||
/// <param name="variables">치환 값을 내주는 쪽 — 환자를 안 골랐으면 null</param>
|
||||
public MDataTableRunner(FormDocument document, IQueryVariableSource? variables)
|
||||
{
|
||||
this.variables = variables ?? EmptySource.Instance;
|
||||
var connection = ConfigService.Current.ConnectionString;
|
||||
workbench = connection.Length > 0 ? new OracleQueryWorkbench(connection) : null;
|
||||
|
||||
foreach (var page in document.Pages)
|
||||
{
|
||||
Collect(page.Controls);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
/// <summary>이름이 붙은 데이터소스들 — 화면이 결과를 목록으로 보여 줄 때 쓴다</summary>
|
||||
public IReadOnlyList<string> Names => queries.Keys.OrderBy(n => n, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
/// <summary>환자 문맥이 붙어 있는가 — 없으면 치환이 전부 빈 값이 되어 결과가 무의미하다</summary>
|
||||
public bool HasContext => !ReferenceEquals(variables, EmptySource.Instance);
|
||||
|
||||
/// <summary>이 데이터소스를 돌린 결과 — 처음 물을 때 한 번만 실행한다</summary>
|
||||
public MDataTableResult Run(string name)
|
||||
{
|
||||
if (cache.TryGetValue(name, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
var result = Execute(name);
|
||||
cache[name] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>모두 돌린다 — 미리보기가 결과를 한꺼번에 보여 줄 때</summary>
|
||||
public IReadOnlyList<MDataTableResult> RunAll() => Names.Select(Run).ToList();
|
||||
|
||||
public TagValue Resolve(DataTableFieldSpec spec)
|
||||
{
|
||||
if (!queries.ContainsKey(spec.TableName))
|
||||
{
|
||||
// 배선이 없는 데이터소스를 가리킨다 — 레거시 런타임은 빈 catch 로 삼켜서
|
||||
// 아무 표시 없이 빈칸이 된다. 여기서는 이름을 말한다.
|
||||
return new TagValue(false, $"데이터소스 '{spec.TableName}' 이 이 서식에 없습니다");
|
||||
}
|
||||
if (spec.Form == DataTableFieldForm.Select && spec.Filter.Length > 0)
|
||||
{
|
||||
// Select 필터는 DataTable.Select 의 메모리 필터 문법이다(SQL 이 아니다).
|
||||
// 아직 옮기지 않았다 — 무시하고 행 번호만 쓰면 <b>다른 행의 값</b>이 조용히 찍힌다.
|
||||
return new TagValue(false, $"Select 필터를 아직 옮기지 않았습니다: {spec.Filter}");
|
||||
}
|
||||
|
||||
var run = Run(spec.TableName);
|
||||
if (!run.Ok)
|
||||
{
|
||||
return new TagValue(false, run.Error ?? "조회에 실패했습니다");
|
||||
}
|
||||
if (spec.RowIndex >= run.Rows.Count)
|
||||
{
|
||||
return new TagValue(false, run.Rows.Count == 0
|
||||
? "조회 결과가 0행입니다"
|
||||
: $"{spec.RowIndex}번 행이 없습니다(결과 {run.Rows.Count}행)");
|
||||
}
|
||||
var column = -1;
|
||||
for (var i = 0; i < run.Columns.Count; i++)
|
||||
{
|
||||
if (string.Equals(run.Columns[i], spec.Field, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
column = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (column < 0)
|
||||
{
|
||||
return new TagValue(false, $"결과에 '{spec.Field}' 컬럼이 없습니다"
|
||||
+ $" (있는 컬럼: {string.Join(", ", run.Columns.Take(8))})");
|
||||
}
|
||||
var cell = run.Rows[spec.RowIndex][column] ?? string.Empty;
|
||||
return cell.Length > 0
|
||||
? new TagValue(true, cell)
|
||||
: new TagValue(false, $"{spec.Field} 값이 비어 있습니다");
|
||||
}
|
||||
|
||||
private MDataTableResult Execute(string name)
|
||||
{
|
||||
var sqlRaw = queries.TryGetValue(name, out var q) ? q : string.Empty;
|
||||
if (sqlRaw.Trim().Length == 0)
|
||||
{
|
||||
return new MDataTableResult(name, string.Empty, Array.Empty<string>(),
|
||||
Array.Empty<string[]>(), "쿼리가 비어 있습니다", Array.Empty<string>());
|
||||
}
|
||||
var converted = QuerySubstitution.Apply(sqlRaw, variables);
|
||||
if (converted.HasLeftover)
|
||||
{
|
||||
// 접두어가 클래스 전체 이름이 아니면 토큰이 SQL 에 남는다 — 실행하면 ORA 구문오류다.
|
||||
// 미리 말해 준다. 오라클 오류 문구보다 이쪽이 고칠 곳을 알려 준다.
|
||||
return new MDataTableResult(name, converted.Sql, Array.Empty<string>(), Array.Empty<string[]>(),
|
||||
"치환되지 않은 변수가 남아 있습니다(접두어가 클래스 전체 이름이어야 합니다): "
|
||||
+ string.Join(", ", converted.Hits
|
||||
.Where(h => h.Kind == QuerySubstitutionKind.NotAVariable)
|
||||
.Select(h => h.Token).Distinct()),
|
||||
converted.UnknownTokens);
|
||||
}
|
||||
if (workbench is null)
|
||||
{
|
||||
return new MDataTableResult(name, converted.Sql, Array.Empty<string>(),
|
||||
Array.Empty<string[]>(), "DB 에 접속되어 있지 않습니다", converted.UnknownTokens);
|
||||
}
|
||||
|
||||
var trial = workbench.Trial(converted.Sql);
|
||||
return new MDataTableResult(name, converted.Sql, trial.Columns, trial.Rows,
|
||||
trial.Ok ? null : trial.Error ?? "조회에 실패했습니다", converted.UnknownTokens);
|
||||
}
|
||||
|
||||
/// <summary>데이터소스를 재귀로 모은다 — 패널·그룹박스 안에 있는 것도 배선 대상이다</summary>
|
||||
private void Collect(IEnumerable<ControlElement> controls)
|
||||
{
|
||||
foreach (var control in controls)
|
||||
{
|
||||
if (control.Type == "MDataTable")
|
||||
{
|
||||
queries[control.Id] = control.Props.GetText("Query") ?? string.Empty;
|
||||
}
|
||||
if (control.Children.Count > 0)
|
||||
{
|
||||
Collect(control.Children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>환자를 안 골랐을 때 — 모든 변수를 "모른다"로 답한다</summary>
|
||||
private sealed class EmptySource : IQueryVariableSource
|
||||
{
|
||||
public static readonly EmptySource Instance = new();
|
||||
|
||||
public string? Scalar(string className, string property) => null;
|
||||
|
||||
public IReadOnlyDictionary<string, string>? Row(string className, string property) => null;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using SheetMe.Core.Catalog;
|
||||
using SheetMe.Data.Stores;
|
||||
|
||||
namespace SheetMe.Designer.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 환자 문맥을 MDataTable 쿼리의 치환 변수로 내준다 — 레거시가 <c>bzPatientInfo</c> 인스턴스를
|
||||
/// 넘기던 자리다.
|
||||
///
|
||||
/// <b>DataRow 접근형이 이 기능의 대부분을 실어 준다.</b>
|
||||
/// <c>PatInfDR.item("아무컬럼")</c> 은 <see cref="PatientContext"/> 의 다섯 사전을 그대로 조회하면
|
||||
/// 되고, 그 사전은 <c>SELECT *</c> 결과라 컬럼을 내가 미리 알 필요가 없다 —
|
||||
/// <b>짐작할 것이 없으니 조용히 틀릴 일도 없다.</b>
|
||||
///
|
||||
/// 스칼라 속성도 대부분 그 다섯 행의 컬럼 하나다. 그래서 표에 컬럼 이름만 적고
|
||||
/// <b>있으면 값, 없으면 모른다</b>고 답한다. 컬럼이 실제로 있는지는 DB 가 판정한다.
|
||||
///
|
||||
/// 산출값(Age·Sex 등 UDF 를 거치는 것)은 넣지 않았다 — 계산식을 짐작하면 값이 조용히 틀린다.
|
||||
/// 넣지 않은 것은 <see cref="QuerySubstitutionKind.Unknown"/> 으로 드러난다.
|
||||
/// </summary>
|
||||
public sealed class PatientQueryVariableSource : IQueryVariableSource
|
||||
{
|
||||
#region Member Fields
|
||||
/// <summary>스칼라 속성 → (문맥 행, 컬럼). 컬럼 존재 여부는 DB 결과가 판정한다</summary>
|
||||
private static readonly Dictionary<string, (string Row, string Column)> ColumnScalars =
|
||||
new(StringComparer.Ordinal)
|
||||
{
|
||||
["ChtNum"] = ("ComInf", "ComChtNum"),
|
||||
["PatTyp"] = ("ComInf", "ComPatTyp"),
|
||||
["ComCvtCom"] = ("ComInf", "ComCvtCom"),
|
||||
["PatMblPhn"] = ("PatInf", "PatMblPhn"),
|
||||
["ComDayCar"] = ("ComInf", "ComDayCar"),
|
||||
};
|
||||
|
||||
private readonly PatientContext context;
|
||||
private readonly string uidCod;
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
public PatientQueryVariableSource(PatientContext context, string uidCod)
|
||||
{
|
||||
this.context = context;
|
||||
this.uidCod = uidCod;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
public string? Scalar(string className, string property)
|
||||
{
|
||||
if (className == QuerySubstitution.WorkClass)
|
||||
{
|
||||
// 작업 정보는 로그인 사용자와 적용일시뿐이다 — 레거시도 이 둘이 실사용의 전부다
|
||||
return property switch
|
||||
{
|
||||
"WrkUid" => uidCod,
|
||||
"AdpDtm" => context.AdpDtm,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
if (className != QuerySubstitution.PatientClass)
|
||||
{
|
||||
// 서식 정보(bzSheetInfo)는 아직 붙이지 않았다 — 모른다고 말한다
|
||||
return null;
|
||||
}
|
||||
// 계산해서 들고 있는 값이 먼저다. 이 둘은 컬럼이 아니라 판정 결과다
|
||||
// (적용일시는 VisitMoment 가 여섯 갈래로 정한다).
|
||||
if (property == "ComNum")
|
||||
{
|
||||
return context.ComNum.ToString("F0");
|
||||
}
|
||||
if (property is "AdpDtm" or "OrderAdpDtm")
|
||||
{
|
||||
return context.AdpDtm;
|
||||
}
|
||||
if (!ColumnScalars.TryGetValue(property, out var map))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var row = RowOf(map.Row);
|
||||
// 행 자체가 없으면(외래의 병실 등) 값이 빈 것이다 — 속성을 모르는 것과 다르다
|
||||
return row.TryGetValue(map.Column, out var value) ? value : row.Count == 0 ? string.Empty : null;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, string>? Row(string className, string property)
|
||||
{
|
||||
if (className != QuerySubstitution.PatientClass)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return property switch
|
||||
{
|
||||
"PatInfDR" => context.PatInf,
|
||||
"ComInfDR" => context.ComInf,
|
||||
"CodInfDR" => context.CodInf,
|
||||
"CoiInfDR" => context.CoiInf,
|
||||
"CowInfDR" => context.CowInf,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private IReadOnlyDictionary<string, string> RowOf(string name) => name switch
|
||||
{
|
||||
"PatInf" => context.PatInf,
|
||||
"ComInf" => context.ComInf,
|
||||
"CodInf" => context.CodInf,
|
||||
"CoiInf" => context.CoiInf,
|
||||
_ => context.CowInf,
|
||||
};
|
||||
#endregion
|
||||
}
|
||||
@@ -59,7 +59,8 @@ public static class PrintService
|
||||
/// 인쇄는 언제나 true 다 — 인쇄는 인쇄다.
|
||||
/// </summary>
|
||||
public static UIElement BuildPageVisual(PageViewModel page, bool printFilter = true,
|
||||
SheetMe.Core.Catalog.ITagValueResolver? tags = null)
|
||||
SheetMe.Core.Catalog.ITagValueResolver? tags = null,
|
||||
SheetMe.Core.Catalog.IDataFieldResolver? fields = null)
|
||||
{
|
||||
var canvas = new Canvas
|
||||
{
|
||||
@@ -91,7 +92,7 @@ public static class PrintService
|
||||
}
|
||||
var presenter = new ContentPresenter
|
||||
{
|
||||
Content = PrintProjection(control, printFilter, tags),
|
||||
Content = PrintProjection(control, printFilter, tags, fields),
|
||||
Width = Math.Max(1, control.Width),
|
||||
Height = Math.Max(1, control.Height),
|
||||
};
|
||||
@@ -121,11 +122,12 @@ public static class PrintService
|
||||
/// 캔버스가 쓰는 VM 은 <b>한 글자도 건드리지 않는다.</b>
|
||||
/// </summary>
|
||||
private static object PrintProjection(ControlViewModel control, bool printFilter,
|
||||
SheetMe.Core.Catalog.ITagValueResolver? tags)
|
||||
SheetMe.Core.Catalog.ITagValueResolver? tags,
|
||||
SheetMe.Core.Catalog.IDataFieldResolver? fields)
|
||||
{
|
||||
if (control is not ContainerViewModel container)
|
||||
{
|
||||
return WithTagValue(control, tags);
|
||||
return WithTagValue(control, tags, fields);
|
||||
}
|
||||
ContainerViewModel? copy = container switch
|
||||
{
|
||||
@@ -145,7 +147,7 @@ public static class PrintService
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (PrintProjection(child, printFilter, tags) is ControlViewModel projected)
|
||||
if (PrintProjection(child, printFilter, tags, fields) is ControlViewModel projected)
|
||||
{
|
||||
copy.Children.Add(projected);
|
||||
}
|
||||
@@ -169,24 +171,51 @@ public static class PrintService
|
||||
/// 빈칸으로 두면 서식이 잘못된 줄 알고, 태그 이름을 그대로 두면 값이 나온 줄 안다.
|
||||
/// 레거시는 실패한 컨트롤을 노란색으로 칠했다(같은 문제를 같은 이유로 다뤘다).
|
||||
/// </summary>
|
||||
private static object WithTagValue(ControlViewModel control, SheetMe.Core.Catalog.ITagValueResolver? tags)
|
||||
private static object WithTagValue(ControlViewModel control, SheetMe.Core.Catalog.ITagValueResolver? tags,
|
||||
SheetMe.Core.Catalog.IDataFieldResolver? fields)
|
||||
{
|
||||
if (tags is null)
|
||||
// 두 관을 순서대로 본다. 태그가 먼저인 이유는 그것이 값을 만드는 더 단순한 경로이고,
|
||||
// 한 컨트롤에 둘이 다 걸려 있으면 어느 쪽이 이기는지 <b>정해져 있어야</b> 하기 때문이다
|
||||
// (정하지 않으면 같은 서식이 실행마다 다르게 보인다).
|
||||
var (label, value) = Bound(control, tags, fields);
|
||||
if (label is null)
|
||||
{
|
||||
return control;
|
||||
}
|
||||
var tag = control.Model.Props.GetText("DataInterfaceTag");
|
||||
if (tag is not { Length: > 0 } || tag == "None")
|
||||
{
|
||||
return control;
|
||||
}
|
||||
var value = tags.Resolve(tag);
|
||||
var text = value.Resolved ? value.Text
|
||||
: value.Text.Length > 0 ? $"[{tag} — {value.Text}]" : $"[{tag}]";
|
||||
: value.Text.Length > 0 ? $"[{label} — {value.Text}]" : $"[{label}]";
|
||||
|
||||
var model = control.Model.Clone();
|
||||
model.Props.SetText("Text", text);
|
||||
return DocumentMapper.CreateControl(model, control.EffectiveFont, control.Foreground);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이 컨트롤에 걸린 배선과 그 값 — 배선이 없으면 라벨이 null 이다.
|
||||
///
|
||||
/// <c>DataTableField</c> 는 형식을 어기면 레거시 런타임이 <b>빈 catch 로 삼켜</b>
|
||||
/// 아무 표시 없이 빈칸이 된다(ucLoadSheetBase.DataTableBinding_). 여기서는 파싱 실패도 말한다 —
|
||||
/// 디자이너에서 정상으로 보이고 임상 화면에서만 빈칸이 되는 것이 이 배선의 고질적 고장이다.
|
||||
/// </summary>
|
||||
private static (string? Label, SheetMe.Core.Catalog.TagValue Value) Bound(
|
||||
ControlViewModel control,
|
||||
SheetMe.Core.Catalog.ITagValueResolver? tags,
|
||||
SheetMe.Core.Catalog.IDataFieldResolver? fields)
|
||||
{
|
||||
var tag = control.Model.Props.GetText("DataInterfaceTag");
|
||||
if (tags is not null && tag is { Length: > 0 } && tag != "None")
|
||||
{
|
||||
return (tag, tags.Resolve(tag));
|
||||
}
|
||||
var wiring = control.Model.Props.GetText("DataTableField");
|
||||
if (fields is null || wiring is not { Length: > 0 })
|
||||
{
|
||||
return (null, SheetMe.Core.Catalog.TagValue.None);
|
||||
}
|
||||
var spec = SheetMe.Core.Catalog.DataTableFieldSpec.Parse(wiring);
|
||||
return spec is null
|
||||
? (wiring, new SheetMe.Core.Catalog.TagValue(false, "배선 형식을 읽을 수 없습니다"))
|
||||
: ($"{spec.TableName}.{spec.Field}", fields.Resolve(spec));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user