2018-01-13

Prism for Xamarin.Forms 7.0 的相依性服務注入修正與使用說明

01/12 Prism for Xamarin.Forms 推出了7.0 正式版本,並且Prism Template Pack 也同步升級到2.0.8 版本。在此次的更新過程中,增加了許多相當好用的功能,不過,也同步中止了之前6.3 版本的好用功能。
在這裡有個相當重大的變更,那就是在以往我們若需要相依性注入服務,將原生專案內的實作介面類別,想要透過Prism的建構式註入到檢視模型ViewModel內,我們僅需要在實作介面類別中,使用Xamarin.Forms.Dependency屬性進行宣告,就可以直接使用,不過,在Prism for Xamarin.Forms 7.0這項福利已經無法使用了。
若我們想要繼續使用這樣的功能,那該如何處理呢?Prism現在將不同的相依性注入服務進行抽象化處理,包裝到Prism.IoC命名空間類,因此,當我們想要進行解析或者註入一個新的物件,可以使用IContainerProvider介面,而當我們要進行註冊一個要注入的介面與型別的時候,可以使用IContainerRegistry介面。
在這以,我們在Xamarin.Forms 的.NET Standard 類別庫內,建立了一個介面
public interface IOSInfo
{
    string Info();
}
接著,就如以往,我們在原生Android 專案內,來實作出這個介面,並且使用了Xamarin.Forms.Dependency 這個屬性來宣告這個類別是可以用於Xamarin.Forms 的相依性注入服務中。
[assembly: Xamarin.Forms.Dependency(typeof(BlankApp2.Droid.OSInfo))]
namespace BlankApp2.Droid
{
    public class OSInfo : IOSInfo
    {
        public string Info()
        {
            return "Android";
        }
    }
}
現在,我們在檢視頁面ViewModel 內的建構函式,想要自動注入IOSInfor 這個介面的實作物件,如下列程式碼。
private IOSInfo _osInfo;
public MainPageViewModel(INavigationService navigationService,
    IOSInfo osInfo)
{
    _navigationService = navigationService;
    _osInfo = osInfo;

    Title = _osInfo.Info();

}
很不幸的,您將會得到底下的例外異常錯誤訊息:
Unhandled Exception:

Unity.Exceptions.ResolutionFailedException: Resolution of the dependency failed, type = 'BlankApp2.ViewModels.MainPageViewModel', name = '(none)'.
Exception occurred while: while resolving.
Exception is: InvalidOperationException - The current type, BlankApp2.IOSInfo, is an interface and cannot be constructed. Are you missing a type mapping?
-----------------------------------------------
At the time of the exception, the container was: 
  Resolving BlankApp2.ViewModels.MainPageViewModel,(none)
  Resolving parameter 'osInfo' of constructor BlankApp2.ViewModels.MainPageViewModel(Prism.Navigation.INavigationService navigationService, BlankApp2.IOSInfo osInfo)
    Resolving BlankApp2.IOSInfo,(none)
因此,想要能夠在Xamarin.Forms專案內,注入原生專案的實作類別物件,您需要自行透過IPlatformInitializer介面來實作出public void RegisterTypes(IContainerRegistry container)方法,在這個方法中有個參數,IContainerRegistry container,我們可以透過這個container來進行註冊行為,如同底下程式碼
public class AndroidInitializer : IPlatformInitializer
{
    public void RegisterTypes(IContainerRegistry container)
    {
        // Register any platform specific implementations
        container.Register<IOSInfo, OSInfo>();
    }
}
當然,每個原生專案內,您都要自己進行註冊動作,如此,您就可以在Xamarin.Forms 專案內,使用建構函式來注入這些原生專案內的實作類別物件。
若您想要在Xamarin.Forms專案內,自行註冊介面與實作類別,可以在App.xaml.cs檔案內,找到RegisterTypes這個方法,在這個方法內,使用containerRegistry參數就可以進行註冊的動作。
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    containerRegistry.RegisterForNavigation<NavigationPage>();
    containerRegistry.RegisterForNavigation<MainPage>();
    containerRegistry.Register<IDITest, DITest>();
}
若想要直接操作Unity 這個相依性服務注入容器,透過containerRegistry 這個參數的GetContainer 方法,就可以得到IUnityContainer 的物件;在底下的程式碼中,我們將會顯示出所有已經註冊,且可以注入的介面與實作物件清單。
var foo = containerRegistry.GetContainer();
foreach (var item in foo.Registrations)
{
    System.Console.WriteLine($"{item.RegisteredType.Name} -> {item.MappedToType.Name}");
}
01-13 19:05:08.541 I/mono-stdout( 5976): IUnityContainer -> IUnityContainer
01-13 19:05:08.542 I/mono-stdout( 5976): IContainerExtension -> IContainerExtension
01-13 19:05:08.542 I/mono-stdout( 5976): ILoggerFacade -> EmptyLogger
01-13 19:05:08.542 I/mono-stdout( 5976): IApplicationProvider -> ApplicationProvider
01-13 19:05:08.542 I/mono-stdout( 5976): IApplicationStore -> ApplicationStore
01-13 19:05:08.542 I/mono-stdout( 5976): IEventAggregator -> EventAggregator
01-13 19:05:08.542 I/mono-stdout( 5976): IDependencyService -> DependencyService
01-13 19:05:08.542 I/mono-stdout( 5976): IPageDialogService -> PageDialogService
01-13 19:05:08.542 I/mono-stdout( 5976): IDeviceService -> DeviceService
01-13 19:05:08.542 I/mono-stdout( 5976): IPageBehaviorFactory -> PageBehaviorFactory
01-13 19:05:08.542 I/mono-stdout( 5976): IModuleCatalog -> ModuleCatalog
01-13 19:05:08.542 I/mono-stdout( 5976): IModuleManager -> ModuleManager
01-13 19:05:08.542 I/mono-stdout( 5976): IModuleInitializer -> ModuleInitializer
01-13 19:05:08.543 I/mono-stdout( 5976): INavigationService -> PageNavigationService
01-13 19:05:08.543 I/mono-stdout( 5976): IOSInfo -> OSInfo
01-13 19:05:08.543 I/mono-stdout( 5976): Object -> NavigationPage
01-13 19:05:08.543 I/mono-stdout( 5976): Object -> MainPage
01-13 19:05:08.543 I/mono-stdout( 5976): IDITest -> DITest
最後,若想要在檢視模型類別或者任何地方,使用容器來取得實作物件,可以使用底下的程式碼來手動注入。
IContainerProvider fooContainer = (App.Current as Prism.Unity.PrismApplication).Container;
var fooOSInfo = fooContainer.Resolve<IDITest>();

2018-01-07

Xamarin.Forms / .NET Standard 體驗之旅5 : 彈出頁面Prism.Plugin.Popups

最近這段時間都在忙於公司相關事務上,因為最近幾天感冒,嚴重咳嗽,頭昏昏的,趁著今天有一點點好轉,繼續進行Xamarin.Forms / .NET Standard 體驗之旅5 的文章中,在這裡需要測試、使用我之前就很想使用的一個套件:Rg.Plugins.Popup。
Rg.Plugins.Popup 套件可以讓您設計的頁面,以彈出方式來顯示在手機螢幕,若此時,我們停用了手機上的實體回上頁按鈕功能,此時,使用者就必須完成該彈出視窗所指定要輸入的功能,或者在彈出視窗中點選取消按鈕,這樣,才會回到最初的螢幕頁面。
在下圖為我們的起始頁面,這個頁面上只有一個按鈕,這個時候,就會彈出我們設計需要使用者輸入帳號與密碼的彈出視窗。
Rg.Plugins.popup
下圖是彈出視窗的顯示結果,四周的綠色遮罩使用螢幕的方式來顯示,甚至連狀態列也會在遮罩顯示範圍,因此,您將看不到導航頁面的回上頁軟體按鈕,想要取消這個彈出視窗,需要點選紅色X圓圈,這樣,才會取消這個彈出視窗。
Rg.Plugins.popup
不過,想要做到這樣的功能,我們需要使用相當多的Code Behind程式碼來完成這樣的需求;在幾天之前,我看到了Prism.Plugin.Popups這個套件,它提供了擴充方法,可以讓我們使用Prism的導航物件服務,一樣可以做到彈出視窗的效果。

測試範例專案原始碼

這篇文章中的測試範例專案原始碼,您可以從這裡取得 https://github.com/vulcanlee/xamarin-forms-develop-notes-example/tree/master/XFPopup

開始建立專案

請點選功能表[檔案] > [新增] > [專案],此時,您會看到底下的[新增專案] 對話視窗。
我們點選Prism Template Pack 提供的Xamarin.Forms 專用的專案樣板,請點選[已安裝] > [Visual C#] > [Prism] > [Prism Blank App (Xamarin.Forms)]。
接著在名稱欄位輸入該專案名稱後,點選[確定] 按鈕,以建立此練習專案。
然後,在[PRISM PROJEC WIZAD] 對話窗中,選擇您要跨平台的作業系統,容器(Container) 這裡,我個人喜歡與習慣使用Prism,您可以選擇您自己要用的容器,最後,點選[ CREATE PROJECT] 按鈕。
.NET Standard Xamarin.Forms Project

安裝Prism.Plugin.Popups.Unity NuGet 套件

在這裡,請使用滑鼠右建,點選剛剛建立好的共用類別庫(也就是.NET Standard 標準類別庫)節點,選擇[管理方案的NuGet 套件] 項目。
在[瀏覽]標籤頁次上,輸入Prism.Plugin.Popups.Unity這個關鍵字,搜尋出這個套件。請選擇Prism.Plugin.Popups.Unity這個套件,要來安裝到.NET Standard專案內。

首頁頁面按鈕事件

首頁頁面的XAML相當的簡單,因為,該頁面上只有一個按鈕,讓我們看看,如何顯示這個彈出視窗;當使用按下按鈕之後,我們需要呼叫_navigationService.PushPopupPageAsync這個方法,該方法是因為安裝了Prism.Plugin.Popups.Unity套件,所提供的擴充方法。
using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;

namespace XFPopup.ViewModels
{

    public class MainPageViewModel : INotifyPropertyChanged, INavigationAware
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public DelegateCommand OpenCommand { get; set; }

        private readonly INavigationService _navigationService;

        public MainPageViewModel(INavigationService navigationService)
        {
            _navigationService = navigationService;

            OpenCommand = new DelegateCommand(async () =>
            {
                var fooPara = new NavigationParameters();
                fooPara.Add("From", "從主頁面來彈出");
                await _navigationService.PushPopupPageAsync("NewPage1Page", fooPara);
            });
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public void OnNavigatingTo(NavigationParameters parameters)
        {

        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {

        }

    }

}

新增彈出視窗頁面

接下來,我們需要新建立一個彈出視窗頁面,我們需要使用PopupPage類別來建立起可彈出的視窗頁面,而其他的設計方法,就如同我們在設計ContentPage相同。
<?xml version="1.0" encoding="utf-8" ?>
<pages:PopupPage 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"
             xmlns:pages="clr-namespace:Rg.Plugins.Popup.Pages;assembly=Rg.Plugins.Popup"
             xmlns:animations="clr-namespace:Rg.Plugins.Popup.Animations;assembly=Rg.Plugins.Popup"
             x:Class="XFPopup.Views.NewPage1Page"
             HasSystemPadding="False"
             >

    <pages:PopupPage.Resources>
        <ResourceDictionary>
            <Style x:Key="EntryStyle" TargetType="Entry">
                <Setter Property="PlaceholderColor" Value="#9cdaf1"/>
                <Setter Property="TextColor" Value="#7dbbe6"/>
            </Style>
        </ResourceDictionary>
    </pages:PopupPage.Resources>
    <pages:PopupPage.Animation>
        <animations:ScaleAnimation
      PositionIn="Bottom"
      PositionOut="Center"
      ScaleIn="1"
      ScaleOut="0.7"
      DurationIn="700"
      EasingIn="BounceOut"/>
    </pages:PopupPage.Animation>

    <Grid
        >
        <BoxView Color="YellowGreen" Opacity="0.6">
        </BoxView>
        <ScrollView
            HorizontalOptions="Center"
            VerticalOptions="Center">
            <Grid
                HorizontalOptions="Center">
                <StackLayout
                    HorizontalOptions="Center" VerticalOptions="Center"
                    BackgroundColor="White">
                    <StackLayout
                        IsClippedToBounds="True"
                        Spacing="3">
                        <Image
                            HorizontalOptions="Center"
                            x:Name="OctocatImage"
              Margin="10"
              HeightRequest="150"
              WidthRequest="150">
                            <Image.Source>
                                <OnPlatform
                  x:TypeArguments="ImageSource"
                  Android="github_octocat.png"
                  iOS="github_octocat.png"
                  WinPhone="Assets/github_octocat.png"/>
                            </Image.Source>
                        </Image>
                        <Label
                            HorizontalOptions="Center"
                            FontSize="20"
                            Text="{Binding From}"/>
                        <Entry
              HorizontalOptions="Center"
              x:Name="UsernameEntry"
              Style="{StaticResource EntryStyle}"
              Placeholder="Username" />
                        <Entry
              HorizontalOptions="Center"
              x:Name="PasswordEntry"
              Style="{StaticResource EntryStyle}"
              Placeholder="Password"
              IsPassword="True"/>
                        <Button
            Margin="10, 5"
            BackgroundColor="#7dbbe6"
            HorizontalOptions="Fill"
            Clicked="OnLogin"
            x:Name="LoginButton"
            Text="Login">
                        </Button>
                    </StackLayout>
                </StackLayout>
                <ContentView
                    HorizontalOptions="End" VerticalOptions="Start"
                >
                    <ContentView.GestureRecognizers>
                        <TapGestureRecognizer Tapped="OnCloseButtonTapped"/>
                    </ContentView.GestureRecognizers>
                    <Image
          x:Name="CloseImage"
          HeightRequest="30"
          WidthRequest="30">
                        <Image.Source>
                            <OnPlatform
              x:TypeArguments="ImageSource"
              Android="close_circle_button.png"
              iOS="close_circle_button.png"
              WinPhone="Assets/close_circle_button.png"/>
                        </Image.Source>
                    </Image>
                </ContentView>
            </Grid>
        </ScrollView>
    </Grid>
</pages:PopupPage>
接著我們打開彈出視窗頁面ViewModel檔案,修正ViewModel 的商業邏輯程式碼。
在這裡,我們展示的是使用Code Behind的方式,來關閉這個彈出視窗:await Navigation.PopAllPopupAsync();,當然,您也可以在ViewModel上來進行這樣效果的操作。
using Rg.Plugins.Popup.Pages;
using System;
using Xamarin.Forms;
using Rg.Plugins.Popup.Extensions;

namespace XFPopup.Views
{
    public partial class NewPage1Page : PopupPage
    {
        public NewPage1Page()
        {
            InitializeComponent();
        }

        protected override bool OnBackButtonPressed()
        {
            return true;
        }
        private async void OnLogin(object sender, EventArgs e)
        {
            //var loadingPage = new LoadingPopupPage();
            //await Navigation.PushPopupAsync(loadingPage);
            //await Task.Delay(2000);
            //await Navigation.RemovePopupPageAsync(loadingPage);
            //await Navigation.PushPopupAsync(new LoginSuccessPopupPage());
        }

        private void OnCloseButtonTapped(object sender, EventArgs e)
        {
            CloseAllPopup();
        }

        protected override bool OnBackgroundClicked()
        {
            CloseAllPopup();

            return false;
        }

        private async void CloseAllPopup()
        {
            await Navigation.PopAllPopupAsync();
        }

    }
}