- 首先,我們打開 Visual Studio 2017 (任何一種版本 Visual Studio Community 2017 / Visual Studio Professional 2017 / Visual Studio Enterprise 2017皆可)
- 點選 Visual Studio 2017 功能表的 [檔案] > [新增] > [專案]
- 當出現 [新增專案] 對話窗,請依序選擇 [已安裝] > [Visual C#] > [Cross-Platform] > [Mobile App (Xamarin.Forms)]
- 請在最下方 [名稱] 文字輸入盒欄位,輸入您想要的專案名稱
- 最後,請點選 [確定] 按鈕
- 現在,Visual Studio 2017 將會顯示 [New Cross Platform App - XFQStart] 這個對話窗,請在這個對話窗中點選 [Master Detail] 圖示,接著在下方的 Platform 欄位中,選擇您想要跨平台的目標,在這裡,我們將會選擇 Android iOS Windows(UWP) 這三個所有的行動作業系統平台;而在 [Code Sharing Strategy] 程式碼共用策略欄位中,請您選擇 [.NET Standard],說明我們將要使愈 SCL (.NET Standard Class Library) 類別庫的方式來時做出我們頁面與商業邏輯的共用程式碼 (在此之前,我們所使用的共用程式碼策略將會是 PCL Portable Class Library,也就是可攜式類別庫)。
- 當然,您也可以選擇使用 Shared Project 的 [Code Sharing Strategy] 共用程式碼策略,來完成在不同行動作業系統平台下的共同商業邏輯的程式碼共用設計方式,Shared Project 與 .NET Standard 兩者的作法不盡相同,前者使用類似專案捷徑的方式來設計,這些共用程式碼將會再編譯時期來進行與原生專案的一起編譯,而後者則是一個類別庫的架構,也就是說,您所設計的所有共用程式碼,都會建立在一個類別庫內,只要同時與猿聲專案一同佈署到手機裝置中,就可以執行了。
- 確認無誤之後,請點選 [OK] 按鈕。
- 此時,Visual Studio 2017 將會開始幫您建立起四個專案,分別是:
- Xamarin.Android 原生專案
- Xamarin.iOS 原生專案
- Windows UWP 原生專案
- .NET Standard Class Library SCL 共用類別庫專案
- 現在,我們可以從 Visual Studio 2017 的方案總管中,看到已經產生了四個專案,如下圖所示,其中,當我們使用 Xamarin.Forms 進行專案開發的時候,原則上大多只會在 SCL 專案上進行程式碼與 XAML 的設計,也就是 .NET Standard Class Library 類別庫內進行程式開發。
- 在這裡,我們展開了 SCL ( .NET 標準類別庫 ) 專案節點,您可以從下圖看到這樣的成果,在這裡,多了許多資料夾,其中,您會看到 MVVM 開發模式會用到的資料夾 Models - Views - ViewModels,把這三個資料夾的大寫字母組合起來,就是 MVVM 囉。
- 在 Services 資料夾內的兩個檔案,則是提供集合資料的服務類別。
- 現在,讓我們來看看這個專案是如何實做出來的。
- 首先,我們來看看要提供 ListView 的集合資料物件的類別,要做些事情。
- 請開啟 IDataStore.cs 檔案,這個檔案內定義了 IDataStore 介面,我們可以看到了這個頁面宣告了 CRUD 的操作所需要時做出來的方法介面,而這個介面中宣告的方法,都是採用非同步的操作。這個介面也是一個泛型介面,也就是當要實作這個介面的時候,也需要提供這個介面所需要的泛型指定型別。
public interface IDataStore<T>
{
Task<bool> AddItemAsync(T item);
Task<bool> UpdateItemAsync(T item);
Task<bool> DeleteItemAsync(T item);
Task<T> GetItemAsync(string id);
Task<IEnumerable<T>> GetItemsAsync(bool forceRefresh = false);
}
public class Item
{
public string Id { get; set; }
public string Text { get; set; }
public string Description { get; set; }
}
- 因此,這個檔案 MockDataStore.cs 中的 MockDataStore 類別,就是實作了 IDataStore 這個介面,並且指定了 Item 為這個介面的泛型型別。
- MockDataStore 類別的建構式,將會預設產生一些測試用的集合紀錄。
- 關於 CRUD 操作的方法實作,則分別定義在 AddItemAsync , GetItemAsync, UpdateItemAsync , DeleteItemAsync ;由於這些介面的實作方法在宣告的時候,都是使用非同步的方式來宣告,因此,我們需要使用 Task.FromResult.aspx) 方法來建立已成功完成且具有指定之結果。
public class MockDataStore : IDataStore<Item>
{
List<Item> items;
public MockDataStore()
{
items = new List<Item>();
var mockItems = new List<Item>
{
new Item { Id = Guid.NewGuid().ToString(), Text = "First item", Description="This is an item description." },
new Item { Id = Guid.NewGuid().ToString(), Text = "Second item", Description="This is an item description." },
new Item { Id = Guid.NewGuid().ToString(), Text = "Third item", Description="This is an item description." },
new Item { Id = Guid.NewGuid().ToString(), Text = "Fourth item", Description="This is an item description." },
new Item { Id = Guid.NewGuid().ToString(), Text = "Fifth item", Description="This is an item description." },
new Item { Id = Guid.NewGuid().ToString(), Text = "Sixth item", Description="This is an item description." },
};
foreach (var item in mockItems)
{
items.Add(item);
}
}
public async Task<bool> AddItemAsync(Item item)
{
items.Add(item);
return await Task.FromResult(true);
}
public async Task<bool> UpdateItemAsync(Item item)
{
var _item = items.Where((Item arg) => arg.Id == item.Id).FirstOrDefault();
items.Remove(_item);
items.Add(item);
return await Task.FromResult(true);
}
public async Task<bool> DeleteItemAsync(Item item)
{
var _item = items.Where((Item arg) => arg.Id == item.Id).FirstOrDefault();
items.Remove(_item);
return await Task.FromResult(true);
}
public async Task<Item> GetItemAsync(string id)
{
return await Task.FromResult(items.FirstOrDefault(s => s.Id == id));
}
public async Task<IEnumerable<Item>> GetItemsAsync(bool forceRefresh = false)
{
return await Task.FromResult(items);
}
}
- 請在方案總管中的 SCL 共用類別庫專案內,找到 MainPage.xaml 這個節點,使用滑鼠雙擊打開這個檔案;您將會看到這個應用程式頁面是宣告採用 TabbedPage 這種頁面,而不是我們經常看到的 ContentPage;因為我們在這裡使用了 TabbedPage 標籤式頁面,所以,我們需要使用 TabbedPage.Children 這個標籤頁面屬性來定義出,這個標籤頁面內擁有的其他頁面,在這裡,我們指定了兩個 NavigationPage 頁面。第一個 NavigationPage 頁面裡面則是嵌入 ItemsPage,第二個 NavigationPage 頁面裡面則是嵌入 AboutPage 頁面。
- 這個 MainPage 的頁面並沒有任何商業邏輯控制程式碼,要顯示哪個頁面,則是由使用者自行操作手機螢幕。
<?xml version="1.0" encoding="utf-8" ?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:XFMDQStart.Views"
x:Class="XFMDQStart.Views.MainPage">
<TabbedPage.Children>
<NavigationPage Title="Browse">
<NavigationPage.Icon>
<OnPlatform x:TypeArguments="FileImageSource">
<On Platform="iOS" Value="tab_feed.png"/>
</OnPlatform>
</NavigationPage.Icon>
<x:Arguments>
<views:ItemsPage />
</x:Arguments>
</NavigationPage>
<NavigationPage Title="About">
<NavigationPage.Icon>
<OnPlatform x:TypeArguments="FileImageSource">
<On Platform="iOS" Value="tab_about.png"/>
</OnPlatform>
</NavigationPage.Icon>
<x:Arguments>
<views:AboutPage />
</x:Arguments>
</NavigationPage>
</TabbedPage.Children>
</TabbedPage>
- 現在,我們先還看看 AboutPage 這個頁面的相關設計,在底下是 AboutPage 頁面的 XAML 宣告內容。我們可以看到這個頁面透過了 ContentPage.BindingContext 屬性,將 ViewModel,也就是 vm:AboutViewModel (這個 XAML 用法,也就是如同 C# 的 new AboutViewModel() ,即產生一個 AboutViewModel 類別的執行個體) 綁定到這個屬性上,如此,整個頁面的控制項,將可以透過 Data Binding 資料綁定的功能,從 AboutViewModel 類別物件中取得相關內容。
- 由於我們需要在這個頁面使用 ViewModel 類別,因此,需要建立一個 XAML Namespace 命名空間, xmlns:vm="clr-namespace:XFMDQStart.ViewModels;" ,如此,在這個頁面中,就可以使用 XFMDQStart.ViewModels .NET 命名空間的類別。
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XFMDQStart.Views.AboutPage"
xmlns:vm="clr-namespace:XFMDQStart.ViewModels;"
Title="{Binding Title}">
<ContentPage.BindingContext>
<vm:AboutViewModel />
</ContentPage.BindingContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackLayout BackgroundColor="{StaticResource Accent}" VerticalOptions="FillAndExpand" HorizontalOptions="Fill">
<StackLayout Orientation="Horizontal" HorizontalOptions="Center" VerticalOptions="Center">
<ContentView Padding="0,40,0,40" VerticalOptions="FillAndExpand">
<Image Source="xamarin_logo.png" VerticalOptions="Center" HeightRequest="64" />
</ContentView>
</StackLayout>
</StackLayout>
<ScrollView Grid.Row="1">
<StackLayout Orientation="Vertical" Padding="16,40,16,40" Spacing="10">
<Label FontSize="22">
<Label.FormattedText>
<FormattedString>
<FormattedString.Spans>
<Span Text="AppName" FontAttributes="Bold" FontSize="22" />
<Span Text=" " />
<Span Text="1.0" ForegroundColor="{StaticResource LightTextColor}" />
</FormattedString.Spans>
</FormattedString>
</Label.FormattedText>
</Label>
<Label>
<Label.FormattedText>
<FormattedString>
<FormattedString.Spans>
<Span Text="This app is written in C# and native APIs using the" />
<Span Text=" " />
<Span Text="Xamarin Platform" FontAttributes="Bold" />
<Span Text="." />
</FormattedString.Spans>
</FormattedString>
</Label.FormattedText>
</Label>
<Label>
<Label.FormattedText>
<FormattedString>
<FormattedString.Spans>
<Span Text="It shares code with its" />
<Span Text=" " />
<Span Text="iOS, Android, and Windows" FontAttributes="Bold" />
<Span Text=" " />
<Span Text="versions." />
</FormattedString.Spans>
</FormattedString>
</Label.FormattedText>
</Label>
<Button Margin="0,10,0,0" Text="Learn more" Command="{Binding OpenWebCommand}" BackgroundColor="{StaticResource Primary}" TextColor="White" />
</StackLayout>
</ScrollView>
</Grid>
</ContentPage>
- 關於 AboutPage 頁面用到的 ViewModel 類別,則是使用底下程式碼定義,在這個類別中,僅定義兩個屬性 Title 的字串屬性與 OpenWebCommand 這個 ICommand 的屬性;其中,後者屬性將會在上面 XAML 中,使用語法 Command="{Binding OpenWebCommand}" 來進行命令的綁定,也就是,當使用者點選這個按鈕(按鈕文字為 Learn more),就會執行 Device.OpenUri 靜態方法,使用手機本身預設瀏覽器來開啟指定 URL 的網頁。
public class AboutViewModel : BaseViewModel
{
public AboutViewModel()
{
Title = "About";
OpenWebCommand = new Command(() => Device.OpenUri(new Uri("https://xamarin.com/platform")));
}
public ICommand OpenWebCommand { get; }
}
- 接著,我們來看看如何使用 ListView 這個控制項來顯示出集合的物件資料,在這個 ItemsPage.xaml 檔案內,我們並沒有看到使用 BindingContext 屬性來設定 ViewModel 的類別,這樣的需求將會使用 Code Behind 的方式來設定,等下我們來檢視這個頁面的 Code Behind的時候,就會看到。
- ListView 控制項的使用方式,如同一般控制項一樣,需要設定 ListView 的相關屬性,另外,還需要宣告 ListView.ItemTemplate 這個項目,在這個節點內,我們可以自訂每筆紀錄要顯示出來的樣貌,與美個控制項的大小與位置。
- ListView 控制項最為重要的屬性為 ItemsSource ,這個屬性將會透過資料綁定 Data Binding 的來指定 ViewModel 類別中的屬性 ( C# Property ),作為 ListView 要顯示的集合資料來源。
- 由於 ListView 支援手勢下更新的操作,因此,我們可以使用 ListView 的 RefreshCommand 屬性,綁定 ViewModel 內有實作 ICommand 的物件,如此,當使用者想要透過下拉更新的手式操作來進行 ListView 內的所有紀錄更新的時候,就可以直接執行 ViewModel 內的 LoadItemsCommand 物件內的委派方法。
- 為了讓 ListView 的表現更加有效率與有效的使用記憶體使用量,我們在這裡使用 ListView 的 CachingStrategy="RecycleElement" 屬性設定。
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XFMDQStart.Views.ItemsPage"
Title="{Binding Title}"
x:Name="BrowseItemsPage">
<ContentPage.ToolbarItems>
<ToolbarItem Text="Add" Clicked="AddItem_Clicked" />
</ContentPage.ToolbarItems>
<ContentPage.Content>
<StackLayout>
<ListView x:Name="ItemsListView"
ItemsSource="{Binding Items}"
VerticalOptions="FillAndExpand"
HasUnevenRows="true"
RefreshCommand="{Binding LoadItemsCommand}"
IsPullToRefreshEnabled="true"
IsRefreshing="{Binding IsBusy, Mode=OneWay}"
CachingStrategy="RecycleElement"
ItemSelected="OnItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Padding="10">
<Label Text="{Binding Text}"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemTextStyle}"
FontSize="16" />
<Label Text="{Binding Description}"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemDetailTextStyle}"
FontSize="13" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
- 接下來,我們來看看 ItemsPage 頁面的 Code Behind 程式碼,請打開 ItemsPage.xaml.cs 這個節點,就會看到如下程式碼。
- 在建構式中, BindingContext = viewModel = new ItemsViewModel(); ,這行程式碼就是用來設定這個頁面要進行資料綁定的來源,是這個頁面的 ViewModel 類別,也就是 ItemsViewModel 。
- 在 Code Behind 中,這裡設定了三個觸發事件的處理商業邏輯。
- OnItemSelected 這個事件會當使用者點選 ListView 的任何一筆紀錄的時候,就會觸發與執行這個事件所綁定的方法,在這裡,將會取得使用者點選的項目物件,建立 ItemDetailPage 物件,並且在建構函式內將這個物件傳送過去,接著,透過 Navigation 導航物件,進行頁面切換與導航的工作。
-
- OnAppearing 事件會在這個頁面顯示在螢幕上的時候,便會觸發執行(例如,按下 Home 按鈕,這個應用程式就會變成背景模式,若您重新切換顯示這個 App,而當時的頁面正好有 OnAppearing 事件,此時,OnAppearing 則又會被觸發一次)
public partial class ItemsPage : ContentPage
{
ItemsViewModel viewModel;
public ItemsPage()
{
InitializeComponent();
BindingContext = viewModel = new ItemsViewModel();
}
async void OnItemSelected(object sender, SelectedItemChangedEventArgs args)
{
var item = args.SelectedItem as Item;
if (item == null)
return;
await Navigation.PushAsync(new ItemDetailPage(new ItemDetailViewModel(item)));
ItemsListView.SelectedItem = null;
}
async void AddItem_Clicked(object sender, EventArgs e)
{
await Navigation.PushModalAsync(new NavigationPage(new NewItemPage()));
}
protected override void OnAppearing()
{
base.OnAppearing();
if (viewModel.Items.Count == 0)
viewModel.LoadItemsCommand.Execute(null);
}
}
- 下面的程式碼就是 ItemsPage 這個頁面的 ViewModel,在這個建構函式中,特別有使用 MessagingCenter.Subscribe 來訂閱一個 AddItem 的事件,這個事件將會於當使用者在新增一筆新紀錄的頁面中,點選了儲存按鈕時候,會使用 MessagingCenter.Send(this, "AddItem", Item); 來觸發這個事件。 MessagingCenter 這個類別在 Xamarin.Forms 專案開發的時候,將會扮演相當重要的腳色,用來支援再不同頁面、不同類別、不同類別庫之間,可以使用事件出發的方式,通知有訂閱這個事件的物件,執行相對應的委派方法。
- ExecuteLoadItemsCommand 命令委派方法,則是當要進行讀取集合紀錄到 ListView 中,才會被執行的。
public class ItemsViewModel : BaseViewModel
{
public ObservableCollection<Item> Items { get; set; }
public Command LoadItemsCommand { get; set; }
public ItemsViewModel()
{
Title = "Browse";
Items = new ObservableCollection<Item>();
LoadItemsCommand = new Command(async () => await ExecuteLoadItemsCommand());
MessagingCenter.Subscribe<NewItemPage, Item>(this, "AddItem", async (obj, item) =>
{
var _item = item as Item;
Items.Add(_item);
await DataStore.AddItemAsync(_item);
});
}
async Task ExecuteLoadItemsCommand()
{
if (IsBusy)
return;
IsBusy = true;
try
{
Items.Clear();
var items = await DataStore.GetItemsAsync(true);
foreach (var item in items)
{
Items.Add(item);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsBusy = false;
}
}
}
- 打開 ItemDetailPage.xaml 頁面宣告,這裡非常簡單的使用 StackLayout 堆疊版面配置的方式,顯示出4個 Label 文字控制項的內容,而這些文字控制項的內容,將會透過資料綁定的方式,從 ViewModel 中取得
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XFMDQStart.Views.ItemDetailPage"
Title="{Binding Title}">
<StackLayout Spacing="20" Padding="15">
<Label Text="Text:" FontSize="Medium" />
<Label Text="{Binding Item.Text}" FontSize="Small"/>
<Label Text="Description:" FontSize="Medium" />
<Label Text="{Binding Item.Description}" FontSize="Small"/>
</StackLayout>
</ContentPage>
- 接下來,我們來查看這個頁面的 Code Behind,請打開 ItemDetailPage.xaml.cs 節點;在這個 Code Behind 中,有兩個建構函式,若為沒有參數的建構函式,將會使用 自行產生一個 ItemDetailViewModel 物件,來指定這個頁面要用到的 ViewModel;若是有參數的建構函式,將會直接指定這個串送進來的物件,作為該頁面的 ViewModel。
public partial class ItemDetailPage : ContentPage
{
ItemDetailViewModel viewModel;
public ItemDetailPage(ItemDetailViewModel viewModel)
{
InitializeComponent();
BindingContext = this.viewModel = viewModel;
}
public ItemDetailPage()
{
InitializeComponent();
var item = new Item
{
Text = "Item 1",
Description = "This is an item description."
};
viewModel = new ItemDetailViewModel(item);
BindingContext = viewModel;
}
}
- 這個頁面的 ViewMOdel 則是相當的簡單,他就是有兩個屬性 Title 與 Item。
public class ItemDetailViewModel : BaseViewModel
{
public Item Item { get; set; }
public ItemDetailViewModel(Item item = null)
{
Title = item?.Text;
Item = item;
}
}
- 最後,我們來看看 NewItemPage.xaml 這個頁面,也就是當您點選新增紀錄後,所會顯示的頁面,在這裡,使用了 ContentPage.ToolbarItems 宣告了一個導航工具列的按鈕,也就是 Save 儲存按鈕,當使用者點選了這個儲存按鈕,就會將這筆紀錄透過 MessagingCenter 發送出一個事件,通知有訂閱這個事件的物件,要將這筆紀錄儲存起來。
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XFMDQStart.Views.NewItemPage"
Title="New Item">
<ContentPage.ToolbarItems>
<ToolbarItem Text="Save" Clicked="Save_Clicked" />
</ContentPage.ToolbarItems>
<ContentPage.Content>
<StackLayout Spacing="20" Padding="15">
<Label Text="Text" FontSize="Medium" />
<Entry Text="{Binding Item.Text}" FontSize="Small" />
<Label Text="Description" FontSize="Medium" />
<Editor Text="{Binding Item.Description}" FontSize="Small" Margin="0" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
- 打開 NewItemPage.xaml.cs 節點,就會看到這個頁面的 Code Behind 程式碼;在這個 Code Behind 程式碼中,將會在建構函式內,設定這個頁面需要用到的 ViewModel 物件,也就是這個頁面物件本身 BindingContext = this;
- 另外,也設定當使用者點選了導航工具列按鈕之後,所觸發的按鈕 Clicked 事件程式碼。
public partial class NewItemPage : ContentPage
{
public Item Item { get; set; }
public NewItemPage()
{
InitializeComponent();
Item = new Item
{
Text = "Item name",
Description = "This is an item description."
};
BindingContext = this;
}
async void Save_Clicked(object sender, EventArgs e)
{
MessagingCenter.Send(this, "AddItem", Item);
await Navigation.PopModalAsync();
}
}
進階研讀
Xamarin.Forms 快速上手
電子書,請點選 這裡XAML in Xamarin.Forms 基礎篇
電子書,請點選 這裡經過一系列的初步認識 Xamarin 原生方式開發與 Xamarin.Forms 方式開發的方式,大部分第一次接觸 Xamarin Toolkit 工具集的人,都會開始存在著許多問題,我究竟要使用原生方式來進行專案開發,還是要採用 Xamarin.Forms 的方式來進行開發呢?在這篇文章中,將會提供這些相關疑問的解釋說明。