如何在 Xamarin.Forms 的 iOS App,需要即時變更回上一頁按鈕的文字
最近遇到上課學員提出一個問題,學員要設計一個頁面,這個頁面可以設定這個 App 要使用的多國語言選單功能,當從某個語系切換到另外一個語系的時候,這個頁面上的相關內容也可以同步的切換成為新的語言,可是,當這個 App 在 Android 平台下的時候,相關的設計都可以正常運作,不過,當在 iOS 平台下的時候,卻發生了問題;問題在於這個頁面的最上方有個導航工具列,在 Android 平台下這個導航工具列上僅會顯示這個頁面的名稱,此時,這個頁面的名稱是可以透過資料綁定的方式,在 ViewModel 程式碼中,即時變更成為任何文字,頁面的工具列上也就會即時變更與顯示出最新的頁面名稱,然而,在 iOS 平台下,導航工具列的左上方出現的卻是回上頁的頁面名稱或者是自訂的回上頁文字,若在設定頁面的 ViewModel 來變更回上頁的按鈕的文字,是無法正常更新的,這是因為回上頁的按鈕名稱是要在上一頁的頁面中來設定的。
這篇文章的範例專案原始碼,可以從 GitHub 取得
下面螢幕截圖為這個範例專案在 Android 平台上的執行結果左下圖為一開始啟動 App 的頁面,右下圖為按下 切換可動態變換按鈕文字 的按鈕,就會切換到右下角的頁面。
下面螢幕截圖為這個範例專案在 iOS 平台上的執行結果左下圖為一開始啟動 App 的頁面,右下圖為按下 切換可動態變換按鈕文字 的按鈕,就會切換到右下角的頁面。
而在首頁上,其宣告的 XAML 內容可以從底下看的出來,這裡並沒有使用
NavigationPage.BackButtonTitle="自訂回上頁按鈕文字"
這樣的宣告,所以,當從首頁切換到下一個頁面的時候,在 iOS 平台下預設將會顯示首頁頁面標題的文字在下一頁的回上頁按鈕文字上。不過,因為這裡的頁面已經過修正,可以自動更新回上頁按鈕文字,與平常看到的不太一樣;若在 Xamarin.Forms 開發的程式,在 iOS 平台下跑起來,其實將會看到如下圖的畫面,若在前一頁面中,沒有使用 NavigationPage.BackButtonTitle
來指定客製按鈕文字,原則上都會出現上一頁面的標題文字在這個頁面的回上頁按鈕上。<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ChangeiOSBackButtonText.Views.MainPage"
Title="上頁文字變換">
<StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
<Label Text="Welcome to Xamarin Forms and Prism!" />
<Button Text="切換可動態變換按鈕文字" Command="{Binding SwithChangePageCommand}"/>
<Button Text="切換一般頁面" Command="{Binding SwithNextPageCommand}"/>
<Button Text="連續兩個頁面" Command="{Binding SwithTwoPageCommand}"/>
</StackLayout>
</ContentPage>
那麼,要如何做到這樣的客製化的回上頁按鈕功能,並且可以結合資料綁定,做到動態可以在 ViewModel 中,來設定要顯示的文字,例如,回上頁按鈕文字、頁面主題名稱,達到如下的效果。
在 Android 系統下,若點選 切換中文 按鈕,會出現左下圖畫面,若點選 Switch English 按鈕,則會出現右下圖畫面。
在 iOS 系統下,若點選 切換中文 按鈕,會出現左下圖畫面,若點選 Switch English 按鈕,則會出現右下圖畫面。
現在來看看這個頁面 XAML 宣告內容,在這裡若想要變更回上頁按鈕的文字,需要在這裡使用這裡自訂 附加屬性 DynamicBackButtonTextAttached.SetBackButtonText ,使用它來指定要顯示的回上頁按鈕文字內容,由於這個附加屬性值可以做到資料綁定的更新通知,所以,想要變更該頁面回上頁按鈕文字內容,就可以從 ViewModel 來修改 ThisBackText 這個屬性值即可。
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
prism:ViewModelLocator.AutowireViewModel="True"
x:Class="ChangeiOSBackButtonText.Views.CanChangePage"
xmlns:DynBtn="clr-namespace:ChangeiOSBackButtonText.AttachedProperties"
DynBtn:DynamicBackButtonTextAttached.SetBackButtonText="{Binding ThisBackText}"
xmlns:localViews="clr-namespace:ChangeiOSBackButtonText.Views"
Title="{Binding Title}">
<StackLayout
>
<Entry Text="{Binding Message}"/>
<Button Text="設定" Command="{Binding SetBackButtonTextCommand}"/>
<Button Text="切換中文" Command="{Binding SetChineseCommand}"/>
<Button Text="Switch English" Command="{Binding SetEnglishCommand}"/>
</StackLayout>
</ContentPage>
設計理念說明
為了要做到這樣的效果,這個時候需要建立一個名為 NaviCustomPage 的 NavigationPage 型別的頁面,並且整個 App 的頁面導航功能,需要透過這個新建立的導航頁面來切換頁面,這樣剛剛所使用的附加屬性 DynamicBackButtonTextAttached.SetBackButtonText 才會正常的運作。
而這個附加屬性的主要目的是要能夠指定當時這個頁面的回上頁按鈕文字內容,不過,這需要透過導航頁面中的 可綁定屬性 DynamicBackButtonText 來指定該導航工具列上的回上頁按鈕文字,該 DynamicBackButtonText 屬性將會透過 附加屬性 DynamicBackButtonTextAttached.SetBackButtonText 來變更,因為在這個附加屬性上,有訂閱 OnSetBackButtonTextChanged 這個屬性變更事件,所以,當該附加屬性有變動就會觸發這個事件,接著將會在這個事件中執行一個輔助支援方法 ChangeBackButtonTextHelper.ChangeBackButtonText,透過這個方法來取得當前導航工具列上的 可綁定屬性 ,也就 DynamicBackButtonText 屬性值。
最後,需要在 iOS 平台下來實作出 NaviCustomPage 的 NavigationRenderer,這裡將會在 iOS 平台下建立一個 NaviCustomPageRenderer 類別來做到這件事情,在這個類別中,將會覆寫方方法 OnElementPropertyChanged ,若該 NaviCustomPage 內的任何一個可綁定屬性有異動的時候,就將會觸發這個事件;在該事件中將會檢查是否是 DynamicBackButtonText 這個屬性有異動,若有變更的話,將會呼叫 UpdateBackButtonTitleText 方法來變更回上頁按鈕。
因此,就可以完成這樣的需求,現在來逐一檢視這樣的設計過程。
NaviCustomPage 建立與加入一個可綁定屬性 DynamicBackButtonText
建立一個新的 NavigationPage,並且指定該頁面的名稱為 NaviCustomPage
<?xml version="1.0" encoding="utf-8" ?>
<NavigationPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
prism:ViewModelLocator.AutowireViewModel="True"
x:Class="ChangeiOSBackButtonText.Views.NaviCustomPage">
</NavigationPage>
打開 NaviCustomPage 的 Code Behind ,加入一個可綁定屬性 DynamicBackButtonText,在這裡並不需要做其他的設計,因為,這個屬性存在的目的僅僅是要能夠讓 iOS 下的 NaviCustomPageRenderer 類別,可以知道這個屬性值有異動,也就是需要更新回上頁按鈕了,而真正要變更回上頁按鈕的處理動作,需要 iOS 平台下的 NaviCustomPageRenderer 類別中來處理。
public partial class NaviCustomPage : NavigationPage
{
#region DynamicBackButtonText 可綁定屬性 BindableProperty
public static readonly BindableProperty DynamicBackButtonTextProperty =
BindableProperty.Create("DynamicBackButtonText", // 屬性名稱
typeof(string), // 回傳類型
typeof(NaviCustomPage), // 宣告類型
"", // 預設值
propertyChanged: OnDynamicBackButtonTextChanged // 屬性值異動時,要執行的事件委派方法
);
public string DynamicBackButtonText
{
set
{
SetValue(DynamicBackButtonTextProperty, value);
}
get
{
return (string)GetValue(DynamicBackButtonTextProperty);
}
}
private static void OnDynamicBackButtonTextChanged(BindableObject bindable, object oldValue, object newValue)
{
}
#endregion
public NaviCustomPage()
{
InitializeComponent();
}
}
指定使用來做頁面導航
在這個測試專案,將會在 App.xaml.cs 內,指定使用 NaviCustomPage 作為導航工具列
protected override async void OnInitialized()
{
InitializeComponent();
await NavigationService.NavigateAsync("MDPage/NaviCustomPage/MainPage");
}
設計附加屬性 DynamicBackButtonTextAttached
在 Xamarin.Forms 專案內,建立這個類別,在此建裡一個附加屬性。在建立這個 附加屬性 類別的時候,有指定這個 propertyChanged 事件,在此綁定到這個 OnSetBackButtonTextChanged 事件上。
當特定 ContentPage 頁面上有指定 DynamicBackButtonTextAttached.SetBackButtonText 屬性且屬性值有變動的時候,這個 OnSetBackButtonTextChanged 就會被觸發,不過,當時的頁面必須要有實作 IDynamicChangeBackText 介面,這樣,才能夠接下來做異動,這裡將會呼叫
ChangeBackButtonTextHelper.ChangeBackButtonText(newString);
方法,這裡將會變更導航頁面上的 DynamicBackButtonText 屬性public class DynamicBackButtonTextAttached
{
#region SetBackButtonText 附加屬性 Attached Property
public static readonly BindableProperty SetBackButtonTextProperty =
BindableProperty.CreateAttached(
propertyName: "SetBackButtonText", // 屬性名稱
returnType: typeof(string), // 回傳類型
declaringType: typeof(ContentPage), // 宣告類型
defaultValue: null, // 預設值
propertyChanged: OnSetBackButtonTextChanged // 屬性值異動時,要執行的事件委派方法
);
public static void SetSetBackButtonText(BindableObject bindable, string entryType)
{
bindable.SetValue(SetBackButtonTextProperty, entryType);
}
public static string GetSetBackButtonText(BindableObject bindable)
{
return (string)bindable.GetValue(SetBackButtonTextProperty);
}
private static void OnSetBackButtonTextChanged(BindableObject bindable, object oldValue, object newValue)
{
ContentPage page = bindable as ContentPage;
if (page == null) return;
string oldString = oldValue as string;
string newString = newValue as string;
if (newString == null) return;
if(page is IDynamicChangeBackText)
{
ChangeBackButtonTextHelper.ChangeBackButtonText(newString);
}
}
#endregion
}
建立輔助支援方法,可以修改當前導航頁面上的 DynamicBackButtonText 屬性
在這裡將會使用 App.Current.MainPage 屬性來檢查當前的頁面是 MasterDetailPage 還是 NaviCustomPage ;若為 MasterDetailPage 頁面,對於 NaviCustomPage 可以透過 MasterDetailPage.Detail 取得;若當前頁面是 NaviCustomPage,則就可以直接取得這個物件。
有了 naviCustomPage 這個物件,就可以透過該物件來存取該導航頁面上剛剛建立的新可綁定屬性 naviCustomPage.DynamicBackButtonText,一旦變更這個屬性值,在 iOS 平台下的 NaviCustomPageRenderer 事件也會被觸發。
public class ChangeBackButtonTextHelper
{
public static void ChangeBackButtonText(string newBackButtonText)
{
NaviCustomPage naviCustomPage = GetNaviCustomPage();
if (naviCustomPage != null)
{
naviCustomPage.DynamicBackButtonText = newBackButtonText;
};
}
public static string GetBackButtonText()
{
string result = "";
NaviCustomPage naviCustomPage = GetNaviCustomPage();
if (naviCustomPage != null)
{
result = naviCustomPage.DynamicBackButtonText;
}
return result;
}
public static NaviCustomPage GetNaviCustomPage()
{
NaviCustomPage naviCustomPage = null;
if (App.Current.MainPage is MasterDetailPage)
{
MasterDetailPage masterDetailPage = App.Current.MainPage as MasterDetailPage;
if (masterDetailPage.Detail is NaviCustomPage)
{
naviCustomPage = masterDetailPage.Detail as NaviCustomPage;
}
}
else if (App.Current.MainPage is NaviCustomPage)
{
naviCustomPage = App.Current.MainPage as NaviCustomPage;
}
return naviCustomPage;
}
}
對於要能夠變更回上頁按鈕文字的 ContentPage,需要實作 IDynamicChangeBackText
首先,對於 IDynamicChangeBackText 這個介面內,並沒有宣告什麼內容,只是為了要能夠分辨出該頁面是否可以透過剛剛設計的可綁定屬性來取得回上頁按鈕文字。
public interface IDynamicChangeBackText
{
}
對於要自動更新回上頁按鈕的 ContentPage,需要打開該頁面的 Code Behind C# 程式碼,實作這個 IDynamicChangeBackText 介面。
public partial class CanChangePage : ContentPage, IDynamicChangeBackText
{
public CanChangePage()
{
InitializeComponent();
}
}
在 iOS 平台下實作 NaviCustomPageRenderer
為了要能夠做出可變動回上頁按鈕,這裡在 iOS 平台下,建立 NaviCustomPageRenderer 類別,該類別需要實作 NavigationRenderer
在這裡需要覆寫 OnPushAsync 與 OnPopViewAsync 這兩個方法,當要透過導航工具列切換到不同頁面的時候,這個 OnPushAsync 方法就會被執行,在此方法內,若該新頁面有實作 IDynamicChangeBackText 這個介面,將會呼叫 SetBackButtonOnPage(page) 方法,該方法將會呼叫 ChangeBackButtonTextHelper.GetBackButtonText() 方法,取得當時設定到的回上頁按鈕自訂文字內容。
[assembly: ExportRenderer(typeof(NaviCustomPage), typeof(NaviCustomPageRenderer))]
namespace ChangeiOSBackButtonText.iOS.Renderers
{
public class NaviCustomPageRenderer : NavigationRenderer
{
UIBarButtonItem barButtonItem;
NaviCustomPage oldMyNaviPage;
NaviCustomPage newMyNaviPage;
protected override Task<bool> OnPushAsync(Page page, bool animated)
{
var retVal = base.OnPushAsync(page, animated);
if (page is IDynamicChangeBackText)
{
SetBackButtonOnPage(page);
}
return retVal;
}
protected override Task<bool> OnPopViewAsync(Page page, bool animated)
{
var retVal = base.OnPopViewAsync(page, animated);
if (page is IDynamicChangeBackText)
{
var stack = page.Navigation.NavigationStack;
var returnPage = stack[stack.Count - 2];
if (returnPage != null)
{
SetBackButtonOnPage(returnPage);
}
}
else
{
//SetDefaultBackButton();
}
return retVal;
}
void SetBackButtonOnPage(Page page)
{
//var stack = page.Navigation.NavigationStack;
//if(stack.Count == 1)
//{
// //SetDefaultBackButton();
//}
if (page is IDynamicChangeBackText)
{
string backButtonText = ChangeBackButtonTextHelper.GetBackButtonText();
SetImageTitleBackButton("Left2", backButtonText, -15);
}
else
{
//SetDefaultBackButton();
}
}
void SetImageTitleBackButton(string imageBundleName, string buttonTitle, int horizontalOffset)
{
var topVC = this.TopViewController;
// Create the image back button
var backButtonImage = new UIBarButtonItem(
UIImage.FromBundle(imageBundleName),
UIBarButtonItemStyle.Plain,
(sender, args) =>
{
topVC.NavigationController.PopViewController(true);
});
// Create the Text Back Button
//var backLeftButtonText = new UIBarButtonItem(
// "<",
// UIBarButtonItemStyle.Plain,
// (sender, args) =>
// {
// topVC.NavigationController.PopViewController(true);
// });
// Create the Text Back Button
var backButtonText = new UIBarButtonItem(
buttonTitle,
UIBarButtonItemStyle.Plain,
(sender, args) =>
{
topVC.NavigationController.PopViewController(true);
});
backButtonText.SetTitlePositionAdjustment(new UIOffset(horizontalOffset, 0), UIBarMetrics.Default);
// Add buttons to the Top Bar
UIBarButtonItem[] buttons = new UIBarButtonItem[2];
buttons[0] = backButtonImage;
buttons[1] = backButtonText;
topVC.NavigationItem.LeftBarButtonItems = buttons;
}
void SetDefaultBackButton()
{
this.TopViewController.NavigationItem.LeftBarButtonItems = null;
}
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
oldMyNaviPage = (NaviCustomPage)e.OldElement;
newMyNaviPage = (NaviCustomPage)e.NewElement;
if (oldMyNaviPage != null)
{
oldMyNaviPage.PropertyChanged -= OnElementPropertyChanged;
}
if (newMyNaviPage != null)
{
newMyNaviPage.PropertyChanged += OnElementPropertyChanged;
}
}
private void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "DynamicBackButtonText")
{
UpdateBackButtonTitleText();
}
}
private void UpdateBackButtonTitleText()
{
string backTitle = "";
backTitle= ChangeBackButtonTextHelper.GetBackButtonText();
if (this.NavigationBar.Items.Count() > 1)
{
this.TopViewController.NavigationItem.LeftBarButtonItems[1].Title = backTitle;
}
}
}
}