Xamarin Forms的Prism第二部分:基本導(dǎo)航和依賴注入(Dependency Injection)模式
在前一篇文章中,我們已經(jīng)開始介紹在Xamarin Forms應(yīng)用程序中如何利用Prism(6.2)的新版本來實現(xiàn)MVVM模式的基本概念。到目前為止,我們還沒有看到什么特別的東西是我們用另一個框架做不到的:我們在上一篇文章中創(chuàng)建了一個視圖(View)、一個視圖模型(ViewModel),然后我們通過綁定連接它們。在這篇文章中,我們將看到Prism如何幫助處理一個在MVVM應(yīng)用程序中很難處理的非常常見的場景:導(dǎo)航和頁面的生命周期。
正如我們在前一篇文章中提到的,我們要為TrackSeries——一個提供電視節(jié)目信息的網(wǎng)站,創(chuàng)建一個簡單的客戶端。該應(yīng)用程序?qū)@示當(dāng)前的頂級系列,將允許用戶發(fā)現(xiàn)更多關(guān)于它的內(nèi)容。為了實現(xiàn)這一目標(biāo),我們可以用一組網(wǎng)站提供的REST服務(wù),這是非常簡單的使用和處理REST服務(wù)的遵循標(biāo)準(zhǔn)的最佳實踐:你調(diào)用一個使用HTTP命令的URL,接收返回一個JSON響應(yīng)結(jié)果。
舉個例子,如果你想知道哪些是頂級系列,你可以執(zhí)行一個HTTP GET請求到以下URL:https://api.trackseries.tv/v1/Stats/TopSeries。服務(wù)將返回給你一個JSON響應(yīng),包含頂級系列的所有細(xì)節(jié):
[ { "id":121361, "name":"Game of Thrones", "followers":10230, "firstAired":"2011-04-17T21:00:00-04:00", "country":"us", "overview":"Seven noble families fight for control of the mythical land of Westeros. Friction between the houses leads to full-scale war. All while a very ancient evil awakens in the farthest north. Amidst the war, a neglected military order of misfits, the Night's Watch, is all that stands between the realms of men and the icy horrors beyond.", "runtime":55, "status":"Continuing", "network":"HBO", "airDay":"Sunday", "airTime":"9:00 PM", "contentRating":"TV-MA", "imdbId":"tt0944947", "tvdbId":121361, "tmdbId":1399, "language":"en", "images":{ "poster":"http://static.trackseries.tv/banners/posters/121361-49.jpg", "fanart":"http://static.trackseries.tv/banners/fanart/original/121361-15.jpg", "banner":"http://static.trackseries.tv/banners/graphical/121361-g22.jpg" }, "genres":[ { "id":2, "name":"Adventure" }, { "id":4, "name":"Drama" }, { "id":5, "name":"Fantasy" } ], "added":"2014-08-08T13:30:46.227", "lastUpdated":"2016-08-18T03:03:50.05", "followedByUser":false, "slugName":"game-of-thrones" }, { "id":257655, "name":"Arrow", "followers":7517, "firstAired":"2012-10-10T20:00:00-04:00", "country":"us", "overview":"Oliver Queen and his father are lost at sea when their luxury yacht sinks. His father doesn't survive. Oliver survives on an uncharted island for five years learning to fight, but also learning about his father's corruption and unscrupulous business dealings. He returns to civilization a changed man, determined to put things right. He disguises himself with the hood of one of his mysterious island mentors, arms himself with a bow and sets about hunting down the men and women who have corrupted his city.", "runtime":45, "status":"Continuing", "network":"The CW", "airDay":"Wednesday", "airTime":"8:00 PM", "contentRating":"TV-14", "imdbId":"tt2193021", "tvdbId":257655, "tmdbId":1412, "language":"en", "images":{ "poster":"http://static.trackseries.tv/banners/posters/257655-8.jpg", "fanart":"http://static.trackseries.tv/banners/fanart/original/257655-47.jpg", "banner":"http://static.trackseries.tv/banners/graphical/257655-g9.jpg" }, "genres":[ { "id":1, "name":"Action" }, { "id":2, "name":"Adventure" }, { "id":4, "name":"Drama" } ], "added":"2014-08-08T13:37:00.133", "lastUpdated":"2016-08-15T03:11:32.013", "followedByUser":false, "slugName":"arrow" }, { "id":153021, "name":"The Walking Dead", "followers":7185, "firstAired":"2010-10-31T21:00:00-04:00", "country":"us", "overview":"The world we knew is gone. An epidemic of apocalyptic proportions has swept the globe causing the dead to rise and feed on the living. In a matter of months society has crumbled. In a world ruled by the dead, we are forced to finally start living. Based on a comic book series of the same name by Robert Kirkman, this AMC project focuses on the world after a zombie apocalypse. The series follows a police officer, Rick Grimes, who wakes up from a coma to find the world ravaged with zombies. Looking for his family, he and a group of survivors attempt to battle against the zombies in order to stay alive.\n", "runtime":50, "status":"Continuing", "network":"AMC", "airDay":"Sunday", "airTime":"9:00 PM", "contentRating":"TV-MA", "imdbId":"tt1520211", "tvdbId":153021, "tmdbId":1402, "language":"en", "images":{ "poster":"http://static.trackseries.tv/banners/posters/153021-38.jpg", "fanart":"http://static.trackseries.tv/banners/fanart/original/153021-77.jpg", "banner":"http://static.trackseries.tv/banners/graphical/153021-g44.jpg" }, "genres":[ { "id":1, "name":"Action" }, { "id":4, "name":"Drama" }, { "id":6, "name":"Horror" }, { "id":20, "name":"Suspense" } ], "added":"2014-08-08T13:31:18.617", "lastUpdated":"2016-08-18T03:04:00.28", "followedByUser":false, "slugName":"the-walking-dead" }, { "id":279121, "name":"The Flash (2014)", "followers":7069, "firstAired":"2014-10-07T20:00:00-04:00", "country":"us", "overview":"After a particle accelerator causes a freak storm, CSI Investigator Barry Allen is struck by lightning and falls into a coma. Months later he awakens with the power of super speed, granting him the ability to move through Central City like an unseen guardian angel. Though initially excited by his newfound powers, Barry is shocked to discover he is not the only \"meta-human\" who was created in the wake of the accelerator explosion – and not everyone is using their new powers for good. Barry partners with S.T.A.R. Labs and dedicates his life to protect the innocent. For now, only a few close friends and associates know that Barry is literally the fastest man alive, but it won't be long before the world learns what Barry Allen has become... The Flash.", "runtime":45, "status":"Continuing", "network":"The CW", "airDay":"Tuesday", "airTime":"8:00 PM", "contentRating":"TV-14", "imdbId":"tt3107288", "tvdbId":279121, "tmdbId":60735, "language":"en", "images":{ "poster":"http://static.trackseries.tv/banners/posters/279121-37.jpg", "fanart":"http://static.trackseries.tv/banners/fanart/original/279121-23.jpg", "banner":"http://static.trackseries.tv/banners/graphical/279121-g7.jpg" }, "genres":[ { "id":1, "name":"Action" }, { "id":2, "name":"Adventure" }, { "id":4, "name":"Drama" }, { "id":8, "name":"Science-Fiction" } ], "added":"2014-08-08T13:45:59.087", "lastUpdated":"2016-08-17T03:09:18.7", "followedByUser":false, "slugName":"the-flash-2014" }, { "id":80379, "name":"The Big Bang Theory", "followers":6922, "firstAired":"2007-09-25T20:00:00-04:00", "country":"us", "overview":"What happens when hyperintelligent roommates Sheldon and Leonard meet Penny, a free-spirited beauty moving in next door, and realize they know next to nothing about life outside of the lab. Rounding out the crew are the smarmy Wolowitz, who thinks he's as sexy as he is brainy, and Koothrappali, who suffers from an inability to speak in the presence of a woman.", "runtime":25, "status":"Continuing", "network":"CBS", "airDay":"Monday", "airTime":"8:00 PM", "contentRating":"TV-PG", "imdbId":"tt0898266", "tvdbId":80379, "tmdbId":1418, "language":"en", "images":{ "poster":"http://static.trackseries.tv/banners/posters/80379-43.jpg", "fanart":"http://static.trackseries.tv/banners/fanart/original/80379-38.jpg", "banner":"http://static.trackseries.tv/banners/graphical/80379-g28.jpg" }, "genres":[ { "id":3, "name":"Comedy" } ], "added":"2014-08-08T13:27:13.18", "lastUpdated":"2016-08-18T03:03:10.947", "followedByUser":false, "slugName":"the-big-bang-theory" }, { "id":176941, "name":"Sherlock", "followers":6387, "firstAired":"2010-07-25T20:30:00+01:00", "country":"gb", "overview":"Sherlock is a British television crime drama that presents a contemporary adaptation of Sir Arthur Conan Doyle's Sherlock Holmes detective stories. Created by Steven Moffat and Mark Gatiss, it stars Benedict Cumberbatch as Sherlock Holmes and Martin Freeman as Doctor John Watson.", "runtime":90, "status":"Continuing", "network":"BBC One", "airDay":"Sunday", "airTime":"8:30 PM", "contentRating":"TV-14", "imdbId":"tt1475582", "tvdbId":176941, "tmdbId":19885, "language":"en", "images":{ "poster":"http://static.trackseries.tv/banners/posters/176941-11.jpg", "fanart":"http://static.trackseries.tv/banners/fanart/original/176941-3.jpg", "banner":"http://static.trackseries.tv/banners/graphical/176941-g5.jpg" }, "genres":[ { "id":2, "name":"Adventure" }, { "id":4, "name":"Drama" }, { "id":14, "name":"Crime" }, { "id":16, "name":"Mystery" }, { "id":21, "name":"Thriller" } ], "added":"2014-08-08T13:32:27.247", "lastUpdated":"2016-08-17T03:07:09.747", "followedByUser":false, "slugName":"sherlock" }, { "id":263365, "name":"Marvel's Agents of S.H.I.E.L.D.", "followers":5372, "firstAired":"2013-09-24T22:00:00-04:00", "country":"us", "overview":"Phil Coulson (Clark Gregg, reprising his role from \"The Avengers\" and \"Iron Man\" ) heads an elite team of fellow agents with the worldwide law-enforcement organization known as SHIELD (Strategic Homeland Intervention Enforcement and Logistics Division), as they investigate strange occurrences around the globe. Its members -- each of whom brings a specialty to the group -- work with Coulson to protect those who cannot protect themselves from extraordinary and inconceivable threats, including a formidable group known as Hydra.", "runtime":45, "status":"Continuing", "network":"ABC (US)", "airDay":"Tuesday", "airTime":"10:00 PM", "contentRating":"TV-PG", "imdbId":"tt2364582", "tvdbId":263365, "tmdbId":1403, "language":"en", "images":{ "poster":"http://static.trackseries.tv/banners/posters/263365-16.jpg", "fanart":"http://static.trackseries.tv/banners/fanart/original/263365-26.jpg", "banner":"http://static.trackseries.tv/banners/graphical/263365-g7.jpg" }, "genres":[ { "id":1, "name":"Action" }, { "id":2, "name":"Adventure" }, { "id":4, "name":"Drama" }, { "id":5, "name":"Fantasy" }, { "id":8, "name":"Science-Fiction" } ], "added":"2014-08-08T13:39:45.967", "lastUpdated":"2016-08-18T03:05:30.987", "followedByUser":false, "slugName":"marvels-agents-of-shield" }, { "id":81189, "name":"Breaking Bad", "followers":5227, "firstAired":"2008-01-20T21:00:00-04:00", "country":"us", "overview":"Walter White, a struggling high school chemistry teacher, is diagnosed with advanced lung cancer. He turns to a life of crime, producing and selling methamphetamine accompanied by a former student, Jesse Pinkman, with the aim of securing his family's financial future before he dies.", "runtime":45, "status":"Ended", "network":"AMC", "airDay":"Sunday", "airTime":"9:00 PM", "contentRating":"TV-MA", "imdbId":"tt0903747", "tvdbId":81189, "tmdbId":1396, "language":"en", "images":{ "poster":"http://static.trackseries.tv/banners/posters/81189-10.jpg", "fanart":"http://static.trackseries.tv/banners/fanart/original/81189-21.jpg", "banner":"http://static.trackseries.tv/banners/graphical/81189-g21.jpg" }, "genres":[ { "id":4, "name":"Drama" }, { "id":14, "name":"Crime" }, { "id":20, "name":"Suspense" }, { "id":21, "name":"Thriller" } ], "added":"2014-08-08T13:27:33.917", "lastUpdated":"2016-08-13T03:01:47.063", "followedByUser":false, "slugName":"breaking-bad" }, { "id":247808, "name":"Suits", "followers":4835, "firstAired":"2011-06-24T21:00:00-04:00", "country":"us", "overview":"Suits follows college drop-out Mike Ross, who accidentally lands a job with one of New York's best legal closers, Harvey Specter. They soon become a winning team with Mike's raw talent and photographic memory, and Mike soon reminds Harvey of why he went into the field of law in the first place.", "runtime":45, "status":"Continuing", "network":"USA Network", "airDay":"Wednesday", "airTime":"9:00 PM", "contentRating":"TV-14", "imdbId":"tt1632701", "tvdbId":247808, "tmdbId":37680, "language":"en", "images":{ "poster":"http://static.trackseries.tv/banners/posters/247808-27.jpg", "fanart":"http://static.trackseries.tv/banners/fanart/original/247808-43.jpg", "banner":"http://static.trackseries.tv/banners/graphical/247808-g17.jpg" }, "genres":[ { "id":4, "name":"Drama" } ], "added":"2014-08-08T13:33:45.423", "lastUpdated":"2016-08-18T03:04:21.37", "followedByUser":false, "slugName":"suits" }, { "id":274431, "name":"Gotham", "followers":4718, "firstAired":"2014-09-23T20:00:00-04:00", "country":"us", "overview":"An action-drama series following rookie detective James Gordon as he battles villains and corruption in pre-Batman Gotham City.", "runtime":45, "status":"Continuing", "network":"FOX (US)", "airDay":"Monday", "airTime":"8:00 PM", "contentRating":"TV-14", "imdbId":"tt3749900", "tvdbId":274431, "tmdbId":60708, "language":"en", "images":{ "poster":"http://static.trackseries.tv/banners/posters/274431-17.jpg", "fanart":"http://static.trackseries.tv/banners/fanart/original/274431-22.jpg", "banner":"http://static.trackseries.tv/banners/graphical/274431-g6.jpg" }, "genres":[ { "id":1, "name":"Action" }, { "id":4, "name":"Drama" }, { "id":8, "name":"Science-Fiction" }, { "id":14, "name":"Crime" }, { "id":21, "name":"Thriller" } ], "added":"2014-08-08T13:44:55.4", "lastUpdated":"2016-08-17T03:08:55.473", "followedByUser":false, "slugName":"gotham" } ]
為了使用這些應(yīng)用程序中的API,我用一組方法創(chuàng)建了一個稱為TsApiService的類,通過.NET框架和流行的JSON.NET庫的HttpClient類,負(fù)責(zé)下載JSON,解析它并返回一組可以使用C#很容易地操縱的對象。為了更好地構(gòu)成我的解決方案, 我已經(jīng)決定把所有的通信相關(guān)類與REST API (如服務(wù)和實體)放置在另一個叫做InfoSeries.Core的便攜式類庫(Portable Class Library)中,這是一個與實際Xamarin Forms應(yīng)用程序的相比不同的PCL。
這就是負(fù)責(zé)解析之前的JSON的方法返回一個C#對象列表:
public async Task<List<SerieFollowersVM>> GetStatsTopSeries() { using (HttpClient client = new HttpClient()) { try { var response = await client.GetAsync("https://api.trackseries.tv/v1/Stats/TopSeries"); if (!response.IsSuccessStatusCode) { var error = await response.Content.ReadAsAsync<TrackSeriesApiError>(); var message = error != null ? error.Message : ""; throw new TrackSeriesApiException(message, response.StatusCode); } return await response.Content.ReadAsAsync<List<SerieFollowersVM>>(); } catch (HttpRequestException ex) { throw new TrackSeriesApiException("", false, ex); } catch (UnsupportedMediaTypeException ex) { throw new TrackSeriesApiException("", false, ex); } } }
HttpClient類的GetAsync() 方法執(zhí)行GET請求到URL,返回結(jié)果包含JSON響應(yīng)的字符串。這個結(jié)果存儲在響應(yīng)的Content 屬性:如果請求成功(我們使用IsSuccessStatusCode 屬性檢查這種情況),我們使用Content 屬性公開的ReadAsAsync< T >方法自動轉(zhuǎn)換為JSON導(dǎo)致SerieFollowersVM 對象的集合。SerieFollowersVM 無非是一個映射JSON響應(yīng)的每個屬性的類 (如name、country或runtime)到一個C#屬性:
public class SerieFollowersVM { public int Id { get; set; } public string Name { get; set; } public int Followers { get; set; } public DateTimeOffset FirstAired { get; set; } public string Country { get; set; } public string Overview { get; set; } public int Runtime { get; set; } public string Status { get; set; } public string Network { get; set; } public DayOfWeek? AirDay { get; set; } public string AirTime { get; set; } public string ContentRating { get; set; } public string ImdbId { get; set; } public int TvdbId { get; set; } public string Language { get; set; } public ImagesSerieVM Images { get; set; } public ICollection<GenreVM> Genres { get; set; } public DateTime Added { get; set; } public DateTime LastUpdated { get; set; } public string SlugName { get; set; } }
在GitHub的完整示例(為了方便各位讀者,小編已經(jīng)為大家整理了,請點擊這里下載)中你會發(fā)現(xiàn)很多這樣的類(映射各種被TrackSeries API返回的JSON響應(yīng))。此外,TsApiService 將實現(xiàn)另外的方法,一個用于我們想在我們的應(yīng)用程序中利用的每個API的方法。我不會詳細(xì)解釋每個方法,因為這將超出本文的范圍,你可以在GitHub上看到所有的細(xì)節(jié)。對于這篇文章的目的,你只需要知道服務(wù)只是公開了一組方法,我們可以在各種ViewModels中使用來檢索可用的電視節(jié)目的信息。
注意:默認(rèn)情況下,HttpClient 類沒有提供一個ReadAsAsync< T >方法,能夠自動對JSON響應(yīng)為C#對象進(jìn)行反序列化。為了獲得該擴(kuò)展方法,我們需要添加Microsoft.AspNet.WebApi.Client NuGet包到便攜類庫(Portable Class Library)。為了讓它正常工作,你需要將這個包添加到解決方案的每個項目(Xamarin Forms PCL、Core PCL和所有特定于平臺的項目)。
然而,為了正確利用依賴注入(dependency injection),我們需要一個接口來描述TsApiService 類提供的操作。這就是我們的接口的樣子:
public interface ITsApiService { Task<List<SerieFollowersVM>> GetStatsTopSeries(); Task<SerieVM> GetSerieByIdAll(int id); Task<SerieInfoVM> GetSerieById(int id); Task<List<SerieSearch>> GetSeriesSearch(string name); Task<SerieFollowersVM> GetStatsSerieHighlighted(); }
現(xiàn)在我們有了一個服務(wù),我們可以學(xué)習(xí)(多虧Prism)我們可以如何注冊到它的依賴容器,它會自動注入在我們的ViewModels。實際上,從這個角度來看,沒有什么特別強調(diào):這與其他MVVM框架使用的方法是相同的,利用依賴注入的方法。首先,我們需要注冊我們想要在容器中使用的接口和實現(xiàn)之間的協(xié)會。在Prism的情況下,我們需要用App類的RegisterTypes()方法,通過使用Container對象和RegisterType< T, Y >()方法(其中T 是接口,Y是具體實現(xiàn)):
protected override void RegisterTypes() { Container.RegisterTypeForNavigation<MainPage>(); Container.RegisterType<ITsApiService, TsApiService>(); }
當(dāng)MainPage 和TsApiService 都在容器注冊了,我們可以在ViewModel獲得它,只需添加一個參數(shù)在公共構(gòu)造函數(shù),就像以下示例:
public class MainPageViewModel : BindableBase { private readonly ITsApiService _apiService; public MainPageViewModel(ITsApiService apiService) { _apiService = apiService; } }
MainPageViewModel 類將被加載時,我們已經(jīng)在容器注冊的ITsApiService實現(xiàn)(在我們的例子中是TsApiService 類)將自動注入構(gòu)造函數(shù)的參數(shù),允許我們以我們將在ViewModel創(chuàng)建的所有其他的方法和屬性來使用它。使用這種方法,我們將容易改變服務(wù)的實現(xiàn),以防我們需要它:它將足以改變App類的注冊類型,并且每個ViewModel將自動開始使用新的版本。
處理導(dǎo)航的生命周期
現(xiàn)在我們有一個服務(wù),它提供了一種方法來檢索頂級系列的列表,在ViewModel加載時我們需要調(diào)用它。我們的目標(biāo)是顯示(在應(yīng)用程序的主頁)最熱門的電視節(jié)目列表。但是,我們即將面對使用MVVM模式時的一個常見的問題:檢索頂級系列列表的方法是異步的,但是隨著當(dāng)前實現(xiàn),唯一我們可以執(zhí)行數(shù)據(jù)加載的地方就是ViewModel的構(gòu)造函數(shù),它不能執(zhí)行異步調(diào)用(在C#中,事實上,一個類的構(gòu)造函數(shù)不能用async關(guān)鍵字,因此,你不能用等待前綴的方法)。在non-MVVM應(yīng)用程序中,這個問題很容易解決,因為導(dǎo)航的生命周期方法是由每一個平臺基本提供的。Xamarin Forms毫無例外,我們可以利用(在XAML頁面類的后面的代碼)OnAppearing()和OnDisappearing()方法:因為它們是事件,我們可以沒有問題地調(diào)用異步代碼。
為了解決這個問題,Prism提供一個稱為INavigationAware的接口,我們可以在ViewModels實現(xiàn)。當(dāng)我們實現(xiàn)它,我們可以訪問OnNavigatedTo()和OnNavigatedFrom()事件,我們可以使用它們來執(zhí)行數(shù)據(jù)加載或清理操作。這就是實現(xiàn)這個接口后我們的MainPageViewModel 的樣子:
public class MainPageViewModel : BindableBase, INavigationAware { private readonly TsApiService _apiService; private ObservableCollection<SerieFollowersVM> _topSeries; public ObservableCollection<SerieFollowersVM> TopSeries { get { return _topSeries; } set { SetProperty(ref _topSeries, value); } } public MainPageViewModel(TsApiService apiService) { _apiService = apiService; } public void OnNavigatedFrom(NavigationParameters parameters) { } public async void OnNavigatedTo(NavigationParameters parameters) { var result = await _apiService.GetStatsTopSeries(); TopSeries = new ObservableCollection<SerieFollowersVM>(result); } }
正如你所看到的,現(xiàn)在我們實現(xiàn)了一個稱為OnNavigatedTo()的方法,我們可以安全地執(zhí)行異步調(diào)用和加載數(shù)據(jù)。我們調(diào)用TsApiService類的GetStatsTopSeries()方法,我們封裝結(jié)果集合到ObservableCollection屬性。這是我們要連接的屬性,通過綁定到一個ListView 控件,為了在主頁顯示電視節(jié)目列表。
出于完整性的考慮,這是MainPage的XAML的樣子:
<?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="InfoSeries.Views.MainPage" Title="Info Series"> <ContentPage.Resources> <ResourceDictionary> <DataTemplate x:Key="TopSeriesTemplate"> <ViewCell> <ViewCell.View> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="1*" /> <ColumnDefinition Width="2*" /> </Grid.ColumnDefinitions> <Image Source="{Binding Images.Poster}" Grid.Column="0" x:Name="TopImage" /> <StackLayout Grid.Column="1" Margin="12, 0, 0, 0" VerticalOptions="Start"> <Label Text="{Binding Name}" FontSize="18" TextColor="#58666e" FontAttributes="Bold" /> <StackLayout Orientation="Horizontal"> <Label Text="Runtime: " FontSize="14" TextColor="#58666e" /> <Label Text="{Binding Runtime}" FontSize="14" TextColor="#98a6ad" Margin="5, 0, 0, 0" /> </StackLayout> <StackLayout Orientation="Horizontal"> <Label Text="Air day: " FontSize="14" TextColor="#58666e" /> <Label Text="{Binding AirDay}" FontSize="14" TextColor="#98a6ad" Margin="5, 0, 0, 0" /> </StackLayout> <StackLayout Orientation="Horizontal"> <Label Text="Country: " FontSize="14" TextColor="#58666e" /> <Label Text="{Binding Country}" FontSize="14" TextColor="#98a6ad" Margin="5, 0, 0, 0" /> </StackLayout> <StackLayout Orientation="Horizontal"> <Label Text="Network: " FontSize="14" TextColor="#58666e" /> <Label Text="{Binding Network}" FontSize="14" TextColor="#98a6ad" Margin="5, 0, 0, 0" /> </StackLayout> </StackLayout> </Grid> </ViewCell.View> </ViewCell> </DataTemplate> </ResourceDictionary> </ContentPage.Resources> <ListView ItemTemplate="{StaticResource TopSeriesTemplate}" ItemsSource="{Binding Path=TopSeries}" RowHeight="200"/> </ContentPage>
如果你已經(jīng)知道Xamarin Forms(或一般的XAML),你應(yīng)該會覺得這段代碼很容易理解:頁面包含一個ListView 控件、一個描述單個電視節(jié)目的模板。我們展示節(jié)目的海報,還有一些其他信息,如標(biāo)題、運行時、生產(chǎn)國家等等。因為(根據(jù)命名約定)MainPageViewModel 類已經(jīng)設(shè)置為頁面的BindingContext ,我們可以通過綁定ListView 的ItemsSource屬性和我們之前在ViewModel填充的TopSeries集合進(jìn)行簡單地連接。
導(dǎo)航與參數(shù)
我們已經(jīng)看到了如何利用OnNavigatedTo()方法來執(zhí)行數(shù)據(jù)加載,但通常這種方法在另一個場景中也是有用的:檢索參數(shù)通過前一頁,這通常需要了解當(dāng)前的上下文(在我們的示例中,在我們的應(yīng)用程序的詳細(xì)信息頁面,我們需要理解用戶已經(jīng)選擇的電視節(jié)目)。
Prism支持這個特性是由于一個稱為NavigationParameters的類稱,可以作為NavigationService的NavigationAsync()方法的一個可選參數(shù)傳遞,它被自動包括作為OnNavigatedTo()和OnNavigatedFrom()事件的參數(shù)。讓我們看看如何通過向我們的應(yīng)用程序添加詳細(xì)信息頁面利用這個特性,顯示選擇的節(jié)目的一些額外的信息。
第一步是同時添加一個新頁面到Views 文件夾中(稱為DetailPage.xaml)和一個新類到ViewModels文件夾中(稱為DetailPageViewModel.cs)。你需要記住,每一頁都需要在App類的容器中注冊,在OnRegisterTypes()方法內(nèi):
protected override void RegisterTypes() { Container.RegisterTypeForNavigation<MainPage>(); Container.RegisterTypeForNavigation<DetailPage>(); Container.RegisterType<ITsApiService, TsApiService>(); }
由于命名約定,我們不需要做任何特別的操作:新頁面和新ViewModel已經(jīng)連接。現(xiàn)在我們需要通過ListView控件中所選條目到新頁面。讓我們先看看如何在主頁處理選擇。通過使用由我親愛的朋友Corrado Cavalli創(chuàng)建的庫,我們會得到一些幫助,它允許你在Xamarin Forms應(yīng)用程序?qū)崿F(xiàn)行為??捎玫男袨橹械?strong>EventToCommand允許我們連接暴露于控件的任何事件與ViewModel中定義的命令。我們要用它來連接ListView 控件的ItemTapped 事件(當(dāng)用戶點擊列表中的一個項目時會觸發(fā))與我們要在MainPageViewModel中創(chuàng)建來觸發(fā)導(dǎo)航到詳細(xì)頁面的命令。
你可以從NuGet安裝由Corrado創(chuàng)建的套包,它的名字叫Corcav.Behaviors。使用它你需要添加一個額外的名稱空間到MainPage的root,像下面這個示例:
<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" xmlns:behaviors="clr-namespace:Corcav.Behaviors;assembly=Corcav.Behaviors" prism:ViewModelLocator.AutowireViewModel="True" x:Class="InfoSeries.Views.MainPage" Title="Info Series"> ... </ContentPage>
然后你可以申請ListView 控件的行為,就像你在普通Windows應(yīng)用程序中會做的一樣:
<ListView ItemTemplate="{StaticResource TopSeriesTemplate}" ItemsSource="{Binding Path=TopSeries}" RowHeight="200"> <behaviors:Interaction.Behaviors> <behaviors:BehaviorCollection> <behaviors:EventToCommand EventName="ItemTapped" Command="{Binding GoToDetailPage}" /> </behaviors:BehaviorCollection> </behaviors:Interaction.Behaviors> </ListView>
由于這種行為,我們已經(jīng)連接了ListView 控件的ItemTapped 事件與我們要在ViewModel定義的稱為GoToDetailPage 的命令。從一個框架的角度,Prism沒有做任何不尋常的事幫助開發(fā)者實現(xiàn)命令:它只是提供了一個稱為DelegateCommand的類,這允許你定義操作來執(zhí)行調(diào)用命令和可選的條件來啟動命令。如果你有一些MVVM Light以往的經(jīng)驗,它會以RelayCommand 類那樣完全相同的方式運作。以下是我們的命令在MainPageViewModel 類的樣子:
private DelegateCommand<ItemTappedEventArgs> _goToDetailPage; public DelegateCommand<ItemTappedEventArgs> GoToDetailPage { get { if (_goToDetailPage == null) { _goToDetailPage = new DelegateCommand<ItemTappedEventArgs>(async selected => { NavigationParameters param = new NavigationParameters(); param.Add("show", selected.Item); await _navigationService.NavigateAsync("DetailPage", param); }); } return _goToDetailPage; } }
我們已經(jīng)創(chuàng)建了的命令是一個參數(shù)化命令;事實上,屬性類型是DelegateCommand< ItemTappedEventArgs >:這種方式,在方法內(nèi)部,我們獲得存儲在Item 屬性中的選中的條目。命令觸發(fā)時調(diào)用的方法展示了如何用參數(shù)的工作原理導(dǎo)航:首先我們創(chuàng)建一個新的NavigationParameters對象,最后,只不過是一個你可以存儲鍵/值對的字典。因此,我們只需添加一個新項,作為關(guān)鍵,關(guān)鍵字show ,作為值,選中的項的類型是SerieFollowersVM。這是與我們在App類中看到的導(dǎo)航的唯一的區(qū)別:其余的都是一樣的,這意味著我們調(diào)用NavigationService的theNavigateAsync()方法,傳遞標(biāo)識詳細(xì)信息頁面(DetailPage)和參數(shù)的關(guān)鍵參數(shù)。
重要事項!在App類中,我們能夠自動使用NavigationService ,因為它繼承自PrismApplication 類。如果我們要在ViewModel中使用NavigationService (像在這種情況下),我們需要使用基于依賴注入(dependency injection)的傳統(tǒng)方法。NavigationService 實例已經(jīng)在Prism容器注冊,所以我們只需要添加一個INavigationService 參數(shù)到MainPageViewModel的公共構(gòu)造函數(shù):
public MainPageViewModel(TsApiService apiService, INavigationService navigationService) { _apiService = apiService; _navigationService = navigationService; }
既然我們已經(jīng)完成了導(dǎo)航到詳細(xì)頁面,我們就需要檢索DetailPageViewModel 類的參數(shù)。第一步,像我們?yōu)?strong>MainPageViewModel做的那樣,讓它從INavigationAware 接口繼承,除了BindableBase 類。通過這種方式,我們可以訪問OnNavigatedTo()事件:
public class DetailPageViewModel : BindableBase, INavigationAware { private SerieFollowersVM _selectedShow; public SerieFollowersVM SelectedShow { get { return _selectedShow; } set { SetProperty(ref _selectedShow, value); } } public DetailPageViewModel() { } public void OnNavigatedFrom(NavigationParameters parameters) { } public void OnNavigatedTo(NavigationParameters parameters) { SelectedShow = parameters["show"] as SerieFollowersVM; } }
前面的代碼顯示了如何處理我們從主頁收到的參數(shù):同一個我們通過的MainPageViewModel對象到作為 OnNavigatedTo()方法的參數(shù)傳遞的NavigateAsync()方法。因此,我們可以用show 鍵簡單的檢索先前存儲的項。在這種情況下,因為我們預(yù)計SerieFollowersVM類型的對象,我們可以執(zhí)行一個計算并將其存儲到稱為SelectedShow的ViewModel的屬性中。多虧了這個屬性,我們可以利用綁定到選擇顯示的各種信息連接到XAML頁面的空間。以下是DetailPage.xaml的樣子:
<?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" Title="{Binding Path=SelectedShow.Name}" x:Class="InfoSeries.Views.DetailPage"> <StackLayout> <Image x:Name="InfoPoster" Source="{Binding Path=SelectedShow.Images.Fanart}" Aspect="AspectFill" /> <Label Text="{Binding Path=SelectedShow.Overview}" LineBreakMode="WordWrap" FontSize="13" TextColor="#98a6ad" Margin="15" /> </StackLayout> </ContentPage>
內(nèi)容很簡單:我們顯示show的圖片(存儲在SelectedShow.Images.Fanart屬性)和一段簡要描述(存儲在SelectedShow.Overview屬性)。
結(jié)束語
在這篇文章中,我們已經(jīng)看到在用Prism 作為MVVM框架創(chuàng)建的Xamarin Forms應(yīng)用程序中處理導(dǎo)航和依賴注入的一些基本概念。在下一篇文章中,我們將看到幾個高級場景,有關(guān)導(dǎo)航和特定于平臺的代碼的處理。你能在GitHub存儲庫找到這篇文章使用的示例應(yīng)用程序(為了方便各位讀者,小編已經(jīng)為大家整理了,請點擊這里下載)。
本文翻譯自:Prism for Xamarin Forms Part II: Basic Navigation and Dependency Injection