# UWP 換頁儲存那些小事
###### tags: `UWP`, `c#`
## 問題描述
當使用者在一個畫面做編輯後,未按下儲存就跳轉別的頁面,此時要跳出==是否儲存變更==的警示彈窗
> 不同的換頁方式有不一樣的應對方式,以下會一一說明
> [name=wendee] [time=Thu, Sep 4, 2019] [color=#f2e948]
## 解法
### 1. Event Handler
#### 使用情境
任何可以互相傳值的頁面轉換皆適用,但層級很深的會寫到崩潰,多層情況建議混用別的解法(ex. UnLoaded...)
ex. pivot, tab 之間的==不同FrameName== 切換 (ex. `BasicInformationPivot`, `OutpatientTimePivot`)
`主頁.xaml.cs`
```csharp=
switch ((SettingPivot)index)
{
case SettingPivot.BasicInformation:
NavigationService.NavigateWithoutAnimation(BasicInformationPivot, typeof(BasicInformationView), null);
break;
case SettingPivot.OutpatientTime:
NavigationService.NavigateWithoutAnimation(OutpatientTimePivot, typeof(OutpatientTimeView), null);
break;
}
```
#### 作法
1. 在主頁建一個 `EventHandler` class
`主頁.xaml.cs`
```csharp=
public class UnsavedChangeEventHandler
{
public void OnExistUnsavedChange(bool changes)
{
OnUnsavedChange(new UnsavedChangeEventArgs
{
Changes = changes
});
}
protected virtual void OnUnsavedChange(UnsavedChangeEventArgs e)
{
EventHandler handler = UnsavedChange;
handler?.Invoke(this, e);
}
public event EventHandler UnsavedChange;
}
public class UnsavedChangeEventArgs : EventArgs
{
public bool Changes { get; set; }
}
```
2. 在主頁建立 `EventHandler` 的property,並註冊事件
`主頁.xaml.cs`
```csharp
private readonly UnsavedChangeEventHandler _eventHandler = new UnsavedChangeEventHandler();
_eventHandler.UnsavedChange += EventHandlerUnsavedChange;
private void EventHandlerUnsavedChange(object sender, EventArgs e)
{
if (e is UnsavedChangeEventArgs args)
{
ViewModel.ExistUnsavedChange = args.Changes;
}
}
```
3. 把主頁的 `EventHandler` 當成參數傳進分頁們
`主頁.xaml.cs`
```csharp=
switch ((SettingPivot)index)
{
case SettingPivot.BasicInformation:
NavigationService.NavigateWithoutAnimation(BasicInformationPivot, typeof(BasicInformationView), new ClinicSettingParam
{
EventHandler = _eventHandler
});
break;
case SettingPivot.OutpatientTime:
NavigationService.NavigateWithoutAnimation(OutpatientTimePivot, typeof(OutpatientTimeView), new ClinicSettingParam
{
EventHandler = _eventHandler
});
}
```
4. 分頁接收param,並在有改動時trigger剛剛註冊的事件
`分頁.xaml.cs`
```csharp=
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (e.Parameter is ClinicSettingParam param)
{
ViewModel.EventHandler = param.EventHandler;
}
}
```
5. 在分頁有改動時trigger剛剛註冊的事件
`分頁ViewModel.cs`
```csharp=
public UnsavedChangeEventHandler EventHandler;
// 確定 isLoaded = true 之後
EventHandler?.OnExistUnsavedChange(isChanged);
```
:::warning
這樣把就可以把彈窗放在 `主頁.xaml`,並從 `ViewModel.ExistUnsavedChange` 判斷分頁是否有改動來決定要不要跳彈窗~
* 彈窗只有一個
:::
### 2. OnNavigatedFrom / OnNavigatingFrom
#### 使用情境
pivot, tab 之間的==相同FrameName== 切換 (ex.`SettingNavFrame`)
```csharp=
switch((Setting)index)
case (Setting.CLINIC):
NavigationService.NavigateWithoutAnimation(SettingNavFrame, typeof(ClinicSettingPage));
break;
case (Setting.OWNEXPENSE):
NavigationService.NavigateWithoutAnimation(SettingNavFrame, typeof(OwnExpenseView));
break;
```
#### 作法
1. 在分頁的 `OnNavigatedFrom / OnNavigatingFrom` 裡做判斷
`分頁.xaml.cs`
```csharp=
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
// check if unsaved changes exist
}
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
// check if unsaved changes exist
// 這邊的好處是決定不想換頁可以這樣寫 e.Cancel = true;
}
```
:::warning
這樣每個分頁都會有各自的提醒彈窗在各自的 `分頁.xaml` 裡,離開時各自在 `OnNavigatedFrom/OnNavigatingFrom` 決定要不要跳彈窗
* 彈窗有多個
:::
### 3. UnLoaded
#### 使用情境
當這一頁會從 `VisualTree` 上消失時
ex1. pivot, tab 之間的 ==相同FrameName== 切換
ex2. 在 ==相同FrameName==內的 pivot, tab 切換至以 ==相同FrameName== 做切換的父級 pivot, tab (像是在診所設定做編輯後,跳至掛號頁面)

#### 作法
1. 以上面情況為例,我們在分頁的 `UnLoaded` 註冊邏輯判斷
`分頁.xaml.cs`
```csharp=
public ClinicSettingPage()
{
Unloaded += async delegate { await CheckChanges(); };
}
private async Task CheckChanges()
{
// check if unsaved change exists
}
```
:::danger
**注意** UnLoaded 一定要用 `async/await/Task` 不然跳彈窗會出錯喔! 畢竟 UnLoaded 發生就表示這一頁就快要不見了,我們不用 await 等一下讓彈窗出現,彈窗就會從visualTree上被拔除然後程式就找不到彈窗就Error了 ><
:::
:::warning
這樣每個分頁都會有各自的提醒彈窗在各自的 `分頁.xaml` 裡,離開時各自在 `UnLoaded` 決定要不要跳彈窗
* 彈窗有多個
:::
### Q & A
#### Q1. 為什麼 ==不同FrameName== 切換不會觸發 `OnNavigatedFrom`?
OnNavigatedFrom 就是要同媽媽換小孩才會被觸發喔! 媽媽不同NavigatedFrom的基礎就不一樣所以不會被trigger (就只是顯示不一樣的媽媽而已,跟pivot的概念一樣)
#### Q2. 為什麼 ==不同FrameName== 切換不會觸發 `UnLoaded`?
`UnLoaded` 是這一頁會從 `VisualTree` 上消失時被觸發,不同FrameName的切換只是顯示上的不一樣,其實每個Frame都還掛在 `VisualTree` 上喔~
> 不過有個很神祕的現象我還沒弄懂,下面是不同FrameName做切換
> 
> 我從**基本資訊**跳到**費用**不會觸發 `UnLoaded`(正常),可是我從**費用**跳回**基本資訊**它就先叫了**基本資訊**的 `Loaded` 後又叫了**基本資訊**的`UnLoaded`! ...今古奇觀qq
> [color=orange]
> >我猜 `Loaded` 和 `Unloaded` 是不一樣的 Page(`Unloaded` 舊的基本資訊 &
`Loaded` 新的基本資訊頁)
[name=sushi]
### 其他失敗的嘗試
#### Windows.Current.VisibilityChanged
這是整個app被最小化和被打開做切換時trigger,和畫面無關