Share via


企業應用程式導覽

注意

本電子書於 2017 年春季出版,此後尚未更新。 這本書中有很多仍然有價值的,但一些材料已經過時。

Xamarin.Forms 包含頁面流覽的支援,這通常是由於內部邏輯驅動狀態變更而由使用者與UI或應用程式本身互動的結果。 不過,在使用 Model-View-ViewModel (MVVM) 模式的應用程式中實作導覽可能相當複雜,因為必須符合下列挑戰:

  • 如何使用不會在檢視之間產生緊密結合和相依性的方法,識別要巡覽至的檢視。
  • 如何協調要巡覽至之檢視的程式,以具現化和初始化。 使用MVVM時,必須透過檢視的系結內容來具現化檢視和檢視模型彼此關聯。 當應用程式使用相依性插入容器時,檢視和檢視模型的具現化可能需要特定的建構機制。
  • 要執行檢視優先瀏覽,還是檢視模型優先流覽。 使用檢視優先導覽時,要導覽至的頁面會參考檢視類型的名稱。 在瀏覽期間,指定的檢視會具現化,以及其對應的檢視模型和其他相依服務。 另一種方法是使用檢視模型優先導覽,其中要巡覽的頁面會參考檢視模型類型的名稱。
  • 如何清除應用程式在檢視和檢視模型中的瀏覽行為。 MVVM 模式提供應用程式UI與其呈現和商業規則之間的分隔。 不過,應用程式的瀏覽行為通常會跨越應用程式的 UI 和簡報部分。 使用者通常會從檢視開始導覽,而檢視會因導覽遭取代。 不過,流覽通常也需要從檢視模型內起始或協調。
  • 如何在導覽期間傳遞參數以進行初始化。 舉例來說,如果使用者導覽至檢視以更新訂單詳細資料,訂單資料必須傳遞至檢視,才能顯示正確的資料。
  • 如何協調流覽,以確保遵守特定商務規則。 例如,在離開檢視之前,可能會提示使用者以便更正任何不正確的資料,或提示使用者提交或捨棄在檢視內所做的任何資料變更。

本章藉由呈現用來執行檢視模型第一 NavigationService 頁導覽的類別,來解決這些挑戰。

注意

NavigationService應用程式所使用的 只是為了在 ContentPage 實例之間執行階層式導覽而設計。 使用服務在其他頁面類型之間巡覽可能會導致非預期的行為。

瀏覽邏輯可以位於檢視的程式代碼後置或數據綁定檢視模型中。 雖然將瀏覽邏輯放在檢視中可能是最簡單的方法,但它不容易透過單元測試進行測試。 將瀏覽邏輯放在檢視模型類別中,表示可以透過單元測試來練習邏輯。 此外,檢視模型接著可以實作邏輯來控制導覽,以確保強制執行特定商務規則。 例如,應用程式可能不允許使用者在沒有先確保輸入的資料有效的情況下,離開頁面。

類別 NavigationService 通常會從檢視模型叫用,以提升可測試性。 不過,從檢視模型流覽至檢視時,需要檢視模型參考檢視,特別是使用中檢視模型未相關聯的檢視,不建議這麼做。 因此, NavigationService 此處顯示的 會指定要巡覽的目標檢視模型類型。

eShopOnContainers 行動應用程式會 NavigationService 使用 類別來提供檢視模型優先導覽。 這個類別會實作 INavigationService 介面,如以下程式碼範例所示:

public interface INavigationService  
{  
    ViewModelBase PreviousPageViewModel { get; }  
    Task InitializeAsync();  
    Task NavigateToAsync<TViewModel>() where TViewModel : ViewModelBase;  
    Task NavigateToAsync<TViewModel>(object parameter) where TViewModel : ViewModelBase;  
    Task RemoveLastFromBackStackAsync();  
    Task RemoveBackStackAsync();  
}

此介面指會定實作類別必須提供下列方法:

方法 目的
InitializeAsync 在啟動應用程式時,導覽至兩個頁面的其中一個。
NavigateToAsync 對指定的頁面執行階層式導覽。
NavigateToAsync(parameter) 執行階層式導覽至指定的頁面,傳遞參數。
RemoveLastFromBackStackAsync 從流覽堆疊移除上一頁。
RemoveBackStackAsync 從流覽堆疊移除所有先前的頁面。

此外, INavigationService 介面會指定實作類別必須提供 PreviousPageViewModel 屬性。 這個屬性會傳回與流覽堆疊中上一頁相關聯的檢視模型類型。

注意

INavigationService 介面也經常會指定 GoBackAsync 方法,用來以程式設計方式返回導覽堆疊中的上一頁。 不過,eShopOnContainers 行動應用程式缺少這個方法,因為不需要此方法。

建立 NavigationService 實例

NavigationService 作 介面的 INavigationService 類別會向 Autofac 相依性插入容器註冊為單一實例,如下列程式代碼範例所示:

builder.RegisterType<NavigationService>().As<INavigationService>().SingleInstance();

介面 INavigationService 會在類別建 ViewModelBase 構函式中解析,如下列程式代碼範例所示:

NavigationService = ViewModelLocator.Resolve<INavigationService>();

這會傳回儲存在 Autofac 相依性插入容器中之 對象的參考 NavigationService ,這個物件是由 InitNavigation 類別中的 App 方法所建立。 如需詳細資訊,請參閱 啟動應用程式時巡覽。

ViewModelBase 類別會將 NavigationService 執行個體儲存在型別為 INavigationServiceNavigationService。 因此,所有衍生自 類別的 ViewModelBase 檢視模型類別都可以使用 NavigationService 屬性來存取 介面所 INavigationService 指定的方法。 這可避免將物件從 Autofac 相依性插入容器插入 NavigationService 每個檢視模型類別的額外負荷。

處理導覽要求

Xamarin.Forms 提供 類別 NavigationPage ,其會實作階層式瀏覽體驗,讓用戶能夠視需要瀏覽頁面、向前和向後流覽。 如需有關階層式導覽的詳細資訊,請參閱階層式導覽

eShopOnContainers 應用程式會包裝 NavigationPage 類別,CustomNavigationView而不是直接使用 NavigationPage 類別,如下列程式代碼範例所示:

public partial class CustomNavigationView : NavigationPage  
{  
    public CustomNavigationView() : base()  
    {  
        InitializeComponent();  
    }  

    public CustomNavigationView(Page root) : base(root)  
    {  
        InitializeComponent();  
    }  
}

這個包裝的目的是為了方便在 類別的 XAML 檔案內設定實例的樣式 NavigationPage

巡覽是在檢視模型類別內執行,方法是叫用其中 NavigateToAsync 一種方法,指定所巡覽頁面的檢視模型類型,如下列程式代碼範例所示:

await NavigationService.NavigateToAsync<MainViewModel>();

下列程式代碼範例顯示 NavigateToAsync 類別所提供的 NavigationService 方法:

public Task NavigateToAsync<TViewModel>() where TViewModel : ViewModelBase  
{  
    return InternalNavigateToAsync(typeof(TViewModel), null);  
}  

public Task NavigateToAsync<TViewModel>(object parameter) where TViewModel : ViewModelBase  
{  
    return InternalNavigateToAsync(typeof(TViewModel), parameter);  
}

每個方法都允許衍生自 ViewModelBase 類別的任何檢視模型類別,藉由叫 InternalNavigateToAsync 用 方法來執行階層式導覽。 此外,第二 NavigateToAsync 個方法可讓巡覽數據指定為傳遞至所巡覽檢視模型的自變數,其中通常用來執行初始化。 如需詳細資訊,請參閱 在導覽期間傳遞參數。

InternalNavigateToAsync方法會執行巡覽要求,如下列程式代碼範例所示:

private async Task InternalNavigateToAsync(Type viewModelType, object parameter)  
{  
    Page page = CreatePage(viewModelType, parameter);  

    if (page is LoginView)  
    {  
        Application.Current.MainPage = new CustomNavigationView(page);  
    }  
    else  
    {  
        var navigationPage = Application.Current.MainPage as CustomNavigationView;  
        if (navigationPage != null)  
        {  
            await navigationPage.PushAsync(page);  
        }  
        else  
        {  
            Application.Current.MainPage = new CustomNavigationView(page);  
        }  
    }  

    await (page.BindingContext as ViewModelBase).InitializeAsync(parameter);  
}  

private Type GetPageTypeForViewModel(Type viewModelType)  
{  
    var viewName = viewModelType.FullName.Replace("Model", string.Empty);  
    var viewModelAssemblyName = viewModelType.GetTypeInfo().Assembly.FullName;  
    var viewAssemblyName = string.Format(  
                CultureInfo.InvariantCulture, "{0}, {1}", viewName, viewModelAssemblyName);  
    var viewType = Type.GetType(viewAssemblyName);  
    return viewType;  
}  

private Page CreatePage(Type viewModelType, object parameter)  
{  
    Type pageType = GetPageTypeForViewModel(viewModelType);  
    if (pageType == null)  
    {  
        throw new Exception($"Cannot locate page type for {viewModelType}");  
    }  

    Page page = Activator.CreateInstance(pageType) as Page;  
    return page;  
}

方法 InternalNavigateToAsync 會先呼叫 CreatePage 方法,執行巡覽至檢視模型。 此方法會找出對應至指定檢視模型類型的檢視,並建立並傳回這個檢視類型的實例。 尋找對應至檢視模型類型的檢視會使用慣例型方法,假設:

  • 檢視與檢視模型類型位於相同的元件中。
  • 檢視位於 中。檢視子命名空間。
  • 檢視模型位於 中。ViewModels 子命名空間。
  • 檢視名稱會對應至檢視模型名稱,並已移除 「模型」。

具現化檢視時,它與對應的檢視模型相關聯。 如需如何發生這種情況的詳細資訊,請參閱 使用檢視模型定位器自動建立檢視模型。

如果所建立的檢視是 LoginView,它會包裝在 類別的新實例 CustomNavigationView 內,並指派給 Application.Current.MainPage 屬性。 否則, CustomNavigationView 會擷取 實例,而且前提是它不是 Null, PushAsync 則會叫用 方法,將建立的檢視推送至瀏覽堆疊。 不過,如果擷 CustomNavigationView 取的實例是 null,所建立的檢視會包裝在 類別的新實例 CustomNavigationView 內,並指派給 Application.Current.MainPage 屬性。 此機制可確保在巡覽期間,頁面會在空白且包含數據時正確新增至瀏覽堆疊。

提示

請考慮快取頁面。 頁面快取會導致目前未顯示之檢視的記憶體耗用量。 不過,若沒有頁面快取,就表示每次巡覽至新頁面時,就會發生頁面及其檢視模型的 XAML 剖析和建構,這可能會對複雜頁面產生效能影響。 對於未使用過多控件的妥善設計頁面,效能應該就足夠了。 不過,如果遇到頁面載入時間緩慢,頁面快取可能會有所説明。

建立檢視並巡覽至 之後, InitializeAsync 就會執行檢視相關聯檢視模型的方法。 如需詳細資訊,請參閱 在導覽期間傳遞參數。

啟動應用程式時, InitNavigation 會叫用 類別中的 App 方法。 下列程式碼範例示範此方法:

private Task InitNavigation()  
{  
    var navigationService = ViewModelLocator.Resolve<INavigationService>();  
    return navigationService.InitializeAsync();  
}

方法會在 Autofac 相依性插入容器中建立新的 NavigationService 物件,並在叫用其方法之前傳回其 InitializeAsync 參考。

注意

INavigationService類別解析ViewModelBase介面時,容器會傳回叫用 InitNavigation 方法時所建立對象的參考NavigationService

下列程式碼範例示範 NavigationServiceInitializeAsync 方法:

public Task InitializeAsync()  
{  
    if (string.IsNullOrEmpty(Settings.AuthAccessToken))  
        return NavigateToAsync<LoginViewModel>();  
    else  
        return NavigateToAsync<MainViewModel>();  
}

MainView如果應用程式具有用於驗證的快取存取權杖,則會巡覽至 。 否則, LoginView 會巡覽至 。

如需 Autofac 相依性插入容器的詳細資訊,請參閱 相依性插入簡介。

在導覽期間傳遞參數

介面所INavigationService指定的其中NavigateToAsync一個方法,可讓導覽數據指定為傳遞至所巡覽檢視模型的自變數,其中通常用來執行初始化。

例如,ProfileViewModel 類別包含使用者在 ProfileView 頁面上選取訂單時執行的 OrderDetailCommand。 接著,這會執行 OrderDetailAsync 方法,如下列程式碼範例所示:

private async Task OrderDetailAsync(Order order)  
{  
    await NavigationService.NavigateToAsync<OrderDetailViewModel>(order);  
}

這個方法會叫用巡覽至 OrderDetailViewModel,傳遞 Order 實例,代表用戶在頁面上選取 ProfileView 的順序。 NavigationService當 類別建立 OrderDetailView時,類別OrderDetailViewModel會具現化並指派給檢視的 BindingContext。 巡覽至 OrderDetailView之後, InternalNavigateToAsync 方法會 InitializeAsync 執行檢視相關聯檢視模型的方法。

方法 InitializeAsync 會在類別中 ViewModelBase 定義為可覆寫的方法。 這個方法會指定自 object 變數,代表巡覽作業期間要傳遞至檢視模型的數據。 因此,檢視想要從導覽作業接收數據的模型類別,提供自己的方法實作 InitializeAsync 來執行必要的初始化。 下列程式碼範例顯示 OrderDetailViewModel 類別中的 InitializeAsync 方法:

public override async Task InitializeAsync(object navigationData)  
{  
    if (navigationData is Order)  
    {  
        ...  
        Order = await _ordersService.GetOrderAsync(  
                        Convert.ToInt32(order.OrderNumber), authToken);  
        ...  
    }  
}

這個方法會 Order 擷取在巡覽作業期間傳入檢視模型的實例,並用它來擷 OrderService 取實例的完整順序詳細數據。

使用行為叫用導覽

導覽通常會由使用者互動從檢視觸發。 例如,LoginView 會在驗證成功之後執行導覽。 下列程式碼範例示範如何透過行為叫用導覽:

<WebView ...>  
    <WebView.Behaviors>  
        <behaviors:EventToCommandBehavior  
            EventName="Navigating"  
            EventArgsConverter="{StaticResource WebNavigatingEventArgsConverter}"  
            Command="{Binding NavigateCommand}" />  
    </WebView.Behaviors>  
</WebView>

在執行階段,EventToCommandBehavior 會回應與 WebView 的互動。 WebView當巡覽至網頁時,Navigating會引發 事件,這會在 中LoginViewModel執行 NavigateCommand 。 預設會將事件的事件引數傳遞給命令。 資料因其在來源和目標之間傳遞,由 EventArgsConverter 屬性中指定的轉換器轉換,這會從 WebNavigatingEventArgs 傳回 Url。 因此,執行 時 NavigationCommand ,網頁的 URL 會當做參數傳遞至已註冊 Action的 。

接著,NavigationCommand 會執行 NavigateAsync 方法,如下列程式碼範例所示:

private async Task NavigateAsync(string url)  
{  
    ...          
    await NavigationService.NavigateToAsync<MainViewModel>();  
    await NavigationService.RemoveLastFromBackStackAsync();  
    ...  
}

這個方法會叫用巡覽至 MainViewModel,並在瀏覽之後從流覽堆疊中移除 LoginView 頁面。

確認或取消導覽

應用程式可能需要在導覽作業期間與使用者互動,讓使用者可以確認或取消導覽。 例如,使用者嘗試在顯示完全完成的資料輸入頁面之前導覽時,可能必須這麼做。 在此情況下,應用程式應該提供通知,讓使用者可離開頁面,或在離開之前取消導覽作業。 這可以在檢視模型類別中達成,方法是使用通知的回應來控制是否叫用導覽。

摘要

Xamarin.Forms 包含頁面流覽的支援,這通常是因為使用者與UI的互動,或應用程式本身,因為內部邏輯驅動狀態變更所致。 但在使用 MVVM 模式的應用程式中實作導覽,可能相當複雜。

本章呈現類別 NavigationService ,用來從檢視模型執行檢視模型優先流覽。 將瀏覽邏輯放在檢視模型類別中,表示可以透過自動化測試來練習邏輯。 此外,檢視模型接著可以實作邏輯來控制導覽,以確保強制執行特定商務規則。