Xamarin Forms的Prism第二部分:基本导航和依赖注入(Dependency Injection)模式
在前一篇文章中,我们已经开始介绍在Xamarin Forms应用程序中如何利用Prism(6.2)的新版本来实现MVVM模式的基本概念。到目前为止,我们还没有看到什么特别的东西是我们用另一个框架做不到的:我们在上一篇文章中创建了一个视图(View)、一个视图模型(ViewModel),然后我们通过绑定连接它们。在这篇文章中,我们将看到Prism如何帮助处理一个在MVVM应用程序中很难处理的非常常见的场景:导航和页面的生命周期。
正如我们在前一篇文章中提到的,我们要为TrackSeries——一个提供电视节目信息的网站,创建一个简单的客户端。该应用程序将显示当前的顶级系列,将允许用户发现更多关于它的内容。为了实现这一目标,我们可以用一组网站提供的REST服务,这是非常简单的使用和处理REST服务的遵循标准的最佳实践:你调用一个使用HTTP命令的URL,接收返回一个JSON响应结果。
举个例子,如果你想知道哪些是顶级系列,你可以执行一个HTTP GET请求到以下URL:。服务将返回给你一个JSON响应,包含顶级系列的所有细节:
[ { "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":"//static.trackseries.tv/banners/posters/121361-49.jpg", "fanart":"//static.trackseries.tv/banners/fanart/original/121361-15.jpg", "banner":"//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":"//static.trackseries.tv/banners/posters/257655-8.jpg", "fanart":"//static.trackseries.tv/banners/fanart/original/257655-47.jpg", "banner":"//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":"//static.trackseries.tv/banners/posters/153021-38.jpg", "fanart":"//static.trackseries.tv/banners/fanart/original/153021-77.jpg", "banner":"//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":"//static.trackseries.tv/banners/posters/279121-37.jpg", "fanart":"//static.trackseries.tv/banners/fanart/original/279121-23.jpg", "banner":"//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":"//static.trackseries.tv/banners/posters/80379-43.jpg", "fanart":"//static.trackseries.tv/banners/fanart/original/80379-38.jpg", "banner":"//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":"//static.trackseries.tv/banners/posters/176941-11.jpg", "fanart":"//static.trackseries.tv/banners/fanart/original/176941-3.jpg", "banner":"//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":"//static.trackseries.tv/banners/posters/263365-16.jpg", "fanart":"//static.trackseries.tv/banners/fanart/original/263365-26.jpg", "banner":"//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":"//static.trackseries.tv/banners/posters/81189-10.jpg", "fanart":"//static.trackseries.tv/banners/fanart/original/81189-21.jpg", "banner":"//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":"//static.trackseries.tv/banners/posters/247808-27.jpg", "fanart":"//static.trackseries.tv/banners/fanart/original/247808-43.jpg", "banner":"//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":"//static.trackseries.tv/banners/posters/274431-17.jpg", "fanart":"//static.trackseries.tv/banners/fanart/original/274431-22.jpg", "banner":"//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" } ]
为了使用这些应用程序中的API,我用一组方法创建了一个称为TsApiService的类,通过.NET框架和流行的JSON.NET库的HttpClient类,负责下载JSON,解析它并返回一组可以使用C#很容易地操纵的对象。为了更好地构成我的解决方案, 我已经决定把所有的通信相关类与REST API (如服务和实体)放置在另一个叫做InfoSeries.Core的便携式类库(Portable Class Library)中,这是一个与实际Xamarin Forms应用程序的相比不同的PCL。
这就是负责解析之前的JSON的方法返回一个C#对象列表:
public async Task<List<SerieFollowersVM>> GetStatsTopSeries() { using (HttpClient client = new HttpClient()) { try { var response = await client.GetAsync("//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() 方法执行GET请求到URL,返回结果包含JSON响应的字符串。这个结果存储在响应的Content 属性:如果请求成功(我们使用IsSuccessStatusCode 属性检查这种情况),我们使用Content 属性公开的ReadAsAsync< T >方法自动转换为JSON导致SerieFollowersVM 对象的集合。SerieFollowersVM 无非是一个映射JSON响应的每个属性的类 (如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的完整示例(为了方便各位读者,小编已经为大家整理了,请点击这里下载)中你会发现很多这样的类(映射各种被TrackSeries API返回的JSON响应)。此外,TsApiService 将实现另外的方法,一个用于我们想在我们的应用程序中利用的每个API的方法。我不会详细解释每个方法,因为这将超出本文的范围,你可以在GitHub上看到所有的细节。对于这篇文章的目的,你只需要知道服务只是公开了一组方法,我们可以在各种ViewModels中使用来检索可用的电视节目的信息。
注意:默认情况下,HttpClient 类没有提供一个ReadAsAsync< T >方法,能够自动对JSON响应为C#对象进行反序列化。为了获得该扩展方法,我们需要添加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(); }
现在我们有了一个服务,我们可以学习(多亏Prism)我们可以如何注册到它的依赖容器,它会自动注入在我们的ViewModels。实际上,从这个角度来看,没有什么特别强调:这与其他MVVM框架使用的方法是相同的,利用依赖注入的方法。首先,我们需要注册我们想要在容器中使用的接口和实现之间的协会。在Prism的情况下,我们需要用App类的RegisterTypes()方法,通过使用Container对象和RegisterType< T, Y >()方法(其中T 是接口,Y是具体实现):
protected override void RegisterTypes() { Container.RegisterTypeForNavigation<MainPage>(); Container.RegisterType<ITsApiService, TsApiService>(); }
当MainPage 和TsApiService 都在容器注册了,我们可以在ViewModel获得它,只需添加一个参数在公共构造函数,就像以下示例:
public class MainPageViewModel : BindableBase { private readonly ITsApiService _apiService; public MainPageViewModel(ITsApiService apiService) { _apiService = apiService; } }
MainPageViewModel 类将被加载时,我们已经在容器注册的ITsApiService实现(在我们的例子中是TsApiService 类)将自动注入构造函数的参数,允许我们以我们将在ViewModel创建的所有其他的方法和属性来使用它。使用这种方法,我们将容易改变服务的实现,以防我们需要它:它将足以改变App类的注册类型,并且每个ViewModel将自动开始使用新的版本。
处理导航的生命周期
现在我们有一个服务,它提供了一种方法来检索顶级系列的列表,在ViewModel加载时我们需要调用它。我们的目标是显示(在应用程序的主页)最热门的电视节目列表。但是,我们即将面对使用MVVM模式时的一个常见的问题:检索顶级系列列表的方法是异步的,但是随着当前实现,唯一我们可以执行数据加载的地方就是ViewModel的构造函数,它不能执行异步调用(在C#中,事实上,一个类的构造函数不能用async关键字,因此,你不能用等待前缀的方法)。在non-MVVM应用程序中,这个问题很容易解决,因为导航的生命周期方法是由每一个平台基本提供的。Xamarin Forms毫无例外,我们可以利用(在XAML页面类的后面的代码)OnAppearing()和OnDisappearing()方法:因为它们是事件,我们可以没有问题地调用异步代码。
为了解决这个问题,Prism提供一个称为INavigationAware的接口,我们可以在ViewModels实现。当我们实现它,我们可以访问OnNavigatedTo()和OnNavigatedFrom()事件,我们可以使用它们来执行数据加载或清理操作。这就是实现这个接口后我们的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); } }
正如你所看到的,现在我们实现了一个称为OnNavigatedTo()的方法,我们可以安全地执行异步调用和加载数据。我们调用TsApiService类的GetStatsTopSeries()方法,我们封装结果集合到ObservableCollection属性。这是我们要连接的属性,通过绑定到一个ListView 控件,为了在主页显示电视节目列表。
出于完整性的考虑,这是MainPage的XAML的样子:
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="//xamarin.com/schemas/2014/forms" xmlns:x="//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>
如果你已经知道Xamarin Forms(或一般的XAML),你应该会觉得这段代码很容易理解:页面包含一个ListView 控件、一个描述单个电视节目的模板。我们展示节目的海报,还有一些其他信息,如标题、运行时、生产国家等等。因为(根据命名约定)MainPageViewModel 类已经设置为页面的BindingContext ,我们可以通过绑定ListView 的ItemsSource属性和我们之前在ViewModel填充的TopSeries集合进行简单地连接。
导航与参数
我们已经看到了如何利用OnNavigatedTo()方法来执行数据加载,但通常这种方法在另一个场景中也是有用的:检索参数通过前一页,这通常需要了解当前的上下文(在我们的示例中,在我们的应用程序的详细信息页面,我们需要理解用户已经选择的电视节目)。
Prism支持这个特性是由于一个称为NavigationParameters的类称,可以作为NavigationService的NavigationAsync()方法的一个可选参数传递,它被自动包括作为OnNavigatedTo()和OnNavigatedFrom()事件的参数。让我们看看如何通过向我们的应用程序添加详细信息页面利用这个特性,显示选择的节目的一些额外的信息。
第一步是同时添加一个新页面到Views 文件夹中(称为DetailPage.xaml)和一个新类到ViewModels文件夹中(称为DetailPageViewModel.cs)。你需要记住,每一页都需要在App类的容器中注册,在OnRegisterTypes()方法内:
protected override void RegisterTypes() { Container.RegisterTypeForNavigation<MainPage>(); Container.RegisterTypeForNavigation<DetailPage>(); Container.RegisterType<ITsApiService, TsApiService>(); }
由于命名约定,我们不需要做任何特别的操作:新页面和新ViewModel已经连接。现在我们需要通过ListView控件中所选条目到新页面。让我们先看看如何在主页处理选择。通过使用由我亲爱的朋友Corrado Cavalli创建的库,我们会得到一些帮助,它允许你在Xamarin Forms应用程序实现行为。可用的行为中的EventToCommand允许我们连接暴露于控件的任何事件与ViewModel中定义的命令。我们要用它来连接ListView 控件的ItemTapped 事件(当用户点击列表中的一个项目时会触发)与我们要在MainPageViewModel中创建来触发导航到详细页面的命令。
你可以从NuGet安装由Corrado创建的套包,它的名字叫Corcav.Behaviors。使用它你需要添加一个额外的名称空间到MainPage的root,像下面这个示例:
<ContentPage xmlns="//xamarin.com/schemas/2014/forms" xmlns:x="//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应用程序中会做的一样:
<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>
由于这种行为,我们已经连接了ListView 控件的ItemTapped 事件与我们要在ViewModel定义的称为GoToDetailPage 的命令。从一个框架的角度,Prism没有做任何不寻常的事帮助开发者实现命令:它只是提供了一个称为DelegateCommand的类,这允许你定义操作来执行调用命令和可选的条件来启动命令。如果你有一些MVVM Light以往的经验,它会以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; } }
我们已经创建了的命令是一个参数化命令;事实上,属性类型是DelegateCommand< ItemTappedEventArgs >:这种方式,在方法内部,我们获得存储在Item 属性中的选中的条目。命令触发时调用的方法展示了如何用参数的工作原理导航:首先我们创建一个新的NavigationParameters对象,最后,只不过是一个你可以存储键/值对的字典。因此,我们只需添加一个新项,作为关键,关键字show ,作为值,选中的项的类型是SerieFollowersVM。这是与我们在App类中看到的导航的唯一的区别:其余的都是一样的,这意味着我们调用NavigationService的theNavigateAsync()方法,传递标识详细信息页面(DetailPage)和参数的关键参数。
重要事项!在App类中,我们能够自动使用NavigationService ,因为它继承自PrismApplication 类。如果我们要在ViewModel中使用NavigationService (像在这种情况下),我们需要使用基于依赖注入(dependency injection)的传统方法。NavigationService 实例已经在Prism容器注册,所以我们只需要添加一个INavigationService 参数到MainPageViewModel的公共构造函数:
public MainPageViewModel(TsApiService apiService, INavigationService navigationService) { _apiService = apiService; _navigationService = navigationService; }
既然我们已经完成了导航到详细页面,我们就需要检索DetailPageViewModel 类的参数。第一步,像我们为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; } }
前面的代码显示了如何处理我们从主页收到的参数:同一个我们通过的MainPageViewModel对象到作为 OnNavigatedTo()方法的参数传递的NavigateAsync()方法。因此,我们可以用show 键简单的检索先前存储的项。在这种情况下,因为我们预计SerieFollowersVM类型的对象,我们可以执行一个计算并将其存储到称为SelectedShow的ViewModel的属性中。多亏了这个属性,我们可以利用绑定到选择显示的各种信息连接到XAML页面的空间。以下是DetailPage.xaml的样子:
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="//xamarin.com/schemas/2014/forms" xmlns:x="//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>
内容很简单:我们显示show的图片(存储在SelectedShow.Images.Fanart属性)和一段简要描述(存储在SelectedShow.Overview属性)。
结束语
在这篇文章中,我们已经看到在用Prism 作为MVVM框架创建的Xamarin Forms应用程序中处理导航和依赖注入的一些基本概念。在下一篇文章中,我们将看到几个高级场景,有关导航和特定于平台的代码的处理。你能在GitHub存储库找到这篇文章使用的示例应用程序(为了方便各位读者,小编已经为大家整理了,请点击这里下载)。
本文翻译自: