XAML in Xamarin.Forms 基礎篇 電子書

Xamarin.Forms 開發實戰

特別說明

2019/09/04

升級到 VS2019 16.2.3 後,要執行 Xamarin.Android 專案,卻得到 System.IO.FileNotFoundException: 'Invalid Image' 錯誤訊息

升級到 VS2019 16.2.3 後,要執行 Xamarin.Android 專案,卻得到 System.IO.FileNotFoundException: 'Invalid Image' 錯誤訊息

最近這兩個月因為忙於 Xamarin.Forms 的授課,所以,一直沒有將上課用的 Surface Pro 4 電腦上的 Visual Studio 2019 軟體升級到最新版本,本來想等到 09/03 最後一天課程上完之後,再來進行 Visual Studio 2019 的升級工作,沒想到 09/03 當天授課的時候,卻發現到 Visual Studio 2019 無法打開,電腦上顯示需要我把 Visual Studio 進行升級,這是我最擔心的一件事情,因為,每次軟體有升級的時候,都會怕對於上課過程中有些影響(這也就是為什麼,當會進行類似 Xamarin.Forms 6 天這樣長時間的授課時候,通常不會把 Visual Studio 進行升級 ,一來擔心新功能的推出對於授課會有,二來擔心有些狀況僅會出現在新升級後的電腦,而學員的電腦上的 Visual Studio 卻沒有升級,造成不一致的問題;所以,在上課之前,我通常是僅會將作業系統進行更新到最新版本這樣的動作而已。
好的,那麼當天究竟發生了甚麼事情?當我將 Visual Studio 2019 升級到最新版本,也就是 16.2.3 版本,當我使用了 Prism Template Pack 建立起一個 Xamarin.Forms 的專案後,首先在 Visual Studio 2019 的錯誤視窗中,看到了底下的訊息。
警告        Skipping BlankApp2.Droid.Resource.Attribute.actionBarSize. Please check that your Nuget Package versions are compatible.    BlankApp2.Android
雖然該訊息僅是個警告訊息,不過,還是覺得怪怪的,緊接著開始進行 Xamarin.Android 的專案建置與執行的動作,不幸的是,此時在螢幕上顯示了 System.IO.FileNotFoundException Message=Invalid Image 這個錯誤訊息,如同底下螢幕截圖顯示的狀況。
這下慘了,課程僅進行到 30 分鐘,若無法建立一個空的 Xamarin.Forms for Prism 專案,那麼要如何繼續底下 6個小時的課程呢?(心裡有著 今日可以提早完課,下次再來的念頭)。
我還是先花了一些時間做了檢測,首先,看到在 Visual Studio 2019 輸出視窗中的最後一行,看到了 Assembly Loader probing location: '/storage/emulated/0/Android/data/com.companyname.appname/files/.__override__/Xamarin.Interactive.dll'. **System.IO.FileNotFoundException:** 'Invalid Image' 這樣的訊息,我想,這個訊息應該是上面對話窗所遇到的主要問題。
接下來的內容,是我回到家裡後,使用另外一台電腦上的 Visual Studio 2019,也把它升級到 16.2.3 版本,結果看到相同的問題,但是,當我不使用 Prism Template Pack 來產生一個 Prism Blank App (Xamarin.Forms) 的應用程式,而是使用 Visual Studio 2019 內建的 行動應用程式 (Xamarin.Forms) 專案範本,建立起一個空的 Xamarin.Forms 的專案,卻沒有發現到這個專案在建置與執行過程中,會出現上述的問題,一切都可以正常運作。
我針對 使用 Prism Template Pack 所產生的 Xamarin.Form 專案,打開 Xamarin Android 專案內的 MainActivity.cs 檔案,接著,在 OnCreate 方法內加入訂閱 AppDomain.CurrentDomain.UnhandledException 這個事件,看看是否能夠捕捉到任何可疑的例外異常錯誤資訊,並且在 int foo = 10; 這個敘述上設定一個中斷點,若執行的過程中可以停在剛剛設定的那行中斷點上,那麼,也許可以從 UnhandledExceptionEventArgs 這個參數中,看到一些端倪。
protected override void OnCreate(Bundle bundle)
{
    AppDomain.CurrentDomain.UnhandledException += (s, e) =>
    {
        // 這一行是要用於設定中斷點之用
        int foo = 10;
    };
    TabLayoutResource = Resource.Layout.Tabbar;
    ToolbarResource = Resource.Layout.Toolbar;

    base.OnCreate(bundle);

    global::Xamarin.Forms.Forms.Init(this, bundle);
    LoadApplication(new App(new AndroidInitializer()));
}
當修正好了之後,再度執行這個專案,依然還是看到這個這個錯誤訊息,並且並沒有停留在剛剛設定的中斷點上,所以,這應該是這個專案尚未啟動到 MainActivity 的時候,就產生了問題。
現在回到 Xamarin.Forms 授課當天,那時,只好針對幾個設定項目進行測試,看看是否可以讓課程繼續進行下去,這個時候,只要使用 Prism Template Pack 建立的專案,可以正常建置、執行,那麼,當天的課程就可以繼續下去囉。
這裡是當天解決這個問題的其中一個做法
  • 首先打開 Xamarin.Android 專案的屬性設定視窗 (可以滑鼠右擊 Android 專案節點,選擇最後面的屬性項目,或者在 Android 專案內找到 Properties 節點,使用滑鼠雙擊這個節點)
  • 當 Android 屬性視窗出現之後,切換到 Android 選項 標籤頁次 Tabbed,且解除 使用 Fast Deployment (僅限偵錯模式) 這個選項)
注意,底下的螢幕截圖,是 使用 Fast Deployment (僅限偵錯模式) 這個選項被設定的狀態,請要解除該 Checkbox 的設定
現在,重新建置與執行這個專案,看看能否執行起來
喔喔,這樣的設定終於解除的當前危機,再度建立一個新的 Prism Black App 專案,看看是否會有同樣的效果,結果也是沒有問題。
另外一個解法則是剛剛測試出來的,不需要取消 使用 Fast Deployment (僅限偵錯模式) 這個選項設定,維持著 Fast Deployment 的設定,不過,在同一個標籤頁次下,也就是 Android 專案屬性視窗下的 Android 選項視窗中,往下捲動將會看到 正在連結 這個設定。
一開始使用 Prism Template Pack 建立起來的 Xamarin Android 專案中,其 正在連結 設定為 無 ,現在可以修正為 僅限 SDK 組譯碼 這個選項,我發現到可以維持在除錯模式下持續使用 Fast Deployment 的機制,也可以正常運作。



2019/06/26

如何在 Xamarin.iOS 專案內建立一個啟動畫面,並且可以自動在不同裝置下,橫向與執行模式下,自動調整 直向 Portrait / 橫向 Landscape

如何在 Xamarin.iOS 專案內建立一個啟動畫面,並且可以自動在不同裝置下,橫向與執行模式下,自動調整 直向 Portrait / 橫向 Landscape

想要讓 Xamarin.Forms 開發出來的專案,不會有 iOS 應用程式一起動,就會看到一片藍色的畫面,就需要根據這篇文章來建立一個啟動螢幕 Splash Screen
這篇文章的專案原始碼,可以從 Github 取得

準備工作與建立 Storeboard

  • 請先複製一張圖片檔案到 Resources 目錄下,這裡使用的片為 MyIcon.png
  • 使用滑鼠右鍵點選 Resources 目錄,選擇 [加入] > [新增項目]
  • 在 [新增項目] 對話窗中,選擇 [已安裝] > [Visual C#] > [Apple] 類別
  • 在該對話窗的中間選擇 [分鏡腳本空白] 這個選項
  • 在名稱欄位輸入 MySplash.storyboard
  • 最後點選 [新增] 按鈕
  • 此時該 Storyboard 檔案將會自動開啟,若此時該專案尚未連線到 Mac 電腦,則會產生錯誤訊息,此時,需要先連線到 Mac 電腦,再度重新打開這個 Storyboard 檔案

設計該 Storyboard 分鏡腳本

  • 若看不到如下圖左方的 [工具箱] 與 [文件大綱] 這兩個視窗,請從 Visual Studio 的檢視視窗中找到這兩個項目,打開這兩個視窗
  • 在 [工具箱] 視窗的搜尋文字輸入盒內,輸入 view 關鍵字,將會看到 View Controller 這個項目
  • 請點選這個選項,並且拖拉到 Visual Studio 的中間地方,並且放開滑鼠,完成建立一個 View Controller 的操作
  • 底下是完成後的操作螢幕截圖
  • 現在,請在工具箱的文字輸入盒內輸入 Im 文字,將會看到出現 Image View 控制項,請將這個圖片控制項拖拉到剛剛建立的 View Controller 上
  • 請在工具箱的文字輸入盒內輸入 lab 文字,將會看到出現 Label 控制項,請將這個文字控制項拖拉到剛剛建立的 View Controller 上
  • 剛剛這兩個控制項可以拖拉到該 View Controller 的任何地方,不過,這裡準備要設計的構想是,要將圖片放到螢幕的最上方,而該文字將會永遠出現在螢幕的下方,因此,這兩個控制項將會拖拉成為如下圖的樣貌。
  • 接下來要來設定圖片控制項
  • 請先點選該圖片控制項,在右方的 [屬性] 視窗中,將會看到 [Image] 屬性項目,請在此選擇剛剛托拉進來的圖片名稱
  • 在 [Content Mode] 屬性欄位中,選擇 [Center] 項目
  • 最後,回到原先的圖片按鈕,透過上下左右的四個點,來調整整個 圖片控制項的大小,並且將該圖片拖拉到儘量離螢幕上方一點,並且水平置中
    • 請點選文字控制項,設定他的文字內容為 [我的 Xamarin.Forms] 與字體大小和文字置中等設定,最後將該文字拖移到螢幕的下方
    • 現在,點選螢幕空白的地方,設定 View 控制項的背景為 綠色,最後的結果將會如下圖所示
    • 現在來看看這樣的設計在不同裝置與螢幕方向下,有沒有問題存在
    • 在 Storyboard 視窗的左下方,將會看到一個 [正在檢視 : iPhone 8 Plus - 橫向 - 寬 R 高壓縮] 這個文字,請點選這個項目。
    • 這裡將會看到不同的裝置,與最右方可以選擇直向或者橫向的顯示方式,在這裡先點選 橫向 選項,將會看到如下圖畫面,整個原先螢幕的下方被切割掉了,只剩下螢幕上方的圖片,現在請再度點選 直向 選項
  • 在該 Storyboard 視窗的右上方將會看到有三個圖示,請點選中間的 [限制式編輯模式] 按鈕
  • 再度點選圖片控制項,將會出現不同的設定節點項目
  • 請點選右方的 T 字型的節點圖示,拖拉該圖示到螢幕的左方,直到出現如下圖的藍色虛線才放開,這將會這點該圖片的限制約束條件;同樣的請設定左方的 T 字型節點圖示,拖拉到螢幕右方,,直到出現如下圖的藍色虛線才放開
  • 同樣的,請也設定上方的 T 字型的節點圖示,拖拉該圖示到螢幕的上方,直到出現如下圖的藍色虛線與有上版面配置輔助線區域才放開
  • 請點選右方的 I 字型的節點圖示,將會彈跳出一個小視窗,請選擇 [高度] 選項,同樣的操作,點選該圖片下方的 I 字型圖示,選擇 [寬度] 選項
  • 此時,可以來查看這樣的設計是否可以正常在 橫向 模式下來顯示,結果是沒有問題的,如下圖所示。
  • 現在,請將 Label 控制項也依據這樣的方式來設定
  • 請滑鼠雙擊 info.plist 這個檔案,點選該視窗的 [視覺資產] 標籤頁次,在下方的 [啟動畫面] 下拉選單中,選擇 [MySplash] 項目
  • 請執行這個專案,分別將裝置轉向成為 直向與橫向 模式,這樣的啟動畫面就不會跑版囉
     



2019/06/25

在 Xamarin.Forms 專案內,強制 iOS / Android 的裝置螢幕轉向 直向 Portrait / 橫向 Landscape

在 Xamarin.Forms 專案內,強制 iOS / Android 的裝置螢幕轉向 直向 Portrait / 橫向 Landscape

當在進行 Xamarin.Forms 專案開發的時候,有些時候需要能夠指定這個應用程式的在執行的時候,是可以使用直式 Portrait 螢幕方式來操作,還是限制只能夠使用橫式 Landscape 螢幕方式來操作,此時,若使用者旋轉其手機,螢幕是不會自動在直式與橫式螢幕上來切換的,完全被鎖定住了;像是這樣的情境,可以在原生專案內進行設定即可,一旦設定完成之後,整個應用程式的所有頁面,都會使用相同的模式來顯示與操作。
延續剛剛的設計,若這個應用程式在某個頁面,此時需要可以讓使用者自行決定要使用橫式或者是直式螢幕方式的操作,也就是要覆蓋原先應用程式的設定值;因為這些功能都需要能夠在原生專案下,使用原生 SDK API 進行呼叫,才能夠進行設定,所以,為了要能夠讓 Xamarin.Forms 的應用程式可以做到這樣的需求,在這個範例中將會使用 Prism 的 事件聚合器 Event Aggregator 這個機制,當在 Xamarin.Forms 的程式碼需要指定特殊需求的螢幕轉向 Orientation,就可以使用事件聚合器發出 Publish 特定事件,而在原生專案內,將會訂閱這個特定事件,所以,當這個事件被觸發之後,將會執行所指定的螢幕轉向的 API 呼叫。
這篇文章的專案原始碼可以從 GitHub 取得

Xamarin.Forms 的專案設計說明

在該 Xamarin.Forms 專案內,建立一個 ScreenOrientationPage.xaml 頁面,該頁面內將會有三個按鈕,這三個按鈕分別會指定這個應用程式在手機上是否可以
  • 自由旋轉 : 表示使用者可以旋轉螢幕,使用直式或者橫式螢幕的方式來操作
  • 限制直式 : 應用程式將會切換成為直式模式來操作,而且不能夠透過旋轉螢幕的方式,切換成為橫式模式。
  • 限制橫式 : 應用程式將會切換成為橫式模式來操作,而且不能夠透過旋轉螢幕的方式,切換成為直式模式。
xaml
<StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
    <Button Text="自由旋轉" 
            Command="{Binding OrientationCommand}" CommandParameter="自由旋轉"/>
    <Button Text="限制直式" 
            Command="{Binding OrientationCommand}" CommandParameter="限制直式"/>
    <Button Text="限制橫式" 
            Command="{Binding OrientationCommand}" CommandParameter="限制橫式"/>
</StackLayout>
而在 ScreenOrientationPage.xaml 頁面的 ViewModel 內,將會透過建構式注入的方式,注入一個 IEventAggregator 具體實作物件,這個物件將會用來發布一個透定的事件;另外將會建立一個 DelegateCommand<string> 這個型別的物件,用來設計當使用點選上述的任何一個按鈕,將需要執行相對應的程式碼,在此,三個按鈕都會綁定到同一個命令物件,並且使用 CommandParameter 的引數值來區分到底是按下了哪個按鈕。
當知道使用者按下了哪個按鈕,現在就可以透過 IEventAggregator 物件,取得特定的事件 ( CustomScreenOrientationEvent ) ,使用 Publish 方法來送出該事件,這裡使用的敘述為 eventAggregator.GetEvent<CustomScreenOrientationEvent>().Publish(customScreenOrientationPayload);,其中, Publish 方法所傳入的物件,將會是該事件所要傳遞的事件參數物件。
public ScreenOrientationPageViewModel(INavigationService navigationService,
    IEventAggregator eventAggregator)
{
    this.navigationService = navigationService;
    this.eventAggregator = eventAggregator;
    OrientationCommand = new DelegateCommand<string>(x =>
    {
        CustomScreenOrientationPayload customScreenOrientationPayload = new CustomScreenOrientationPayload();
        if (x == "自由旋轉")
        {
            customScreenOrientationPayload.CustomScreenOrientation = CustomScreenOrientation.Unspecified;
        }
        else if (x == "限制直式")
        {
            customScreenOrientationPayload.CustomScreenOrientation = CustomScreenOrientation.UserPortrait;
        }
        else
        {
            // 限制橫式
            customScreenOrientationPayload.CustomScreenOrientation = CustomScreenOrientation.UserLandscape;
        }
        eventAggregator.GetEvent<CustomScreenOrientationEvent>().Publish(customScreenOrientationPayload);
    });
}
對於 CustomScreenOrientationEvent 這個事件將會定義在底下的類別中,他需要繼承 PubSubEvent 這個類別,對於這個事件要被觸發的時候,可以透過 CustomScreenOrientationPayload 類別來指定要如何觸發這個事件,在訂閱該事件端,可以透過這個物件決定要如何執行相關程式碼;訂閱端的程式碼將會分別撰寫在 Android / iOS 這兩個原生專案內。
public enum CustomScreenOrientation
{
    UserPortrait,
    UserLandscape,
    Unspecified
}
public class CustomScreenOrientationEvent : PubSubEvent<CustomScreenOrientationPayload>
{
}
public class CustomScreenOrientationPayload
{
    public CustomScreenOrientation CustomScreenOrientation { get; set; }
}

如何指定該應用程式預設的螢幕轉向設定

現在,將要來針對 Android 專案進行設定,讓這個 Xamarin.Forms App 只能夠使用直式螢幕顯示的方式來操作。 請打開 Android 專案內的 MainActivity.cs 檔案,找到 class MainActivity 類別的定義程式碼的上方,將會看到 Activity 屬性的宣告,請在最後面加上 ScreenOrientation = ScreenOrientation.Portrait 的設定,這樣表示這個應用程式在執行的時候,不管手機是使用何種方向來操作,都是使用直式螢幕的方式來顯示,也就是會如圖同底下的螢幕截圖效果。
[Activity(Label = "ForceOrientation", Icon = "@mipmap/ic_launcher",
    Theme = "@style/MainTheme", MainLauncher = true,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation,
    ScreenOrientation = ScreenOrientation.Portrait)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
    protected override void OnCreate(Bundle bundle)
    {
        TabLayoutResource = Resource.Layout.Tabbar;
        ToolbarResource = Resource.Layout.Toolbar;
        base.OnCreate(bundle);
        global::Xamarin.Forms.Forms.Init(this, bundle);
        LoadApplication(new App(new AndroidInitializer()));
    }
}
對於 iOS 專案,請使用這個設定方式,讓這個 Xamarin.Forms App 在 iOS 系統下執行的時候,只能夠使用直式螢幕顯示的方式來操作。 請打開 iOS 專案內的 Info.plist 檔案,這裡將會是使用 XML 檢視方式來編輯;請找到 UISupportedInterfaceOrientations,請將該文字後面的 <array>...</array> 內容,修改成為只有 <string>UIInterfaceOrientationPortrait</string> 這個宣告項目,這樣表示這個應用程式在執行的時候,不管手機是使用何種方向來操作,都是使用直式螢幕的方式來顯示,也就是會如圖同底下的螢幕截圖效果。
當然,也可以使用滑鼠雙擊 Info.plist 這個檔案節點,此時會出現視覺化的設定畫面,如下圖所示,在這裡可以在 [裝置方向] 的地方,只要勾選 [直向] 這個選項即可,其他的不用勾選,也是可以做到如上面設定的相同效果。
<key>UISupportedInterfaceOrientations</key>
<array>
    <string>UIInterfaceOrientationPortrait</string>
</array>

由程式來控制螢幕要顯示的方向,到底是直向還是橫向

首先來看看 Android 的專案,同樣的打開 MainActivity.cs 這個檔案,將會新設計一個方法 SubscribePrismEvent(),在這個方法內,將會透過 App.Current.Container 取得 Prism 的 DI Container 容器物件,接著透過該 containerProvider 物件,解析出 IEventAggregator 的具體實作物件出來,如此,便可以透過 eventAggregator.GetEvent<CustomScreenOrientationEvent>().Subscribe 方法來訂閱 CustomScreenOrientationEvent 這個事件,也就是說,當這個事件有被 Publish 的時候,將會觸發這個 Subscribe 的委派方法。
在這個訂閱事件委派方法內,依據該事件傳送進來的 CustomScreenOrientationPayload 物件值,設定 RequestedOrientation 這個屬性值,這樣,螢幕就會依據所指定的條件,自動進行轉向了。最後,請在 OnCreate() 方法內,加入呼叫 SubscribePrismEvent(); 這個方法即可。
例如,在此點選 限制橫式 按鈕,就會將正在以直向顯示的螢幕內容,自動轉換成為橫向顯示了,如下圖所示。
[Activity(Label = "ForceOrientation", Icon = "@mipmap/ic_launcher",
    Theme = "@style/MainTheme", MainLauncher = true,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation,
    ScreenOrientation = ScreenOrientation.Portrait)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
    protected override void OnCreate(Bundle bundle)
    {
        TabLayoutResource = Resource.Layout.Tabbar;
        ToolbarResource = Resource.Layout.Toolbar;

        base.OnCreate(bundle);

        global::Xamarin.Forms.Forms.Init(this, bundle);
        LoadApplication(new App(new AndroidInitializer()));
        SubscribePrismEvent();
    }

    public void SubscribePrismEvent()
    {
        IContainerProvider containerProvider = App.Current.Container;
        IEventAggregator eventAggregator = containerProvider.Resolve<IEventAggregator>();
        eventAggregator.GetEvent<CustomScreenOrientationEvent>().Subscribe(x =>
        {
            if (x.CustomScreenOrientation == CustomScreenOrientation.Unspecified)
            {
                RequestedOrientation = ScreenOrientation.Unspecified;
            }
            else if (x.CustomScreenOrientation == CustomScreenOrientation.UserPortrait)
            {
                RequestedOrientation = ScreenOrientation.Portrait;
            }
            else
            {
                RequestedOrientation = ScreenOrientation.Landscape;
            }
        });
    }

}
接下來看看如何在 iOS 平台下做到由程式來控制螢幕轉向,請打開 AppDelegate.cs 這個檔案,將會新設計一個方法 SubscribePrismEvent(),在這個方法內,將會透過 App.Current.Container 取得 Prism 的 DI Container 容器物件,接著透過該 containerProvider 物件,解析出 IEventAggregator 的具體實作物件出來,如此,便可以透過 eventAggregator.GetEvent<CustomScreenOrientationEvent>().Subscribe 方法來訂閱 CustomScreenOrientationEvent 這個事件,也就是說,當這個事件有被 Publish 的時候,將會觸發這個 Subscribe 的委派方法。
在這個訂閱事件委派方法內,依據該事件傳送進來的 CustomScreenOrientationPayload 物件值,該類別內新增的一個屬性,那就是 ScreenOrientation ,使其成為該事件所期望的螢幕轉向方向,而在 SubscribePrismEvent() 方法內,最後有呼叫 UIViewController.AttemptRotationToDeviceOrientation();敘述,將會嘗試要將螢幕進行轉向的相關程式碼執行;另外,需要覆寫 public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, [Transient] UIWindow forWindow) 這個方法,在此方法內,依據 ScreenOrientation 屬性值,回傳需要的螢幕轉向設定值。。最後,請在 FinishedLaunching() 方法內,加入呼叫 SubscribePrismEvent(); 這個方法即可。
例如,在此點選 限制橫式 按鈕,就會將正在以直向顯示的螢幕內容,自動轉換成為橫向顯示了,如下圖所示。
// The UIApplicationDelegate for the application. This class is responsible for launching the 
// User Interface of the application, as well as listening (and optionally responding) to 
// application events from iOS.
[Register("AppDelegate")]
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
    CustomScreenOrientation ScreenOrientation = CustomScreenOrientation.UserPortrait;
    //
    // This method is invoked when the application has loaded and is ready to run. In this 
    // method you should instantiate the window, load the UI into it and then make the window
    // visible.
    //
    // You have 17 seconds to return from this method, or iOS will terminate your application.
    //
    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    {
        global::Xamarin.Forms.Forms.Init();
        LoadApplication(new App(new iOSInitializer()));
        SubscribePrismEvent();
        return base.FinishedLaunching(app, options);
    }
    public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, [Transient] UIWindow forWindow)
    {
        if (ScreenOrientation == CustomScreenOrientation.Unspecified)
        {
            return UIInterfaceOrientationMask.All;
        }
        else if (ScreenOrientation == CustomScreenOrientation.UserLandscape)
        {
            return UIInterfaceOrientationMask.Landscape;
        }
        else
        {
            return UIInterfaceOrientationMask.Portrait;
        }
    }
    public void SubscribePrismEvent()
    {
        IContainerProvider containerProvider = App.Current.Container;
        IEventAggregator eventAggregator = containerProvider.Resolve<IEventAggregator>();
        eventAggregator.GetEvent<CustomScreenOrientationEvent>().Subscribe(x =>
        {
            ScreenOrientation = x.CustomScreenOrientation;
            NSNumber n ;
            NSString key = new NSString("orientation");
            if (x.CustomScreenOrientation == CustomScreenOrientation.Unspecified)
            {
                n = new NSNumber((int)UIInterfaceOrientation.Unknown);
            }
            else if (x.CustomScreenOrientation == CustomScreenOrientation.UserPortrait)
            {
                n = new NSNumber((int)UIInterfaceOrientation.Portrait);
            }
            else
            {
                n = new NSNumber((int)UIInterfaceOrientation.LandscapeLeft);
            }
            UIDevice.CurrentDevice.SetValueForKey(
  new NSNumber((int)n),
  new NSString("orientation"));
            UIViewController.AttemptRotationToDeviceOrientation();
        });
    }
}




2019/06/18

如何使用 On_PropertyName_Changed 與 附加屬性 Attached Properties 和行為 Behavior,來得知 Entry 與 Editor 已經輸入了多少文字

如何使用 On_PropertyName_Changed 與 附加屬性 Attached Properties 和行為 Behavior,來得知 Entry 與 Editor 已經輸入了多少文字

當在進行 Xamarin.Forms 專案開發的時候,有些時候需要得知 Entry 或者 Editor 這兩種檢視或者稱為控制向,使用者已經輸入了多少內容或者字數,通常可以透過這兩個控制項的 Entry.TextChanged 事件或者 Editor.TextChanged 事件來得知使用者已經輸入了甚麼內容,這樣就可以計算出使用者已經輸入多少文字內容,可是,若要使用事件的話,就需要使用該頁面的 Code Behind 的方式來撰寫 C# 事件委派方法,讓使用者輸入文字的時候,可以觸發這個事件,以便進行計算字數工作。
在使用 Prism 這類 MVVM 開發框架的設計模式的時候,幾乎所有的商業邏輯都會撰寫到 View (頁面) 的 ViewModel (檢視模型) 上,如此,要如何得知使用者已經在 Entry / Editor 檢視中輸入甚麼內容呢?這裡將會提供底下方式來解決此一需求。
這篇文章的範例程式原始碼,可以從 GitHub 取得。

PropertyChanged.Fody 套件提供的 屬性 變更觸發事件

在 PropertyChanged.Fody 的網頁中,將會有一篇關於 On_PropertyName_Changed 文章,告訴 Xamarin.Forms 開發者如何在 ViewModel 中來撰寫出當所綁定的屬性值有異動的時候,將會觸發一個 ViewModel 上的委派方法。
在此應用中,將會在頁面中使用底下 XAML 進行宣告一個 Entry 檢視,其中,當使用者輸入的文字內容,將會自動綁定到 ViewModel 類別內的 C# Property 屬性 EntryChangedInputText 上,而 ViewModel 類別內的 C# Property 屬性 EntryChangedInputTextLength 將會是該輸入文字的字數數值。
xaml
        <Label Text="{Binding EntryChangedInputTextLength}"/>
        <Entry
            Text="{Binding EntryChangedInputText}"/>
現在,請在該頁面的檢視模型 ViewModel 中,定義一個方法,其方法名稱必須為 On屬性名稱Changed ,在這裡將會定義一個 OnEntryChangedInputTextChanged 方法,當這個 Xamarin.Forms 專案執行的時候,使用者在該 Entry 輸入任何文字,將會觸發 OnEntryChangedInputTextChanged 方法執行,在此方法內,將會撰寫符合需求的計算字數商業邏輯程式碼,計算完成之後,就可以儲存到 EntryChangedInputTextLength 屬性內,如此,就可以在螢幕上看到該輸入字數數量了。
        public string EntryChangedInputText { get; set; }
        public int EntryChangedInputTextLength { get; set; } 
        public void OnEntryChangedInputTextChanged()
        {
            if(string.IsNullOrEmpty(EntryChangedInputText))
            {
                EntryChangedInputTextLength = 0;
            }
            else
            {
                EntryChangedInputTextLength = EntryChangedInputText.Length;
            }
        }

使用 Prism 提供的事件 To 命令的行為擴充功能

Prism 這個 MVVM 開發框架,提供了一個 Xamarin.Forms Behavior 行為的物件,其中一個加值行為就是可以設定當某個事件被觸發的時候,需要執行某個命令;如此,使用這個方法將會把相關商業處理邏輯寫道 ViewModel 上,而不在需要撰寫 Code Behind 程式碼了。
首先,請先在該 ContentPage 節點上宣告一個新的命名空間 xmlns:behavior="clr-namespace:Prism.Behaviors;assembly=Prism.Forms" ,這裡將會宣告一個前置詞 Prefix , behavior ,指向 Prims 行為擴充功能組件上,現在,可以在這個頁面上,加入一個 Label 與 Entry,其中 Entry 需要使用 Event.Behaviors 的屬性項目 Property Element 方式,指定新加入的行為物件;這裡將會使用 behavior:EventToCommandBehavior 這個型別,來使用 EventName 這個屬性來指定觸發事件的名稱與 Command 來指定要綁定到 ViewModel 中的一個 DelegateCommand 屬性物件。
xaml
        <Label Text="{Binding EntryBehaviorInputTextLength}"/>
        <Entry
            Text="{Binding EntryBehaviorInputText}">
            <Entry.Behaviors>
                <behavior:EventToCommandBehavior
                    EventName="TextChanged" Command="{Binding EntryChangedCommand}"/>
            </Entry.Behaviors>
        </Entry>
在 ViewModel 內,會在建構函式內建立一個 DelegateCommand 型別的物件到 EntryChangedCommand 屬性上,在建立這個 EntryChangedCommand 屬性的時候,將會傳入一個委派方法,這個委派方法將會用來轉寫計算使用者輸入字數的相關程式碼。
        public string EntryBehaviorInputText { get; set; }
        public int EntryBehaviorInputTextLength { get; set; }
        public DelegateCommand EntryChangedCommand { get; set; }
        public MainPageViewModel(INavigationService navigationService)
        {
            this.navigationService = navigationService;
            EntryChangedCommand = new DelegateCommand(() =>
            {
                EntryBehaviorInputTextLength = EntryBehaviorInputText is null ? 0 : EntryBehaviorInputText.Length;
            });
        }

使用附加屬性來計算使用輸入的文字數量

對於想要針對某個控制項加入可以綁定的屬性,讓 ViewModel 可以根據 View 的屬性異動,進行相關的商業邏輯處理,第一個就是使用原來檢視上的屬性來做處理,若有特殊需求,需要額外的屬性,可以建立一個新類別,繼承原來使用的檢視類別,就可以在這個新控制項中加入一個新的可綁定屬性;第二個方式,就是使用附加屬性,針對想要使用該附加屬性來附加這些附加屬性。
最後一個,就是要建立兩個附加屬性,第一個是 EnableCharCount ,這是用來設定是否要啟用自動計算字數的附加屬性,第二個是 EditorInputTextLength 用來儲存使用者輸入字數的數量值,底下為設計出來的附加屬性 XAML 用法。
由於等下要設計的附加屬性將會限制這兩個附加屬性僅能夠套用到 Entry 或者 Editor 上,當這兩個附加屬性附加到其他檢視的時候,將不會有任何效果的。
這裡有一點要特別注意,對於 CharCountAttachedProperty.CharNumber 這個附加屬性需要在資料綁定延伸標記語法內,要使用 Mode=TwoWay ,這樣才會形成雙向綁定運作方式。
xaml
        <Label Text="{Binding EditorInputTextLength}"/>
        <Label Text="{Binding EntryInputTextLength}"/>
        <Editor
            Text="{Binding EditorInputText}"
            AutoSize="TextChanges"
            local:CharCountAttachedProperty.EnableCharCount="True"
            local:CharCountAttachedProperty.CharNumber="{Binding EditorInputTextLength, Mode=TwoWay}"
            />
        <Entry
            Text="{Binding EntryInputText}"
            local:CharCountAttachedProperty.EnableCharCount="True"
            local:CharCountAttachedProperty.CharNumber="{Binding EntryInputTextLength, Mode=TwoWay}"/>
現在,可以建立一個類別,在此類別上建立兩個附加屬性,在此,建立的類別名稱為 CharCountAttachedProperty,接著,在此類別內,使用程式碼片段 xfAttachedProperty 自動產生附加屬性的相關程式碼。
第一個附加屬性為 EnableCharCountProperty ,在這裡附加屬性將會用來控制是否要進行輸入文字內容的計算工作,在呼叫 BindableProperty.CreateAttached 方法的時候,將會使用 declaringType 來指定這個附加屬性可以附加到那些 XAML 項目上,這裡指定的是 typeof(View),代表任何使用者控制項都可以使用這個附加屬性。
在 EnableCharCountProperty 內最為重要的工作那就是要定義 propertyChanged 這個參數值,傳入一個委派方法 OnEnableCharCountChanged,在此委派方法終將會使用 if (bindable is Entry || bindable is Editor) 來檢查現在該附加屬性是否用在 Entry 或者 Editor 這兩個檢視上,若不是的話,則不會做任何處理。所附加的檢視為 Entry 或者 Editor ,將會檢查是否要啟用這個自動計算字數的屬性值,若為 True,則會訂閱 Entry.TextChanged 事件或者 Editor.TextChanged 事件,否則若為 False,則會取消 TextChanged 的事件訂閱。
記得,當訂閱視覺項目的事件的時候,一定要規劃解除訂閱事件的程式碼,否則,會有可能產生 記憶體遺失 Memory Leak 的問題。
在 OnEntryTextChanged 方法內,將會進行該自動計算使用者輸入文字的字數計算,將結果儲存到 CharNumberProperty 這個附加屬性上。
最後,還是使用相同的程式碼片段, xfAttachedProperty 自動建立 CharNumberProperty 附加屬性程式碼,而在這個 CharNumberProperty 附加屬性上,並不需要加入額外的其他城市碼。
public class CharCountAttachedProperty
{
    #region EnableCharCount 附加屬性 Attached Property
    public static readonly BindableProperty EnableCharCountProperty =
           BindableProperty.CreateAttached(
               propertyName: "EnableCharCount",   // 屬性名稱 
               returnType: typeof(bool), // 回傳類型 
               declaringType: typeof(View), // 宣告類型 
               defaultValue: false, // 預設值 
               propertyChanged: OnEnableCharCountChanged  // 屬性值異動時,要執行的事件委派方法
           );

    public static void SetEnableCharCount(BindableObject bindable, bool entryType)
    {
        bindable.SetValue(EnableCharCountProperty, entryType);
    }
    public static bool GetEnableCharCount(BindableObject bindable)
    {
        return (bool)bindable.GetValue(EnableCharCountProperty);
    }

    private static void OnEnableCharCountChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is Entry || bindable is Editor)
        {
            bool isEnable = (bool)newValue;
            if (isEnable)
            {
                if (bindable is Entry)
                {
                    (bindable as Entry).TextChanged += OnEntryTextChanged;
                }
                else if (bindable is Editor)
                {
                    (bindable as Editor).TextChanged += OnEntryTextChanged;
                }
            }
            else
            {
                if (bindable is Entry)
                {
                    (bindable as Entry).TextChanged -= OnEntryTextChanged;
                }
                else if (bindable is Editor)
                {
                    (bindable as Editor).TextChanged -= OnEntryTextChanged;
                }
            }
        }
        else
        {

        }
    }
    #endregion
    private static void OnEntryTextChanged(object sender, TextChangedEventArgs e)
    {
        int foo = CharCountAttachedProperty.GetCharNumber(sender as BindableObject);
        int length = e.NewTextValue.Length;
        CharCountAttachedProperty.SetCharNumber(sender as BindableObject, length);
        (sender as BindableObject).SetValue(CharNumberProperty, length);
        int foo2 = CharCountAttachedProperty.GetCharNumber(sender as BindableObject);
    }

    #region CharNumber 附加屬性 Attached Property
    public static readonly BindableProperty CharNumberProperty =
           BindableProperty.CreateAttached(
               propertyName: "CharNumber",   // 屬性名稱 
               returnType: typeof(int), // 回傳類型 
               declaringType: typeof(View), // 宣告類型 
               defaultValue: 0, // 預設值 
               propertyChanged: OnCharNumberChanged  // 屬性值異動時,要執行的事件委派方法
           );

    public static void SetCharNumber(BindableObject bindable, int entryType)
    {
        bindable.SetValue(CharNumberProperty, entryType);
    }
    public static int GetCharNumber(BindableObject bindable)
    {
        return (int)bindable.GetValue(CharNumberProperty);
    }

    private static void OnCharNumberChanged(BindableObject bindable, object oldValue, object newValue)
    {
    }

    #endregion

}