跳轉到

程式資訊

  • 程式 IDContentWindow
  • 所屬子系統:Other
  • 狀態:stable

內容視窗容器(ContentWindow)

本頁屬系統機制說明

「內容視窗容器」不是業務表單,沒有自己的查詢、欄位或單據。它是整個系統各種模態彈窗(轉單、設定、子功能對話框、進度條、附件、簽核…)共用的外框:負責標題列、視窗大小、關閉按鈕、確定取消(DialogResult)與生命週期。本頁說明這個框架如何運作,受眾偏向系統與開發人員;一般使用者多半不會直接看到「ContentWindow」這個名字,而是看到它承載的各種彈窗內容。

功能簡介

當系統需要以模態(彈出後必須處理完才能回主畫面)方式顯示一段內容時,不會各自 new 一個獨立 Window,而是統一透過 Context.ContentWindowService.ShowContentWindow(vm) 把一個「內容 ViewModel」交給 ContentWindow 承載。ContentWindow 提供共用的外框:

  • 一條標題列(顯示內容 VM 給的標題)。
  • 視窗大小/置中(預設置於主視窗中央,可由內容 VM 指定寬高或自動貼合內容)。
  • 一個 ContentControl,把內容 VM 透過 App.xaml 的 DataTemplate 對應到實際畫面塞進去。
  • 關閉按鈕的顯示/隱藏,以及 確定取消 的結果回傳(DialogResult)。

在流程中的位置:它是 MVVM 模態對話框的「承載外框」。任何需要彈窗的功能(例如轉傳票簽核附件、報表預覽、進度條等)都共用這一個容器,因此外觀與行為一致。

另有一個無邊框的變體 NoBorderContentWindow,用於不需要標題列/邊框的浮動內容(例如全透明的進度浮層)。

前置需求

條件 影響
內容 VM 實作 IContentOfContentWindow 唯有實作此介面(一般繼承 ERPProgramContentWindowViewModel)的 VM 才能被 ShowContentWindow 承載
內容 VM 覆寫 Parameter 提供標題、寬、高、是否顯示關閉按鈕、是否貼合內容;缺少則用容器預設值
App.xaml 有對應的 DataTemplate 容器只放 ContentControl,實際畫面靠 App.xaml 把內容 VM 型別對應到 View;少了對應會顯示空白
主視窗(MainWindow)已開啟 容器以主視窗為 Owner 並置中;主視窗的寬高也決定彈窗的預設最大尺寸(主視窗寬高各減 100)

沒有獨立權限

內容視窗容器本身不掛任何選單代碼或權限。能不能開某個彈窗,取決於承載在裡面的那支業務功能的權限,與容器無關。

操作步驟

對一般使用者而言,操作的是「彈窗裡的內容」,容器只提供共通的關閉行為:

  1. 從某支業務功能觸發彈窗(例如按工具列的轉傳票、簽核、附件等)。系統會以模態方式彈出 ContentWindow主畫面在彈窗關閉前會被鎖住(無法操作背後的版面)。
  2. 在彈窗標題列確認標題文字,了解這個彈窗在做什麼。
  3. 操作彈窗內的內容(依各功能而異)。
  4. 結束方式有兩種:
    1. 完成並確認:內容功能會把結果設為「成功」(DialogResult = true),彈窗關閉,呼叫端視為使用者按了 確定
    2. 取消/關閉:按彈窗的關閉按鈕或內容功能提供的 取消(對應 DialogResult = false),彈窗關閉,呼叫端視為使用者放棄。
  5. 彈窗關閉後,主畫面自動解除鎖定,回到原本的作業。

關閉按鈕可被隱藏

某些「不可中途中斷」的彈窗(例如執行中的進度條)會隱藏關閉按鈕,必須等內部工作完成後自動關閉。這是內容功能刻意設定的,不是當機。

欄位說明

容器沒有資料欄位,下表是內容 VM 透過 ParameterContentWindowParameter)交給容器的設定項,決定彈窗外觀與行為:

設定項 必填 說明/格式/預設
Title(標題) 顯示在標題列;空字串時標題列空白
Width(寬) 指定彈窗寬度;0 表示不指定,改用容器基準值(有主視窗時為主視窗寬 −100,無主視窗時退回內建預設 1500)
Height(高) 指定彈窗高度;0 表示不指定,改用容器基準值(有主視窗時為主視窗高 −100,無主視窗時退回內建預設 800)
NeedCloseButton(顯示關閉鈕) 預設依建構式而定;為 true 時容器顯示關閉按鈕,為 false 時隱藏(搭配「不可中途關閉」的彈窗)
IsSizeToContent(貼合內容) 預設 false;為 true 時忽略寬高,視窗自動縮放至內容大小(例如小型進度條)

尺寸的取捨規則

容器先以「主視窗寬高各減 100」當基準,再與 Parameter 指定的寬高取較小值Math.Min)。也就是說,內容 VM 指定的尺寸是「上限」,永遠不會超出主視窗範圍;若指定 0 則完全交給基準值。

欄位連動與附加功能

這一節說明容器在背後替所有彈窗做了哪些事——也就是「當你開/關一個彈窗時,系統實際發生什麼」。

開窗:ShowContentWindow 的完整流程

當任一功能呼叫 Context.ContentWindowService.ShowContentWindow(vm)

  1. 系統先鎖住主版面Container.LayoutIsEnabled = false),讓背景無法操作,達成模態效果。
  2. 透過 ProgramNavigator.SwitchToContentWindow(vm, Open) 把這個彈窗登記到程式導覽紀錄(把目前程式/檢視推入 history 堆疊,記錄「目前開著哪個彈窗」)。若內容 VM 是 ERPProgramBaseViewModel,這一步也會自動呼叫其 Init() 做初始化。
  3. 建立 ContentWindow 並把內容 VM 包進專屬的 ContentWindowViewModel
    1. 容器把內容 VM 設為 Content,再透過 App.xaml 的 DataTemplate 對應出實際 View 顯示在 ContentControl
    2. 容器把內容 VM 的 Parameter.Title 設為標題列文字。
    3. 回填參照:容器把自己(ContentWindowViewModel)回填到內容 VM 的 ContentWindowViewModel 屬性,內容 VM 之後就能用它來關窗。
  4. Parameter 計算尺寸並決定關閉按鈕是否顯示,然後以模態ShowDialog)方式顯示,阻塞呼叫端直到彈窗關閉。
  5. 彈窗關閉後:
    1. 解除主版面鎖定(LayoutIsEnabled = true)。
    2. SwitchToContentWindow(vm, Close) 登記彈窗關閉。
    3. 若內容 VM 實作了 IDisposable自動呼叫 Dispose() 釋放資源。
  6. 回傳 boolDialogResult == truetrue(使用者確認/流程成功),否則回 false。呼叫端通常以 if (ShowContentWindow(vm)) { ... } 判斷使用者是否按了確定。

關窗:內容 VM 如何結束彈窗

容器本身不提供業務按鈕,關窗的主動權在內容 VM

  • 內容 VM 把 ContentWindowViewModel.DialogResult = true(確定/成功)或 = false(取消)即可關閉彈窗。容器透過 RadWindowDialogResult.DialogResult 附加屬性綁定到這個值:一旦設值,附加屬性的回呼會把 TsWindowRadWindow)的 DialogResult 設好並呼叫 Close()RadWindow 不會因 DialogResult 自動關窗,所以靠這個回呼補上關閉動作)。
  • 繼承 ERPProgramContentWindowViewModel 的內容 VM 內建一個 ExitCommand(建構式 new TsCommand(ExitExecute)):執行時會把 DialogResult 設為 false,等同「取消」。XAML 的取消按鈕通常綁這個命令。
  • 容器標題列的關閉按鈕(X)走 RadWindow 自身的關閉流程:直接關閉視窗、不主動設定 DialogResult,因此 DialogResult 維持 nullShowContentWindow 視同 false(取消)。是否允許用 X 關閉由 CanClose 控制(見下節)。

隱藏關閉按鈕(不可中途中斷的彈窗)

  1. 預設情況下,包裝用的 ContentWindowViewModel.NeedHideCloseButton = true,視窗載入時會把容器設為不可關閉CanClose = false),使用者無法靠 X 中途關閉。
  2. Parameter.NeedCloseButton == true,容器會把 NeedHideCloseButton 改為 false顯示關閉按鈕,允許使用者自行關閉。
  3. 換言之,要不要給使用者一個「手動關閉」的出口,由內容功能透過 Parameter 決定。

載入後自動執行(進度條型彈窗)

  1. 若內容 VM 額外實作 IContentWindowLoadedAsync,容器在視窗 Loaded 之後會自動 await OnContentWindowLoadedAsync() 一次
  2. 典型用途是「邊顯示進度邊做事,做完自動關窗」:例如 ProgressBarViewModel 在此事件裡執行指定的 WorkAsync(),並在工作結束(含發生例外)後,於 finally 區塊把 DialogResult = true 讓視窗自動關閉
  3. 這類彈窗通常隱藏關閉按鈕、並用 IsSizeToContent 讓視窗貼合一個小進度條。

無邊框變體(NoBorderContentWindow)

  • NoBorderContentWindow 共用同一套 ContentWindowViewModelParameter 機制,且尺寸計算(Math.Min 取上限、IsSizeToContentNeedCloseButtonNeedHideCloseButton)與 ContentWindow 相同,但套用無邊框、透明背景的自訂 ControlTemplateBorderAdornerDecoratorContentPresenter),且置中於整個螢幕WindowStartupLocation=CenterScreen)而非主視窗。
  • 適合不需要標題列/邊框的浮動內容。差別在於:它的 Loaded 並未真的去隱藏關閉按鈕(對應程式碼目前被註解掉),因此 NeedHideCloseButton 在這個變體上不會實際生效——它本來就靠無邊框樣板而不顯示標準的關閉鈕。

與其他開窗方式的關係

新功能一律用 ShowContentWindow(搭配繼承 ERPProgramContentWindowViewModel 的 VM)。舊式的 WindowHelper().OpenWindow(...) 直接 new Window() 的寫法已廢棄,是漸進遷移的對象,不要在新程式碼使用。

常見問題

彈窗開起來是一片空白,看不到內容?

容器只放一個 ContentControl,實際畫面靠 App.xamlDataTemplate 把「內容 VM 型別」對應到對應的 View。若該內容 VM 沒有在 App.xaml 註冊對應的 DataTemplate,就會顯示空白。請確認對應的 <DataTemplate DataType="..."> 存在。

為什麼有些彈窗沒有關閉按鈕,關不掉?

這是刻意的。預設 NeedHideCloseButton = true 會讓視窗不可關閉;只有當內容功能在 Parameter 指定 NeedCloseButton = true 時才會顯示關閉按鈕。像進度條這類「不可中途中斷」的彈窗會隱藏關閉鈕,等內部工作完成後自動關閉,請耐心等候而非強制結束程式。

呼叫端怎麼知道使用者按了確定還是取消?

ShowContentWindow阻塞到彈窗關閉,並回傳 bool:內容 VM 把 DialogResult 設為 true(確定/成功)時回 true,否則回 false。呼叫端標準寫法是 if (Context.ContentWindowService.ShowContentWindow(vm)) { ... }

彈窗為什麼總是比主視窗小,無法放到很大?

容器以「主視窗寬高各減 100」為基準,並與內容 VM 指定的寬高取較小值,所以彈窗永遠不會超出主視窗範圍。若要更大的可視空間,請先放大主視窗。

彈窗關閉後,背後表單還是鎖住的,怎麼辦?

正常情況下容器在 ShowDialog 結束後會自動把 LayoutIsEnabled 設回 true 解除鎖定。若異常卡住,多半是內容 VM 在關閉流程中拋出例外導致流程中斷;請從內容功能本身排查,容器本身不持有業務邏輯。

相關功能


需補截圖清單

本頁為系統機制說明,以下截圖待 runtime 驗收後補拍(放 docs/Other/images/):

  1. contentwindow-step1.png — 一個典型的 ContentWindow 彈窗(含標題列與關閉按鈕)
  2. contentwindow-step2.png — 隱藏關閉按鈕的進度條型彈窗(IsSizeToContent 貼合內容)
  3. contentwindow-step3.png — NoBorderContentWindow 無邊框透明浮層對照