XAML in Xamarin.Forms 基礎篇 電子書

XAML in Xamarin.Forms 基礎篇 電子書
XAML in Xamarin.Forms 基礎篇 電子書

Xamarin.Forms 快速入門 電子書

Xamarin.Forms 快速入門 電子書
Xamarin.Forms 快速入門 電子書

2017/04/24

簡化 MVVM 的綁定屬性設計

當我們在進行 MVVM 開發設計的時候,在 ViewModel 內需要在 View 中綁定的屬性,都要實作 INotifyPropertyChanged 這個事件;當我們在使用 Prism 開發框架的時候,雖然,提供了一個 BindableBase 類別,來幫助我們實作出 INotifyPropertyChange 事件,可是,在宣告 ViewModel 的屬性時候,還是有些麻煩。

了解更多關於 [Xamarin.Android] 的使用方式
了解更多關於 [Xamarin.iOS] 的使用方式
了解更多關於 [Xamarin.Forms] 的使用方式
了解更多關於 [Hello, Android:快速入門] 的使用方式
了解更多關於 [Hello, iOS – 快速入門] 的使用方式
了解更多關於 [Xamarin.Forms 快速入門] 的使用方式

因此,PropertyChanged.Fody 這個 NuGet 套件出現了,它簡化了 ViewModel 的設計。在官方文件中舉個例子:
在這裡,我們僅需要在 ViewModel 類別中,使用了 ImplementPropertyChanged 這個屬性宣告,在整個 ViewModel 內,每個要綁定到 View 中的屬性,僅需要使用 .NET Property 的方式來宣告即可。
[ImplementPropertyChanged]
public class Person 
{        
    public string GivenNames { get; set; }
    public string FamilyName { get; set; }

    public string FullName
    {
        get
        {
            return string.Format("{0} {1}", GivenNames, FamilyName);
        }
    }
}
PropertyChanged.Fody 會在編譯時期,自動為我們產生出相對應的 INotifyPropertyChanged 事件實作,如同底下程式碼所示:
public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    string givenNames;
    public string GivenNames
    {
        get { return givenNames; }
        set
        {
            if (value != givenNames)
            {
                givenNames = value;
                OnPropertyChanged("GivenNames");
                OnPropertyChanged("FullName");
            }
        }
    }

    string familyName;
    public string FamilyName
    {
        get { return familyName; }
        set 
        {
            if (value != familyName)
            {
                familyName = value;
                OnPropertyChanged("FamilyName");
                OnPropertyChanged("FullName");
            }
        }
    }

    public string FullName
    {
        get
        {
            return string.Format("{0} {1}", GivenNames, FamilyName);
        }
    }

    public virtual void OnPropertyChanged(string propertyName)
    {
        var propertyChanged = PropertyChanged;
        if (propertyChanged != null)
        {
            propertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
傑克,這是不是太神奇了呀,看到這個套件,我立馬想要來在 Prism 框架中來實作、測試看看。
我建立了一個 View ,這個頁面中,有兩個資料綁定的屬性,與一個命令綁定屬性。
<?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="PrismUnityApp7.Views.MainPage"
             Title="MainPage">
    <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
        <Label Text="{Binding Title}" />
        <Entry Text="{Binding entry}" />
        <Button Text="Set" Command="{Binding SetCommand}"/>
    </StackLayout>
</ContentPage>
此時,我的 ViewModel 變得清爽多了;經過執行,還真的可以運作。
    [ImplementPropertyChanged]
    public class MainPageViewModel : INavigationAware
    {
        public string Title { get; set; }
        public string entry { get; set; }

        public DelegateCommand SetCommand { get; set; }

        public MainPageViewModel()
        {
            SetCommand = new DelegateCommand(() =>
            {
                Title = entry;
            });
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public void OnNavigatingTo(NavigationParameters parameters)
        {

        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {
            if (parameters.ContainsKey("title"))
                Title = (string)parameters["title"] + " and Prism";
        }
    }
sharp
有興趣的人,可以參可 PropertyChanged.Fody

2017/04/22

Xamarin FAQ 2-28 : 使用 StackLayout, 控制項之間會有空白

問題

當您使用 StackLayout 版面配置,裡面設定了多個控制項,不論採用水平或者垂直排列方式,您都會發現到各控制項中,都會保留一定的空白空間,可是,我該如何移除這些空間呢?

解答

Xamarin.Forms 的 StackLayout 預設在各控制項間預留 6 個單位,因此,若您不想要這些保留空間,您可以在 StackLayout 內,使用 Spacing="0" 就可以消除這些預留空白了
Xamarin-跨平台手機應用程式設計入門-粉絲團

Xamarin FAQ 2-27 : 在XAML內,設定多個列舉值

問題

有些 XAML 的屬性是個列舉,可以設定多個列舉值,例如, FontAttributes 就是其中一列,可是,當我需要在 XAML 中定義多個列舉值的時候,我該如何定義呢?

解答

您可以參考底下用法,多個列舉值,可以使用逗號 , 將其分開即可
        <Label Text="多奇數位創意有限公司" FontAttributes="Italic,Bold"/>
Xamarin-跨平台手機應用程式設計入門-粉絲團

2017/04/21

使用 Xamarin + Azure 無法進行 Facebook 身分驗證

最近,公司有個專案需要提供 Facebook 身分驗證的機制,當然,就會想到使用 Xamarin.Auth 這個套件來使用;我之前在 Xamarin 工作紡電子書中,也有交代如何透過 Xamarin.Auth ,進行市面上一般社群網站的 oAuth2 的身分驗證開發方法。
可是,很不幸的,不論是依據電子書所交代的步驟,或者直接開啟墊子書中所提供的範例專案,都無法進行身分驗證。
問題發生在,當輸入完成 Facebook 的帳號與密碼之後,因為是第一次使用,需要需要同意使用 Facebook App,不過,此時,畫面是被凍結住的,在這個網頁中,您無法點選任何操作與按鈕。
這個時候,當然會想到,會不會是 Xamarin.Auth 這個套件出了問題,不過,我之前也寫過了使用 Azure Mobile App 的身分驗證電子書,理所當然的,就會打開該電子書的範例專案來測試看看;不幸的是,兩者的情況都是一樣,網頁被凍結住了。
經過一番調查,看到這篇文章:
原來又是 Facebook 動了手腳,只有在 iOS 的環境中,無法正常地進行 Facebook 的身分驗證,而同樣的程式,在 Android 平台下,卻是可以正常運作的。
因此,按照該篇文章的指示,進行調整 Azure Mobile 的 MOBILESERVICESDOTNET_EXTENSION_VERSION 服務,最後的情況,當然也是一樣的,畫面被凍結了。
Update the MOBILESERVICESDOTNET_EXTENSION_VERSION value from 1.0.478 to 1.0.479.
這個時候,突然想到9年以前處理的問題,便針對 iOS 的 WebKit 調整他的 User-Agent Webviews and User-Agent strings ,將 iOS 環境中的 WebKit 瀏覽器的 User-Agent 調整成為 Facebook for iOS User-Agent string : Mozilla/5.0 (iPhone; CPU iPhone OS 8_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12D508 [FBAN/FBIOS;FBAV/27.0.0.10.12;FBBV/8291884;FBDV/iPhone7,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/8.2;FBSS/3; FBCR/vodafoneIE;FBID/phone;FBLC/en_US;FBOP/5]
底下為設定 WebKit User-Agent 的方法
string userAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 8_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12D508 [FBAN/FBIOS;FBAV/27.0.0.10.12;FBBV/8291884;FBDV/iPhone7,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/8.2;FBSS/3; FBCR/vodafoneIE;FBID/phone;FBLC/en_US;FBOP/5]";

NSDictionary dictionary = NSDictionary.FromObjectAndKey(NSObject.FromObject(userAgent), NSObject.FromObject("UserAgent"));
NSUserDefaults.StandardUserDefaults.RegisterDefaults(dictionary);
最後,當然是如期解決了問題,iOS 平台下,可以使用 Azure Mobile App 來進行 Facebook 的身分驗證了。

Xamarin應用程式,一執行就閃退的解決方案

在這個星期,遇到兩次這樣的問題,花了一些時間來處理這類問題,不過,其實這樣的問題,有一個比較簡潔的處理方式。

了解更多關於 [Xamarin.Android] 的使用方式
了解更多關於 [Xamarin.iOS] 的使用方式
了解更多關於 [Xamarin.Forms] 的使用方式
了解更多關於 [Hello, Android:快速入門] 的使用方式
了解更多關於 [Hello, iOS – 快速入門] 的使用方式
了解更多關於 [Xamarin.Forms 快速入門] 的使用方式
當您的應用程式一啟動之後,就發現會自然的閃退,這個時候,若您有設定了中斷點,不論您設定在哪個地方(原生專案或者核心PCL專案內),似乎,您的程式都無法在這些中斷點停下來,還是會自動閃退。
通常來說,您會看到底下的錯誤訊息
Objective-C exception thrown.  Name: NSInternalInconsistencyException Reason: Application windows are expected to have a root view controller at the end of application launch
不論您的程式是剛剛開始進行開發,還是已經寫了很多內容了,這個時候,請都不要緊張,我強烈建議您採用 刪去法 開始進行這類問題除錯。
首先,請先建立一個空白的 ContentPage,並且在 App.xaml.cs 內,指定 MainPage 到這個 ContentPage;並且,再度執行一次,看看是否可以正常運作。
若您還是遇到不幸的情況,請在原生專案的進入點方法內,看看是否有執行甚麼初始化的方法,若有的話,請記得將其註解起來;接著再度執行一次,看看能否正常執行。
經過這樣幾次,不斷地將程式碼刪去之後,應該可以找到問題所在。
我遇到的兩個情況分別是:
  1. XAML 語法錯誤,導致執行時期,要顯示這個頁面的時候,造成執行時期的閃退。
  2. 有個靜態屬性,當進行物件值初始化的時候,發生了異常,導致程式直接閃退;當然,這個類別內的其他靜態屬性物件,也無法被存取了。

iOS 取消 ATS App Transport Security 限制

預設,Xamarin.iOS 的App,需要使用 Https 來進行網路存取,可是,有些時候,您期望還是要使用不加密的方式來存取網路;這個時候,您可以在 Info.plist 檔案內,加入底下的設定:
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>
更多這方面的說明,可以參考 App Transport Security

如何清空 Mac 電腦中的 Provisioning Profile 快取資料

有些時候,若您修改的 Provisioning Profile 設定資訊後,發現到在 Windows 端的 Visual Studio 沒有辦法取得最新的 Provisioning Profile 設定資訊。
有個終極作法,那就是,在 Mac 電腦端,把這些快取的 Provisioning Profile 檔案先全部刪除掉,接著,再透過 Xcode 將其全部從 Apple Developer 網站中下載下來。
您可以切換到底下目錄,將這個目錄下的所有物件全部都刪除掉即可
~/Library/MobileDevice/Provisioning\ Profiles

2017/04/17

Xamarin.Forms 與 Azure Mobile App Lab 13

在 Xamarin.Forms 專案,修正進行 Azure 行動應用推播中樞註冊

在這裡,我們將要把我們寫好的 Xamarin.Forms 專案,設定使用 Azure 行動應用的推播服務功能,也就是要在應用程式啟動時候,與 Azure 行動應用的推播中樞進行註冊,並且可以接收與顯示推播訊息內容。

第一次準備工作

在您原先做好的 Xamarin.Forms 專案中,需要加入 Google Cloud Messaging Client 元件

加入 Google Cloud Messaging Client 元件

  • 使用 Visual Studio 2015 打開 XFDoggy.sln 專案
  • 滑鼠右擊 原生Android專案的 Components,選擇 Get More Components
  • 在搜尋文字輸入盒內,填入 Google Cloud Messaging Client 搜尋這個元件
  • 點選 Google Cloud Messaging Client 元件,接著點選 Add to App 按鈕

修改 原生 Android 專案

在這裡,我們僅需要修正 原生 Android 專案,讓 Android 應用程式可以與行動中樞溝通。

設計自己的 GcmServiceBase 類別

  • 滑鼠右擊 原生Android專案中的 Infrastructure 資料夾,選擇 加入 > 類別
  • 在對話窗中,點選 Visual C# > Class
  • 在名稱欄位中,輸入 GcmService
  • 最後,點選 新增 按鈕
  • 將底下程式碼置換這個 GcmService 類別
    請解析要加入的適當命名空間
  • 請將這行程式碼 public static string[] SENDER_IDS = new string[] { "300143732939" }; 的 300143732939 文字,置換成為您申請到的 FCM 寄件者 ID
using Android.App;
using Android.Content;
using Android.Media;
using Android.Support.V4.App;
using Android.Util;
using Gcm.Client;
using Microsoft.WindowsAzure.MobileServices;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using XFDoggy.Helpers;

[assembly: Permission(Name = "@PACKAGE_NAME@.permission.C2D_MESSAGE")]
[assembly: UsesPermission(Name = "@PACKAGE_NAME@.permission.C2D_MESSAGE")]
[assembly: UsesPermission(Name = "com.google.android.c2dm.permission.RECEIVE")]
[assembly: UsesPermission(Name = "android.permission.INTERNET")]
[assembly: UsesPermission(Name = "android.permission.WAKE_LOCK")]
//GET_ACCOUNTS is only needed for android versions 4.0.3 and below
[assembly: UsesPermission(Name = "android.permission.GET_ACCOUNTS")]

namespace XFDoggy.Droid.Infrastructure
{
    [Service]
    public class GcmService : GcmServiceBase
    {
        public static string RegistrationID { get; private set; }

        public GcmService()
            : base(PushHandlerBroadcastReceiver.SENDER_IDS) { }

        protected override void OnRegistered(Context context, string registrationId)
        {
            Log.Verbose("PushHandlerBroadcastReceiver", "GCM Registered: " + registrationId);
            RegistrationID = registrationId;

            var push = MainHelper.client.GetPush();

            MainActivity.CurrentActivity.RunOnUiThread(() => Register(push, null));
        }

        public async void Register(Microsoft.WindowsAzure.MobileServices.Push push, IEnumerable<string> tags)
        {
            try
            {
                const string templateBodyGCM = "{\"data\":{\"message\":\"$(messageParam)\"}}";

                JObject templates = new JObject();
                templates["genericMessage"] = new JObject
                {
                    { "body", templateBodyGCM}
                };

                await push.RegisterAsync(RegistrationID, templates);
                Log.Info("Push Installation Id", push.InstallationId.ToString());
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.Message);
                Debugger.Break();
            }
        }

        protected override void OnMessage(Context context, Intent intent)
        {
            Log.Info("PushHandlerBroadcastReceiver", "GCM Message Received!");

            var msg = new StringBuilder();

            if (intent != null && intent.Extras != null)
            {
                foreach (var key in intent.Extras.KeySet())
                    msg.AppendLine(key + "=" + intent.Extras.Get(key).ToString());
            }

            //Store the message
            var prefs = GetSharedPreferences(context.PackageName, FileCreationMode.Private);
            var edit = prefs.Edit();
            edit.PutString("last_msg", msg.ToString());
            edit.Commit();

            string message = intent.Extras.GetString("message");
            if (!string.IsNullOrEmpty(message))
            {
                createNotification("多奇數位創意通知", message);
                return;
            }

            string msg2 = intent.Extras.GetString("msg");
            if (!string.IsNullOrEmpty(msg2))
            {
                createNotification("New hub message!", msg2);
                return;
            }

            createNotification("Unknown message details", msg.ToString());
        }

        void createNotification(string title, string desc)
        {
            //Create notification
            var notificationManager = GetSystemService(Context.NotificationService) as NotificationManager;

            //Create an intent to show ui
            var uiIntent = new Intent(this, typeof(MainActivity));

            //Use Notification Builder
            NotificationCompat.Builder builder = new NotificationCompat.Builder(this);

            //Create the notification
            //we use the pending intent, passing our ui intent over which will get called
            //when the notification is tapped.
            var notification = builder.SetContentIntent(PendingIntent.GetActivity(this, 0, uiIntent, 0))
                    .SetSmallIcon(Android.Resource.Drawable.SymActionEmail)
                    .SetTicker(title)
                    .SetContentTitle(title)
                    .SetContentText(desc)

                    //Set the notification sound
                    .SetSound(RingtoneManager.GetDefaultUri(RingtoneType.Notification))

                    //Auto cancel will remove the notification once the user touches it
                    .SetAutoCancel(true).Build();

            //Show the notification
            notificationManager.Notify(1, notification);
        }

        protected override void OnUnRegistered(Context context, string registrationId)
        {
            Log.Error("PushHandlerBroadcastReceiver", "Unregistered RegisterationId : " + registrationId);
        }

        protected override void OnError(Context context, string errorId)
        {
            Log.Error("PushHandlerBroadcastReceiver", "GCM Error: " + errorId);
        }
    }

    [BroadcastReceiver(Permission = Gcm.Client.Constants.PERMISSION_GCM_INTENTS)]
    [IntentFilter(new string[] { Gcm.Client.Constants.INTENT_FROM_GCM_MESSAGE }, Categories = new string[] { "@PACKAGE_NAME@" })]
    [IntentFilter(new string[] { Gcm.Client.Constants.INTENT_FROM_GCM_REGISTRATION_CALLBACK }, Categories = new string[] { "@PACKAGE_NAME@" })]
    [IntentFilter(new string[] { Gcm.Client.Constants.INTENT_FROM_GCM_LIBRARY_RETRY }, Categories = new string[] { "@PACKAGE_NAME@" })]
    public class PushHandlerBroadcastReceiver : GcmBroadcastReceiverBase<GcmService>
    {
        public static string[] SENDER_IDS = new string[] { "300143732939" };
    }

}

修改 MainActivity

  • 在 原生Android專案中,開啟 MainActivity.cs
  • 將底下程式碼置換這個 GcmService 類別
    請解析要加入的適當命名空間
using System;

using Android.App;
using Android.Content.PM;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Android.OS;
using Prism.Unity;
using Microsoft.Practices.Unity;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.MobileServices;
using XFDoggy.Helpers;

using Gcm.Client;
using XFDoggy.Droid.Infrastructure;

[assembly: UsesPermission(Android.Manifest.Permission.AccessNetworkState)]
[assembly: UsesPermission(Android.Manifest.Permission.ReadExternalStorage)]
[assembly: UsesPermission(Android.Manifest.Permission.WriteExternalStorage)]
[assembly: UsesPermission(Android.Manifest.Permission.CallPhone)]
namespace XFDoggy.Droid
{
    [Activity(Label = "多奇數位創意", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity, IAuthenticate
    {
        #region Firebase 的推播設定用程式碼
        // Create a new instance field for this activity.
        static MainActivity instance = null;

        // Return the current activity instance.
        public static MainActivity CurrentActivity
        {
            get
            {
                return instance;
            }
        }

        #endregion

        #region Azure 行動應用之身分驗證用到程式碼

        // Define a authenticated user.
        private MobileServiceUser user;


        public async Task<bool> Authenticate(MobileServiceAuthenticationProvider p登入方式)
        {
            // 驗證結果
            var success = false;
            // 要顯示的訊息
            var message = string.Empty;
            try
            {
                // 呼叫 Azure Mobile 用戶端的 LoginAsync 方法,依據指定的登入類型,進行身分驗證登入
                user = await MainHelper.client.LoginAsync(this, p登入方式);
                if (user != null)
                {
                    message = string.Format("you are now signed-in as {0}.", user.UserId);
                    success = true;
                }
            }
            catch (Exception ex)
            {
                message = ex.Message;
            }

            // 顯示登入成功或者失敗.
            //AlertDialog.Builder builder = new AlertDialog.Builder(this);
            //builder.SetMessage(message);
            //builder.SetTitle("Sign-in result");
            //builder.Create().Show();

            return success;
        }
        #endregion

        protected override void OnCreate(Bundle bundle)
        {
            TabLayoutResource = Resource.Layout.tabs;
            ToolbarResource = Resource.Layout.toolbar;

            base.OnCreate(bundle);

            global::Xamarin.Forms.Forms.Init(this, bundle);

            #region Azure 行動應用之身分驗證用到程式碼
            // 將在 Android 原生平台實作的 IAuthenticate 物件,指定到 核心PCL 專案內
            App.Init((IAuthenticate)this);
            #endregion

            #region Firebase 的推播設定用程式碼
            // Set the current instance of MainActivity.
            instance = this;
            #endregion

            LoadApplication(new App(new AndroidInitializer()));

            #region Firebase 的推播設定用程式碼
            try
            {
                // Check to ensure everything's set up right
                GcmClient.CheckDevice(this);
                GcmClient.CheckManifest(this);

                // Register for push notifications
                System.Diagnostics.Debug.WriteLine("Registering...");
                GcmClient.Register(this, PushHandlerBroadcastReceiver.SENDER_IDS);
            }
            catch (Java.Net.MalformedURLException)
            {
                CreateAndShowDialog("There was an error creating the client. Verify the URL.", "Error");
            }
            catch (Exception e)
            {
                CreateAndShowDialog(e.Message, "Error");
            }
            #endregion
        }

        #region Firebase 的推播設定用程式碼
        private void CreateAndShowDialog(String message, String title)
        {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);

            builder.SetMessage(message);
            builder.SetTitle(title);
            builder.Create().Show();
        }
        #endregion

        #region Firebase 的推播設定用程式碼

        #endregion
    }

    public class AndroidInitializer : IPlatformInitializer
    {
        public void RegisterTypes(IUnityContainer container)
        {

        }
    }
}

執行與測試

  • 您需要有安裝 Google Play 服務的裝置,才能夠進行底下的測試