2017/9/8

Xamarin.Forms 教學系列文(二十四.貳 - 3)Navigation 參數傳遞 - Messaging center & ViewModel


學習目標
  • Messaging center 應用程式內部推播
  • ViewModel ... 不熟的請回去看 18章

這一小節提到的兩個都是較特別的方法,

例如 ViewModel,我認為不太算正統的參數傳遞,而是一種利用 MVVM 達到我們目的的方法。

而 Messaging center 有點像是 App 內部推播,利用接收者(Subscribe) 發送者(Send) 的關係來傳遞參數,其用法也相當特別~


Messaging Center

Messaging Center 有三個方法可用:
  • Subscribe (接收者)
  • Send (發送者)
  • Unsubscribe (釋放接收者的資源)

你可以同時寫多個接收者(Subscribe)發送者(Send) 只要一發送訊息,所有 ID 一樣的接收者就能同時接到訊息。
行為就很像是推播,只是限於 App 內部使用。訊息的內容不限制任何型態的物件。

而接收者需要有 兩個條件 成立,才能接收到發送者送出的訊息:
  1. ID 一樣
  2. 傳送接收的參數型態一樣
//資料型態不一樣,即使 ID 一樣,接收者也收不到資料~

寫好接收者後,他會一直等待發送者發送訊息,那如果不會再接收訊息了,可以用 Unsubscribe 方法將資源釋放掉。

來看程式,一樣是 HomePage 和 InfoPage

我們在 HomePage 點選 ListView 項目時,放了一個 發送者,去通知 InfoPage 的接收者,並更改畫面上的資料。

HomePage
public partial class DataTransfer2HomePage : ContentPage
{
    var list = new ObservableCollection<Information>();

    public DataTransfer2HomePage()
    {
        InitializeComponent();

        listView.ItemsSource = list;

        // 等待 ID為"InformationReady" 的發送者送出訊息.
        MessagingCenter.Subscribe<Datatransfer2infopage,Information>
            (this, "InformationReady", (sender, info) => 
            {
                int index = list.IndexOf(info);
                if (index != -1)
                {
                    list[index] = info;
                }
                else
                {
                    list.Add(info);
                }
            });
    }

    async void OnGetInfoButtonClicked(object sender, EventArgs args)
    {
        await Navigation.PushAsync(new DataTransfer2InfoPage());
    }

    // ListView 項目點選時
    async void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs args)
    {
        if (args.SelectedItem != null)
        {
            // Deselect the item. 
            listView.SelectedItem = null;
            DataTransfer2InfoPage infoPage = new DataTransfer2InfoPage();
            await Navigation.PushAsync(infoPage);

            // 發送訊息給 ID為"InitializeInfo"的接收者,並把 SelectedItem 物件傳遞出去
            MessagingCenter.Send<Datatransfer2homepage, Information>
                (this, "InitializeInfo", (Information)args.SelectedItem);
        }
    }
}

InfoPage
public partial class DataTransfer2InfoPage : ContentPage
{
    Information info = new Information();
    public DataTransfer2InfoPage()
    {
        InitializeComponent();

        // 接收 "InitializeInfo" 發送的訊息.
        MessagingCenter.Subscribe<Datatransfer2homepage, Information> 
            (this, "InitializeInfo", (sender, info) =>
            {
                // 處理從 HomePage 傳過來的 info 物件
                this.info = info;

                nameEntry.Text = info.Name ?? "";
                emailEntry.Text = info.Email ?? "";

                if (!String.IsNullOrWhiteSpace(info.Language))
                {
                    languagePicker.SelectedIndex = languagePicker.Items.IndexOf(info.Language);
                }

                datePicker.Date = info.Date;

                // 釋放 Subscribe
                MessagingCenter.Unsubscribe<Datatransfer2homepage, Information>
                (this, "InitializeInfo");
            });
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        
        info.Name = nameEntry.Text;
        info.Email = emailEntry.Text;
        int index = languagePicker.SelectedIndex;

        info.Language = index == -1 ? null : languagePicker.Items[index];
        info.Date = datePicker.Date;

        // 發送訊息回 HomePage 的接收者,更新 ListView 資料
        MessagingCenter.Send<Datatransfer2infopage, Information>
            (this, "InformationReady", info);
    }
}

ViewModel

InfoPage 新增或編輯完,再通知 HomePage 的 ListView 做修改,這樣的特性本身就有改為 MVVM 的潛能...

這段範例比較特殊的是,新增時會先在 HomePage ListView 內加一筆空的 info 物件
InfoPage 輸入資料時,MVVM 的機制會去通知 HomePage 的那筆 info 物件做同步更新。

這邊提出一個問題,若使用者今天點新增後沒有做任何事情就返回上一頁,是否要再加入刪除的動作?

範例結合了兩個學過的技能,一個是 App 全域變數,另一個則是 MVVM

既然要用 MVVM 就得先建立 ViewModel~

ViewModel
public class InformationViewModel : ViewModelBase
{
    string name, email, language;
    DateTime date = DateTime.Today;

    public string Name
    {
        set { SetProperty(ref name, value); }
        get { return name; }
    }

    public string Email
    {
        set { SetProperty(ref email, value); }
        get { return email; }
    }

    public string Language
    {
        set { SetProperty(ref language, value); }
        get { return language; }
    }

    public DateTime Date
    {
        set { SetProperty(ref date, value); }
        get { return date; }
    }
}

為了等等方便 Binding,再來建立一個 AppData 類別,裡面包含 DataSource 和 CurrentInfo:
public class AppData
{
    public AppData()
    {
        InfoCollection = new ObservableCollection<Informationviewmodel>();
    }

    public IList<Informationviewmodel> InfoCollection { private set; get; }

    public InformationViewModel CurrentInfo { set; get; }
}

App
定義全域變數並初始化 AppData 物件
public class App : Application
{
    public App()
    {
        // 確保有連結到 toolkit library. 頗特別的這裡
        new Xamarin.FormsBook.Toolkit.ObjectToIndexConverter<object>();

        // 初始化 AppData
        AppData = new AppData();

        // Go to the home page. 
        MainPage = new NavigationPage(new DataTransfer5HomePage());
    }

    public AppData AppData { private set; get; } 
    … 
}


HomePage
XAML:
直接綁定 Application.Current 的 AppData
<!--直接綁定 Application.Current 的 AppData-->
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="DataTransfer5.DataTransfer5HomePage" 
             Title="Home Page" 
             BindingContext="{Binding Source={x:Static Application.Current},Path=AppData}">

    <Grid>
        <Button Text="Add New Item"
                Grid.Row="0" 
                FontSize="Large" 
                HorizontalOptions="Center" 
                VerticalOptions="Center" 
                Clicked="OnGetInfoButtonClicked" />
        
        <!--可以注意下 ItemSource-->
        <ListView x:Name="listView"
                  Grid.Row="1"
                  ItemsSource="{Binding InfoCollection}" 
                  ItemSelected="OnListViewItemSelected">
            
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <StackLayout Orientation="Horizontal">
                            <Label Text="{Binding Name}" />
                            <Label Text=" / " />
                            <Label Text="{Binding Email}" />
                            <Label Text=" / " />
                            <Label Text="{Binding Language}" />
                            <Label Text=" / " />
                            <Label Text="{Binding Date, StringFormat='{0:d}'}" />
                        </StackLayout>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</ContentPage>

.cs
多了一個 GoToInfoPage 方法,執行新增或編輯並導向 InfoPage:
public partial class DataTransfer5HomePage : ContentPage
{
    public DataTransfer5HomePage()
    {
        InitializeComponent();
    }

    // Button Clicked handler. 
    void OnGetInfoButtonClicked(object sender, EventArgs args)
    {
        // 執行新增並導向下一頁.
        GoToInfoPage(new InformationViewModel(), true);
    }

    // ListView ItemSelected handler.
    void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs args)
    {
        if (args.SelectedItem != null)
        {
            listView.SelectedItem = null;

            // 執行編輯並導向下一頁
            GoToInfoPage((InformationViewModel)args.SelectedItem, false);
        }
    }

    async void GoToInfoPage(InformationViewModel info, bool isNewItem)
    {
        // 從 BindingContext 取得 AppData
        AppData appData = (AppData)BindingContext;

        // 指派給 AppData.CurrentInfo
        appData.CurrentInfo = info;

        // 導向 InfoPage
        await Navigation.PushAsync(new DataTransfer5InfoPage());

        // 若是新增
        if (isNewItem)
        {
            // 新增一筆空 item 到 ListView 內
            appData.InfoCollection.Add(info);
        }
    }
}
要注意的是新增一筆 appData.InfoCollection.Add(info)PushAsync 後面執行,這樣才不會下一頁還沒出現,ListView 就先多了一筆空項目...

InfoPage
只要!!!!! 將 XAML 綁定好就結束了
<!--綁定 AppData.CurrentInfo-->
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:toolkit= "clr-namespace:Xamarin.FormsBook.Toolkit;assembly=Xamarin.FormsBook.Toolkit" 
             x:Class="DataTransfer5.DataTransfer5InfoPage" 
             Title="Info Page"
             BindingContext="{Binding Source={x:Static Application.Current}, Path=AppData.CurrentInfo}">
   
    <StackLayout Padding="20, 0" Spacing="20">
        <Entry Text="{Binding Name}" Placeholder="Enter Name" />
        <Entry Text="{Binding Email}" Placeholder="Enter Email Address" />
        <Picker x:Name="languagePicker" Title="Favorite Programming Language">
            <Picker.Items>
                <x:String>C#</x:String>
                <x:String>F#</x:String>
                <x:String>Objective C</x:String>
                <x:String>Swift</x:String>
                <x:String>Java</x:String>
            </Picker.Items>
            <Picker.SelectedIndex>
                <Binding Path="Language">
                    <Binding.Converter>
                        <toolkit:ObjectToIndexConverter x:TypeArguments="x:String">
                            <x:String>C#</x:String>
                            <x:String>F#</x:String>
                            <x:String>Objective C</x:String>
                            <x:String>Swift</x:String>
                            <x:String>Java</x:String>
                        </toolkit:ObjectToIndexConverter>
                    </Binding.Converter>
                </Binding>
            </Picker.SelectedIndex>
        </Picker>
        <DatePicker Date="{Binding Date}" />
    </StackLayout>
</ContentPage>

MVVM 雖然前置作業多了一些 (建立 ViewModel),但實際上在撰寫和維護時會相對的較便利,不用陷入事件處理和顯示資料的泥沼~

不熟的同學再回 18 章複習一下!!

沒有留言:

張貼留言