@moon791017/neo-skills 1.0.30 → 1.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# .NET Tag Helper Coding Style & Advanced Patterns
|
|
2
2
|
|
|
3
|
-
This guide defines advanced coding styles and implementation patterns for ASP.NET Core Tag Helpers, specifically targeting complex UI components, extending built-in behaviors, and frontend asset management mechanisms.
|
|
3
|
+
This guide defines advanced coding styles and implementation patterns for ASP.NET Core Tag Helpers, specifically targeting complex UI components, extending built-in behaviors, and frontend asset management mechanisms. **When developing Tag Helpers, developers MUST refer to the implementation patterns in the Example section (Section 3) to ensure adherence to these standards.**
|
|
4
4
|
|
|
5
5
|
## 1. Inheriting and Extending Built-in Tag Helpers
|
|
6
6
|
|
|
@@ -11,7 +11,101 @@ When you need to customize standard form elements (e.g., `<select>`), you should
|
|
|
11
11
|
- You must explicitly set `output.TagName`; otherwise, the browser will output tags with custom names (e.g., `<select-ai-agent>`).
|
|
12
12
|
- Call `await base.ProcessAsync(context, output)` to allow the base class to handle native attribute binding.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
## 2. Mandatory Custom Tag Helper Properties & Asset Management
|
|
15
|
+
|
|
16
|
+
To ensure consistency, performance, and maintainability across all custom UI components, every custom Tag Helper MUST implement the following properties and logic.
|
|
17
|
+
|
|
18
|
+
### 2.1 Mandatory Properties
|
|
19
|
+
|
|
20
|
+
1. **`asp-for` (Model Binding)**:
|
|
21
|
+
Support strong typing by including a `ModelExpression` property. This is crucial for retrieving model metadata (Name, Id, Value, Validation) and generating safe HTML identifiers.
|
|
22
|
+
```csharp
|
|
23
|
+
[HtmlAttributeName("asp-for")]
|
|
24
|
+
public ModelExpression For { get; set; } = null!;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
2. **CSS Class Handling (UI Class)**:
|
|
28
|
+
Tag Helpers must provide a default CSS class. If the user provides a custom class in the HTML tag:
|
|
29
|
+
- If the user class is identical to the default, maintain the default.
|
|
30
|
+
- If the user class is different, **append** it to the default class (ensure a space separator).
|
|
31
|
+
|
|
32
|
+
```csharp
|
|
33
|
+
[HtmlAttributeName("class")]
|
|
34
|
+
public string? CssClass { get; set; }
|
|
35
|
+
|
|
36
|
+
protected void ProcessCssClass(TagHelperOutput output, string defaultClass)
|
|
37
|
+
{
|
|
38
|
+
if (string.IsNullOrEmpty(CssClass))
|
|
39
|
+
{
|
|
40
|
+
output.Attributes.SetAttribute("class", defaultClass);
|
|
41
|
+
}
|
|
42
|
+
else if (CssClass.Trim() == defaultClass)
|
|
43
|
+
{
|
|
44
|
+
output.Attributes.SetAttribute("class", defaultClass);
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
{
|
|
48
|
+
output.Attributes.SetAttribute("class", $"{defaultClass} {CssClass.Trim()}");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
3. **`AutoLoadAssets` (Asset Management)**:
|
|
54
|
+
Include a boolean property to control automatic resource loading, defaulting to `true`.
|
|
55
|
+
```csharp
|
|
56
|
+
/// <summary>
|
|
57
|
+
/// Whether to automatically load corresponding JS and CSS. Default is true.
|
|
58
|
+
/// </summary>
|
|
59
|
+
[HtmlAttributeName("auto-load-assets")]
|
|
60
|
+
public bool AutoLoadAssets { get; set; } = true;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 2.2 Inheritance and Composition Standards
|
|
64
|
+
|
|
65
|
+
1. **Independent Tag Helpers**: Any standalone custom Tag Helper representing a single UI control (e.g., a specialized dropdown) MUST inherit from the corresponding built-in ASP.NET Core Tag Helper (e.g., `SelectTagHelper`, `InputTagHelper`, `RadioTagHelper`). This leverages framework-standard model binding and validation logic.
|
|
66
|
+
|
|
67
|
+
2. **Composite Tag Helpers**: For components aggregating multiple elements (e.g., a search form):
|
|
68
|
+
- **Internal Elements**: MUST be implemented using built-in C# UI Tag Helpers to ensure they benefit from standard ASP.NET Core features.
|
|
69
|
+
- **Outer Container (`div-class`)**: MUST allow users to pass a custom CSS class via a `div-class` attribute.
|
|
70
|
+
- **CSS Consistency**: The handling of `div-class` MUST strictly follow the rules defined in **Section 2.1.2 (CSS Class Handling)**, ensuring the default container class is preserved or appended to, rather than simply overwritten.
|
|
71
|
+
|
|
72
|
+
### 2.3 Mandatory Asset Loading Pattern
|
|
73
|
+
|
|
74
|
+
When `AutoLoadAssets` is `true`, the Tag Helper must register its resources using the following standard pattern:
|
|
75
|
+
|
|
76
|
+
- **Version Management**: Always use `IFileVersionProvider.AddFileVersionToPath` to ensure browser cache busting.
|
|
77
|
+
- **De-duplication & Injection**: Use `HttpContext.AddStyle` and `HttpContext.AddScript` (see Section 3 for implementation) to ensure assets are only rendered once per page.
|
|
78
|
+
|
|
79
|
+
```csharp
|
|
80
|
+
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
|
81
|
+
{
|
|
82
|
+
// ... UI Generation Logic ...
|
|
83
|
+
|
|
84
|
+
if (AutoLoadAssets)
|
|
85
|
+
{
|
|
86
|
+
var httpContext = ViewContext.HttpContext;
|
|
87
|
+
var requestPathBase = httpContext.Request.PathBase;
|
|
88
|
+
|
|
89
|
+
// 1. Resolve Path with Version
|
|
90
|
+
string cssPath = "/css/components/my-component.css";
|
|
91
|
+
string versionedCss = _fileVersionProvider.AddFileVersionToPath(requestPathBase, cssPath);
|
|
92
|
+
|
|
93
|
+
string jsPath = "/js/components/my-component.js";
|
|
94
|
+
string versionedJs = _fileVersionProvider.AddFileVersionToPath(requestPathBase, jsPath);
|
|
95
|
+
|
|
96
|
+
// 2. Register via Context Extensions (renders in designated Layout blocks)
|
|
97
|
+
httpContext.AddStyle($"<link rel=\"stylesheet\" href=\"{versionedCss}\" />", "MyComponentKey");
|
|
98
|
+
httpContext.AddScript($"<script src=\"{versionedJs}\"></script>", "MyComponentKey");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
## 3 Example
|
|
106
|
+
|
|
107
|
+
### Extending a Dropdown
|
|
108
|
+
|
|
15
109
|
```csharp
|
|
16
110
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
|
17
111
|
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
|
@@ -176,19 +270,22 @@ namespace [YourNamespace].TagHelpers
|
|
|
176
270
|
}
|
|
177
271
|
}
|
|
178
272
|
}
|
|
273
|
+
```
|
|
179
274
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
## 2. Composite Complex UI Components and Model Binding
|
|
275
|
+
### Composite Complex UI Components and Model Binding
|
|
183
276
|
|
|
184
277
|
For complex UIs containing multiple elements (like a complete search form), you should use `TagBuilder` to compose the DOM structure. Using `ModelExpression` allows you to accurately bind Tag Helper attributes to ViewModel properties, retrieve the exact field name (`.Name`) for the frontend, and integrate backend permission validation logic.
|
|
185
278
|
|
|
186
|
-
|
|
187
|
-
|
|
279
|
+
``` csharp
|
|
280
|
+
|
|
188
281
|
/// <summary>
|
|
189
282
|
/// Composite AI Report Search TagHelper.
|
|
190
283
|
/// A UI component integrating date range, business unit, submission unit, inspection report menu, and search button.
|
|
191
284
|
/// </summary>
|
|
285
|
+
/// <summary>
|
|
286
|
+
/// 組合式 AI 報告搜尋 TagHelper
|
|
287
|
+
/// 整合了日期範圍、檢驗單位、送檢單位、檢驗報告選單與查詢按鈕的 UI 組件
|
|
288
|
+
/// </summary>
|
|
192
289
|
[HtmlTargetElement("combo-ai-report-search")]
|
|
193
290
|
public class ComboAIReportSearchTagHelper : TagHelper
|
|
194
291
|
{
|
|
@@ -210,66 +307,72 @@ For complex UIs containing multiple elements (like a complete search form), you
|
|
|
210
307
|
}
|
|
211
308
|
|
|
212
309
|
/// <summary>
|
|
213
|
-
///
|
|
214
|
-
///
|
|
310
|
+
/// 是否自動載入對應的 JS 與 CSS,預設為 true
|
|
311
|
+
/// 若設定為 true,TagHelper 會自動在組件後方插入相關資源標籤,並確保單次請求僅載入一次
|
|
215
312
|
/// </summary>
|
|
216
313
|
[HtmlAttributeName("auto-load-assets")]
|
|
217
314
|
public bool AutoLoadAssets { get; set; } = true;
|
|
218
315
|
|
|
219
|
-
#region Model Binding Attributes (
|
|
316
|
+
#region Model Binding Attributes (支援自定義 ID 與 Name)
|
|
220
317
|
/// <summary>
|
|
221
|
-
///
|
|
318
|
+
/// 繫結開始日期的模型表達式
|
|
222
319
|
/// </summary>
|
|
223
320
|
[HtmlAttributeName("asp-for-sdate")]
|
|
224
321
|
public ModelExpression ForSDate { get; set; }
|
|
225
322
|
|
|
226
323
|
/// <summary>
|
|
227
|
-
///
|
|
324
|
+
/// 繫結結束日期的模型表達式
|
|
228
325
|
/// </summary>
|
|
229
326
|
[HtmlAttributeName("asp-for-edate")]
|
|
230
327
|
public ModelExpression ForEDate { get; set; }
|
|
231
328
|
|
|
232
329
|
/// <summary>
|
|
233
|
-
///
|
|
330
|
+
/// 繫結檢驗單位的模型表達式
|
|
234
331
|
/// </summary>
|
|
235
332
|
[HtmlAttributeName("asp-for-bu")]
|
|
236
333
|
public ModelExpression ForBusinessUnit { get; set; }
|
|
237
334
|
|
|
238
335
|
/// <summary>
|
|
239
|
-
///
|
|
336
|
+
/// 繫結送檢單位的模型表達式
|
|
240
337
|
/// </summary>
|
|
241
338
|
[HtmlAttributeName("asp-for-hisno")]
|
|
242
339
|
public ModelExpression ForHISNo { get; set; }
|
|
243
340
|
|
|
244
341
|
/// <summary>
|
|
245
|
-
///
|
|
342
|
+
/// 繫結檢驗報告的模型表達式
|
|
246
343
|
/// </summary>
|
|
247
344
|
[HtmlAttributeName("asp-for-report")]
|
|
248
345
|
public ModelExpression ForAIReport { get; set; }
|
|
249
346
|
#endregion
|
|
250
347
|
|
|
251
|
-
#region Class Attributes (
|
|
348
|
+
#region Class Attributes (支援 CSS 客製化)
|
|
252
349
|
[HtmlAttributeName("sdate-class")]
|
|
253
|
-
public string SDateClass { get; set; } = "form-control
|
|
350
|
+
public string SDateClass { get; set; } = "sdate-li77"; // form-control 要預設有
|
|
254
351
|
|
|
255
352
|
[HtmlAttributeName("edate-class")]
|
|
256
|
-
public string EDateClass { get; set; } = "form-control
|
|
353
|
+
public string EDateClass { get; set; } = "edate-li77"; // form-control 要預設有
|
|
257
354
|
|
|
258
355
|
[HtmlAttributeName("bu-class")]
|
|
259
|
-
public string BusinessUnitClass { get; set; } = "form-control
|
|
356
|
+
public string BusinessUnitClass { get; set; } = "bu-li77"; // form-control 要預設有
|
|
260
357
|
|
|
261
358
|
[HtmlAttributeName("hisno-class")]
|
|
262
|
-
public string HISNoClass { get; set; } = "form-control
|
|
359
|
+
public string HISNoClass { get; set; } = "hisno-li77"; // form-control 要預設有
|
|
263
360
|
|
|
264
361
|
[HtmlAttributeName("report-class")]
|
|
265
|
-
public string AIReportClass { get; set; } = "form-control
|
|
362
|
+
public string AIReportClass { get; set; } = "ai-report-li77"; // form-control 要預設有
|
|
266
363
|
|
|
267
364
|
[HtmlAttributeName("btn-class")]
|
|
268
|
-
public string BtnClass { get; set; } = "btn btn-primary
|
|
365
|
+
public string BtnClass { get; set; } = "ai-btn-search-li77"; // btn btn-primary 要預設有
|
|
366
|
+
|
|
367
|
+
/// <summary>
|
|
368
|
+
/// 外層容器的 CSS Class。預設為 ai-report-search-container-li77
|
|
369
|
+
/// </summary>
|
|
370
|
+
[HtmlAttributeName("div-class")]
|
|
371
|
+
public string ContainerClass { get; set; } = "ai-report-search-container-li77";
|
|
269
372
|
#endregion
|
|
270
373
|
|
|
271
374
|
/// <summary>
|
|
272
|
-
///
|
|
375
|
+
/// 取得目前的 ViewContext 以存取 HttpContext 等資訊
|
|
273
376
|
/// </summary>
|
|
274
377
|
[HtmlAttributeNotBound]
|
|
275
378
|
[ViewContext]
|
|
@@ -281,89 +384,94 @@ For complex UIs containing multiple elements (like a complete search form), you
|
|
|
281
384
|
var currentDT = _commonService.GetCurrentDateTime();
|
|
282
385
|
var isSuperAdmin = currentUser.IsSuperAdminRole;
|
|
283
386
|
|
|
284
|
-
//
|
|
387
|
+
// 設定外層容器
|
|
285
388
|
output.TagName = "div";
|
|
286
389
|
output.TagMode = TagMode.StartTagAndEndTag;
|
|
287
|
-
output.Attributes.SetAttribute("class", "ai-report-search-container");
|
|
288
390
|
|
|
289
|
-
//
|
|
290
|
-
string
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
391
|
+
// 處理自訂容器 Class,如果使用者自訂了,則把自訂的加在前面,並且保留預設的以便 JS 定位
|
|
392
|
+
string finalContainerClass = ContainerClass == "ai-report-search-container-li77"
|
|
393
|
+
? "ai-report-search-container-li77"
|
|
394
|
+
: $"{ContainerClass} ai-report-search-container-li77";
|
|
395
|
+
|
|
396
|
+
output.Attributes.SetAttribute("class", finalContainerClass);
|
|
397
|
+
|
|
398
|
+
// 產生日選單與按鈕的 ID (一律使用 TagBuilder.CreateSanitizedId 產生)
|
|
399
|
+
string sDateId = TagBuilder.CreateSanitizedId(ForSDate?.Name ?? "Search_SDate", "_");
|
|
400
|
+
string eDateId = TagBuilder.CreateSanitizedId(ForEDate?.Name ?? "Search_EDate", "_");
|
|
401
|
+
string buId = TagBuilder.CreateSanitizedId(ForBusinessUnit?.Name ?? "Search_BusinessUnit", "_");
|
|
402
|
+
string hisNoId = TagBuilder.CreateSanitizedId(ForHISNo?.Name ?? "Search_HISNo", "_");
|
|
403
|
+
string reportId = TagBuilder.CreateSanitizedId(ForAIReport?.Name ?? "Search_AIReport", "_");
|
|
404
|
+
|
|
405
|
+
// 處理內層元素的 CSS Class,確保基底 class (form-control / btn btn-primary) 加上自定義或預設的 class
|
|
406
|
+
string finalSDateClass = SDateClass == "sdate-li77" ? "form-control sdate-li77" : $"form-control {SDateClass} sdate-li77";
|
|
407
|
+
string finalEDateClass = EDateClass == "edate-li77" ? "form-control edate-li77" : $"form-control {EDateClass} edate-li77";
|
|
408
|
+
string finalBUClass = BusinessUnitClass == "bu-li77" ? "form-control bu-li77" : $"form-control {BusinessUnitClass} bu-li77";
|
|
409
|
+
string finalHISNoClass = HISNoClass == "hisno-li77" ? "form-control hisno-li77" : $"form-control {HISNoClass} hisno-li77";
|
|
410
|
+
string finalReportClass = AIReportClass == "ai-report-li77" ? "form-control ai-report-li77" : $"form-control {AIReportClass} ai-report-li77";
|
|
411
|
+
string finalBtnClass = BtnClass == "ai-btn-search-li77" ? "btn btn-primary ai-btn-search-li77" : $"btn btn-primary {BtnClass} ai-btn-search-li77";
|
|
296
412
|
|
|
297
413
|
var inputGroup = new TagBuilder("div");
|
|
298
414
|
inputGroup.AddCssClass("input-group");
|
|
299
415
|
|
|
300
|
-
// 1.
|
|
301
|
-
inputGroup.InnerHtml.AppendHtml(CreateSpan("
|
|
302
|
-
inputGroup.InnerHtml.AppendHtml(
|
|
416
|
+
// 1. 開始日期
|
|
417
|
+
inputGroup.InnerHtml.AppendHtml(CreateSpan("日期"));
|
|
418
|
+
inputGroup.InnerHtml.AppendHtml(await CreateUIAsync(ForSDate, "date", finalSDateClass, DateTime.Now.ToString("yyyy-MM-dd"), "開始日期", sDateId));
|
|
303
419
|
|
|
304
|
-
// 2.
|
|
305
|
-
inputGroup.InnerHtml.AppendHtml(CreateSpan("
|
|
306
|
-
inputGroup.InnerHtml.AppendHtml(
|
|
420
|
+
// 2. 結束日期
|
|
421
|
+
inputGroup.InnerHtml.AppendHtml(CreateSpan("至"));
|
|
422
|
+
inputGroup.InnerHtml.AppendHtml(await CreateUIAsync(ForEDate, "date", finalEDateClass, DateTime.Now.ToString("yyyy-MM-dd"), "結束日期", eDateId));
|
|
307
423
|
|
|
308
|
-
// 3.
|
|
424
|
+
// 3. 檢驗單位 (僅超級管理員可見)
|
|
309
425
|
if (isSuperAdmin)
|
|
310
426
|
{
|
|
311
|
-
inputGroup.InnerHtml.AppendHtml(CreateSpan("
|
|
312
|
-
inputGroup.InnerHtml.AppendHtml(
|
|
427
|
+
inputGroup.InnerHtml.AppendHtml(CreateSpan("檢驗單位"));
|
|
428
|
+
inputGroup.InnerHtml.AppendHtml(await CreateUIAsync(ForBusinessUnit, null, finalBUClass, null, null, buId));
|
|
313
429
|
}
|
|
314
430
|
|
|
315
|
-
// 4.
|
|
316
|
-
inputGroup.InnerHtml.AppendHtml(CreateSpan("
|
|
317
|
-
|
|
431
|
+
// 4. 送檢單位 (依權限自動過濾或初始化)
|
|
432
|
+
inputGroup.InnerHtml.AppendHtml(CreateSpan("送檢單位"));
|
|
433
|
+
|
|
434
|
+
List<SelectListItem> hisNoOptions = new List<SelectListItem>();
|
|
318
435
|
if (!isSuperAdmin)
|
|
319
436
|
{
|
|
320
|
-
//
|
|
437
|
+
// 非超級管理員:預先從資料庫抓取授權的組織清單
|
|
438
|
+
hisNoOptions.Add(new SelectListItem { Text = "", Value = "" });
|
|
321
439
|
var options = await GetAllowListOptionsAsync(currentUser.UserID, currentDT);
|
|
322
|
-
|
|
323
|
-
foreach (var opt in options)
|
|
324
|
-
{
|
|
325
|
-
var optionTag = new TagBuilder("option");
|
|
326
|
-
optionTag.Attributes.Add("value", opt.Value);
|
|
327
|
-
optionTag.InnerHtml.Append(opt.Text);
|
|
328
|
-
hisNoSelect.InnerHtml.AppendHtml(optionTag);
|
|
329
|
-
}
|
|
440
|
+
hisNoOptions.AddRange(options);
|
|
330
441
|
}
|
|
331
|
-
inputGroup.InnerHtml.AppendHtml(
|
|
442
|
+
inputGroup.InnerHtml.AppendHtml(await CreateUIAsync(ForHISNo, null, finalHISNoClass, null, null, hisNoId, hisNoOptions));
|
|
332
443
|
|
|
333
|
-
// 5.
|
|
334
|
-
inputGroup.InnerHtml.AppendHtml(CreateSpan("
|
|
335
|
-
var
|
|
336
|
-
|
|
337
|
-
inputGroup.InnerHtml.AppendHtml(reportSelect);
|
|
444
|
+
// 5. 檢驗報告 (預設為空,由 JS 透過 API 非同步填充)
|
|
445
|
+
inputGroup.InnerHtml.AppendHtml(CreateSpan("檢驗報告"));
|
|
446
|
+
var reportOptions = new List<SelectListItem> { new SelectListItem { Text = "", Value = "" } };
|
|
447
|
+
inputGroup.InnerHtml.AppendHtml(await CreateUIAsync(ForAIReport, null, finalReportClass, null, null, reportId, reportOptions));
|
|
338
448
|
|
|
339
|
-
// 6.
|
|
449
|
+
// 6. 查詢按鈕
|
|
340
450
|
var btn = new TagBuilder("button");
|
|
341
451
|
btn.Attributes.Add("type", "button");
|
|
342
|
-
btn.
|
|
343
|
-
btn.
|
|
344
|
-
btn.InnerHtml.AppendHtml("<i class='fa fa-search'></i> Search");
|
|
452
|
+
btn.AddCssClass(finalBtnClass);
|
|
453
|
+
btn.InnerHtml.AppendHtml("<i class='fa fa-search'></i> 查詢");
|
|
345
454
|
inputGroup.InnerHtml.AppendHtml(btn);
|
|
346
455
|
|
|
347
456
|
output.Content.SetHtmlContent(inputGroup);
|
|
348
457
|
|
|
349
|
-
//
|
|
458
|
+
// 自動載入資產
|
|
350
459
|
if (AutoLoadAssets)
|
|
351
460
|
{
|
|
352
461
|
var httpContext = ViewContext.HttpContext;
|
|
353
462
|
|
|
354
|
-
|
|
355
|
-
string cssPath = "[your-css-path]";
|
|
463
|
+
string cssPath = "/css/AIAnalysis/ComboAIReportSearch.css";
|
|
356
464
|
string vCssPath = _fileVersionProvider.AddFileVersionToPath(httpContext.Request.PathBase, cssPath);
|
|
357
465
|
httpContext.AddStyle($"<link rel=\"stylesheet\" href=\"{vCssPath}\" />", "ComboAIReportSearch");
|
|
358
466
|
|
|
359
|
-
|
|
360
|
-
string jsPath = "[your-js-path]";
|
|
467
|
+
string jsPath = "/js/AIAnalysis/ComboAIReportSearch.js";
|
|
361
468
|
string vJsPath = _fileVersionProvider.AddFileVersionToPath(httpContext.Request.PathBase, jsPath);
|
|
362
469
|
httpContext.AddScript($"<script src=\"{vJsPath}\"></script>", "ComboAIReportSearch");
|
|
363
470
|
}
|
|
364
471
|
}
|
|
365
472
|
|
|
366
|
-
#region Helper Methods
|
|
473
|
+
#region Helper Methods
|
|
474
|
+
|
|
367
475
|
private TagBuilder CreateSpan(string text)
|
|
368
476
|
{
|
|
369
477
|
var span = new TagBuilder("span");
|
|
@@ -372,30 +480,105 @@ For complex UIs containing multiple elements (like a complete search form), you
|
|
|
372
480
|
return span;
|
|
373
481
|
}
|
|
374
482
|
|
|
375
|
-
|
|
483
|
+
/// <summary>
|
|
484
|
+
/// 通用 UI 產生方法,根據是否有 ModelExpression 選擇產生方式
|
|
485
|
+
/// </summary>
|
|
486
|
+
private async Task<IHtmlContent> CreateUIAsync(ModelExpression @for, string type, string className, string value, string title, string sanitizedId, IEnumerable<SelectListItem> items = null)
|
|
487
|
+
{
|
|
488
|
+
// 如果有 ModelExpression,使用原生 TagHelper 物件
|
|
489
|
+
if (@for != null)
|
|
490
|
+
{
|
|
491
|
+
if (string.IsNullOrEmpty(type)) // Select
|
|
492
|
+
{
|
|
493
|
+
return await CreateSelectTagHelperAsync(@for, className, sanitizedId, items);
|
|
494
|
+
}
|
|
495
|
+
else // Input
|
|
496
|
+
{
|
|
497
|
+
return await CreateInputTagHelperAsync(@for, type, className, value, title, sanitizedId);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 如果沒有 ModelExpression,使用 IHtmlGenerator 直接產生 (模擬 TagHelper 行為)
|
|
502
|
+
if (string.IsNullOrEmpty(type)) // Select
|
|
503
|
+
{
|
|
504
|
+
var select = _generator.GenerateSelect(
|
|
505
|
+
ViewContext,
|
|
506
|
+
null,
|
|
507
|
+
sanitizedId,
|
|
508
|
+
sanitizedId,
|
|
509
|
+
items ?? new List<SelectListItem>(),
|
|
510
|
+
false,
|
|
511
|
+
new { @class = className, id = sanitizedId });
|
|
512
|
+
return select;
|
|
513
|
+
}
|
|
514
|
+
else // Input
|
|
515
|
+
{
|
|
516
|
+
var input = _generator.GenerateTextBox(
|
|
517
|
+
ViewContext,
|
|
518
|
+
null,
|
|
519
|
+
sanitizedId,
|
|
520
|
+
value,
|
|
521
|
+
null,
|
|
522
|
+
new { @class = className, id = sanitizedId, type = type, title = title });
|
|
523
|
+
return input;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/// <summary>
|
|
528
|
+
/// 使用 InputTagHelper 產生輸入框
|
|
529
|
+
/// </summary>
|
|
530
|
+
private async Task<IHtmlContent> CreateInputTagHelperAsync(ModelExpression @for, string type, string className, string value, string title, string sanitizedId)
|
|
376
531
|
{
|
|
377
|
-
var
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
532
|
+
var inputTagHelper = new InputTagHelper(_generator)
|
|
533
|
+
{
|
|
534
|
+
For = @for,
|
|
535
|
+
InputTypeName = type,
|
|
536
|
+
ViewContext = this.ViewContext
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
var output = new TagHelperOutput(
|
|
540
|
+
"input",
|
|
541
|
+
new TagHelperAttributeList(),
|
|
542
|
+
(useCachedResult, encoder) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent())
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
output.Attributes.SetAttribute("id", sanitizedId);
|
|
546
|
+
if (!string.IsNullOrEmpty(className)) output.Attributes.SetAttribute("class", className);
|
|
547
|
+
if (!string.IsNullOrEmpty(value)) output.Attributes.SetAttribute("value", value);
|
|
548
|
+
if (!string.IsNullOrEmpty(title)) output.Attributes.SetAttribute("title", title);
|
|
549
|
+
|
|
550
|
+
await inputTagHelper.ProcessAsync(new TagHelperContext(new TagHelperAttributeList(), new Dictionary<object, object>(), Guid.NewGuid().ToString()), output);
|
|
551
|
+
return output;
|
|
385
552
|
}
|
|
386
553
|
|
|
387
|
-
|
|
554
|
+
/// <summary>
|
|
555
|
+
/// 使用 SelectTagHelper 產生下拉選單
|
|
556
|
+
/// </summary>
|
|
557
|
+
private async Task<IHtmlContent> CreateSelectTagHelperAsync(ModelExpression @for, string className, string sanitizedId, IEnumerable<SelectListItem> items = null)
|
|
388
558
|
{
|
|
389
|
-
var
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
559
|
+
var selectTagHelper = new SelectTagHelper(_generator)
|
|
560
|
+
{
|
|
561
|
+
For = @for,
|
|
562
|
+
Items = items ?? new List<SelectListItem>(),
|
|
563
|
+
ViewContext = this.ViewContext
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
var output = new TagHelperOutput(
|
|
567
|
+
"select",
|
|
568
|
+
new TagHelperAttributeList(),
|
|
569
|
+
(useCachedResult, encoder) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent())
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
output.Attributes.SetAttribute("id", sanitizedId);
|
|
573
|
+
if (!string.IsNullOrEmpty(className)) output.Attributes.SetAttribute("class", className);
|
|
574
|
+
|
|
575
|
+
await selectTagHelper.ProcessAsync(new TagHelperContext(new TagHelperAttributeList(), new Dictionary<object, object>(), Guid.NewGuid().ToString()), output);
|
|
576
|
+
return output;
|
|
394
577
|
}
|
|
395
578
|
#endregion
|
|
396
579
|
|
|
397
580
|
/// <summary>
|
|
398
|
-
///
|
|
581
|
+
/// 獲取當前使用者被授權的所有送檢機構列表
|
|
399
582
|
/// </summary>
|
|
400
583
|
private async Task<List<SelectListItem>> GetAllowListOptionsAsync(string userId, DateTime currentDT)
|
|
401
584
|
{
|
|
@@ -403,18 +586,18 @@ For complex UIs containing multiple elements (like a complete search form), you
|
|
|
403
586
|
return authOrgs.Select(x => new SelectListItem
|
|
404
587
|
{
|
|
405
588
|
Value = x.HisNo,
|
|
406
|
-
Text = $"
|
|
589
|
+
Text = $"【{x.HisNo}】 {x.OrgName}"
|
|
407
590
|
}).ToList();
|
|
408
591
|
}
|
|
409
592
|
}
|
|
410
593
|
|
|
411
|
-
|
|
594
|
+
```
|
|
595
|
+
|
|
412
596
|
|
|
413
|
-
|
|
597
|
+
### Automatic Frontend Asset Loading and Deduplication (Asset Management)
|
|
414
598
|
|
|
415
599
|
When a Tag Helper depends on specific JavaScript or CSS, developers must ensure that these resources are not loaded multiple times if the tag is used repeatedly on the same page. It is recommended to implement an asset collector pattern by extending `HttpContext.Items`.
|
|
416
600
|
|
|
417
|
-
### Example: Asset Deduplication Extension Methods
|
|
418
601
|
|
|
419
602
|
```csharp
|
|
420
603
|
public static partial class HtmlExtension
|
|
@@ -492,83 +675,4 @@ public static partial class HtmlExtension
|
|
|
492
675
|
|
|
493
676
|
---
|
|
494
677
|
|
|
495
|
-
## 4. Mandatory Custom Tag Helper Properties & Asset Management
|
|
496
|
-
|
|
497
|
-
To ensure consistency, performance, and maintainability across all custom UI components, every custom Tag Helper MUST implement the following properties and logic.
|
|
498
|
-
|
|
499
|
-
### 4.1 Mandatory Properties
|
|
500
|
-
|
|
501
|
-
1. **`asp-for` (Model Binding)**:
|
|
502
|
-
Support strong typing by including a `ModelExpression` property. This is crucial for retrieving model metadata (Name, Id, Value, Validation) and generating safe HTML identifiers.
|
|
503
|
-
```csharp
|
|
504
|
-
[HtmlAttributeName("asp-for")]
|
|
505
|
-
public ModelExpression For { get; set; } = null!;
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
2. **CSS Class Handling (UI Class)**:
|
|
509
|
-
Tag Helpers must provide a default CSS class. If the user provides a custom class in the HTML tag:
|
|
510
|
-
- If the user class is identical to the default, maintain the default.
|
|
511
|
-
- If the user class is different, **append** it to the default class (ensure a space separator).
|
|
512
|
-
|
|
513
|
-
```csharp
|
|
514
|
-
[HtmlAttributeName("class")]
|
|
515
|
-
public string? CssClass { get; set; }
|
|
516
678
|
|
|
517
|
-
protected void ProcessCssClass(TagHelperOutput output, string defaultClass)
|
|
518
|
-
{
|
|
519
|
-
if (string.IsNullOrEmpty(CssClass))
|
|
520
|
-
{
|
|
521
|
-
output.Attributes.SetAttribute("class", defaultClass);
|
|
522
|
-
}
|
|
523
|
-
else if (CssClass.Trim() == defaultClass)
|
|
524
|
-
{
|
|
525
|
-
output.Attributes.SetAttribute("class", defaultClass);
|
|
526
|
-
}
|
|
527
|
-
else
|
|
528
|
-
{
|
|
529
|
-
output.Attributes.SetAttribute("class", $"{defaultClass} {CssClass.Trim()}");
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
3. **`AutoLoadAssets` (Asset Management)**:
|
|
535
|
-
Include a boolean property to control automatic resource loading, defaulting to `true`.
|
|
536
|
-
```csharp
|
|
537
|
-
/// <summary>
|
|
538
|
-
/// Whether to automatically load corresponding JS and CSS. Default is true.
|
|
539
|
-
/// </summary>
|
|
540
|
-
[HtmlAttributeName("auto-load-assets")]
|
|
541
|
-
public bool AutoLoadAssets { get; set; } = true;
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
### 4.2 Mandatory Asset Loading Pattern
|
|
545
|
-
|
|
546
|
-
When `AutoLoadAssets` is `true`, the Tag Helper must register its resources using the following standard pattern:
|
|
547
|
-
|
|
548
|
-
- **Version Management**: Always use `IFileVersionProvider.AddFileVersionToPath` to ensure browser cache busting.
|
|
549
|
-
- **De-duplication & Injection**: Use `HttpContext.AddStyle` and `HttpContext.AddScript` (see Section 3 for implementation) to ensure assets are only rendered once per page.
|
|
550
|
-
|
|
551
|
-
```csharp
|
|
552
|
-
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
|
553
|
-
{
|
|
554
|
-
// ... UI Generation Logic ...
|
|
555
|
-
|
|
556
|
-
if (AutoLoadAssets)
|
|
557
|
-
{
|
|
558
|
-
var httpContext = ViewContext.HttpContext;
|
|
559
|
-
var requestPathBase = httpContext.Request.PathBase;
|
|
560
|
-
|
|
561
|
-
// 1. Resolve Path with Version
|
|
562
|
-
string cssPath = "/css/components/my-component.css";
|
|
563
|
-
string versionedCss = _fileVersionProvider.AddFileVersionToPath(requestPathBase, cssPath);
|
|
564
|
-
|
|
565
|
-
string jsPath = "/js/components/my-component.js";
|
|
566
|
-
string versionedJs = _fileVersionProvider.AddFileVersionToPath(requestPathBase, jsPath);
|
|
567
|
-
|
|
568
|
-
// 2. Register via Context Extensions (renders in designated Layout blocks)
|
|
569
|
-
httpContext.AddStyle($"<link rel=\"stylesheet\" href=\"{versionedCss}\" />", "MyComponentKey");
|
|
570
|
-
httpContext.AddScript($"<script src=\"{versionedJs}\"></script>", "MyComponentKey");
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
```
|
|
574
|
-
```
|