Xamarin.FormsでRxのメッセージ伝搬


ViewModelから呼び出すロジックを2層に分けて、最下層レイヤーからViewModelまでメッセージを伝搬させてみる。

[livedoorの天気API](http://weather.livedoor.com/weather_hacks/webservice)からJSONデータを取得して画面に表示する。

## コード

### View
```xml: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"
	x:Class="MyApp.Views.MainPage">

	<StackLayout VerticalOptions="Center" HorizontalOptions="Center" Padding="20">
		<Label Text="MainPage" TextColor="Black" HorizontalTextAlignment="Center" />
		<Button Text="Execute" Command="{Binding ExecuteCommand}" />
		<Grid>
			<Grid.ColumnDefinitions>
				<ColumnDefinition Width="Auto" />
				<ColumnDefinition Width="*" />
			</Grid.ColumnDefinitions>
			
			<Label Grid.Row="0" Grid.Column="0" Text="title" HorizontalTextAlignment="End" />
			<Label Grid.Row="0" Grid.Column="1" Text="{Binding Weather.title}" />

			<Label Grid.Row="1" Grid.Column="0" Text="location.area" HorizontalOptions="End" />
			<Label Grid.Row="1" Grid.Column="1" Text="{Binding Weather.location.area}" />

			<Label Grid.Row="2" Grid.Column="0" Text="location.prefecture" HorizontalOptions="End" />
			<Label Grid.Row="2" Grid.Column="1" Text="{Binding Weather.location.prefecture}" />

			<Label Grid.Row="3" Grid.Column="0" Text="location.city" HorizontalOptions="End" />
			<Label Grid.Row="3" Grid.Column="1" Text="{Binding Weather.location.city}" />
		</Grid>
	</StackLayout>

</ContentPage>
```

```c#:コードビハインド
using Xamarin.Forms;
using MyApp.ViewModels;

namespace MyApp.Views
{
	public partial class MainPage : ContentPage
	{
		public MainPage()
		{
			InitializeComponent();
			BindingContext = new MainPageViewModel();
		}
	}
}
```


*"コードビハインドにコードを書くな"* とおっしゃる方もおるようですが、Viewはデザイナーさんがメンテできることが前提。
デザイナーさんに向かって

```
<ContentPage.BindingContext>
  <viewModels:MainPageViewModel />
</ContentPage.BindingContext>
```

をXAMLに書けなんて言えない。デザイナーさんはこれがなんだか分かるわけない。


### ViewModel

```c#:MainPageViewModel.cs
using System;
using System.ComponentModel;
using System.Windows.Input;
using System.Threading.Tasks;

using Xamarin.Forms;

using MyApp.Models;
using MyApp.APIs;
using MyApp.Services;

using System.Diagnostics;

namespace MyApp.ViewModels
{
	public class MainPageViewModel : INotifyPropertyChanged
	{
		public ICommand ExecuteCommand { protected set; get; }

        protected WeatherApi _weatherApi;

		protected Weather _weather;
		public Weather Weather {
			get { return _weather; }
			set {
				_weather= value;
				OnPropertyChanged(nameof(Weather));
			}
		}

		public MainPageViewModel()
		{
			InitializeCommands();

            _weatherApi = new WeatherApi(new HttpService());

            // 監視開始
            _weatherApi.Subscribe(
                value => {
                    Debug.WriteLine("OnNext");
                    this.Weather = value;
                },
                e => {
					Debug.WriteLine("OnError");
					Debug.WriteLine(e.StackTrace);
                },
                () => {
					Debug.WriteLine("OnCompleted");
				}
            );
		}

		protected void InitializeCommands()
		{
            // NOTE:
            // この Command オブジェクトを async にしておかないと、
            // 処理が終了するまでUIが固まる。
            this.ExecuteCommand = new Command(async () => {
                await Task.Run(() => {
                    _weatherApi.GetData();
                });
			});
		}

		//
		// INotifyPropertyChanged の実装
		//
		public event PropertyChangedEventHandler PropertyChanged;

		protected virtual void OnPropertyChanged(string propertyName)
		{
			if (PropertyChanged != null) {
	            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
			}
		}
	}
}
```

### ロジック層

```c#:WeatherApi.cs
using System;
using System.Reactive.Subjects;

using Newtonsoft.Json;

using MyApp.Services;
using MyApp.Models;

using System.Diagnostics;

namespace MyApp.APIs
{
    public class WeatherApi : IObservable<Weather>
    {
        const string API_ENTRYPOINT_URI = "http://weather.livedoor.com/forecast/webservice/json/v1";

        protected Subject<Weather> _source = new Subject<Weather>();

        protected HttpService _httpService;
        protected Weather _weather;

        public WeatherApi(HttpService httpService)
        {
            _httpService = httpService;
            
            // 監視開始
            _httpService.Subscribe(
                value => {
                	Debug.WriteLine("OnNext");
                	string json = value;
					if (json == null || string.IsNullOrEmpty(json)) {
						throw new EmptyContentException();
					}
					_weather = JsonConvert.DeserializeObject<Weather>(json);
					// 上位層に変更を通知
 					_source.OnNext(_weather);
                },
                e => {
                  Debug.WriteLine("OnError");
                  // 上位層にエラーを通知
                  _source.OnError(e);
                },
                () => {
					Debug.WriteLine("OnCompleted");
					// 上位層に完了を通知
					_source.OnCompleted();
                }
            );
        }

        public void GetData()
        {
            string city = "016010";  // 016010: 札幌市
            _httpService.GetContent($"{API_ENTRYPOINT_URI}?city={city}");
        }

        public IDisposable Subscribe(IObserver<Weather> observer)
        {
            return _source.Subscribe(observer);
        }

        class ApiException : Exception { }
		class EmptyContentException : ApiException { }
	}
}
```

```c#:HttpService.cs
using System;
using System.Threading.Tasks;
using System.Net;
using System.Net.Http;
using System.Reactive.Subjects;

namespace MyApp.Services
{
	public class HttpService : IObservable<string>
	{
       protected Subject<string> _source = new Subject<string>();

		public async void GetContent(string uri)
		{
			HttpClient client = new HttpClient();
			HttpResponseMessage response = await client.GetAsync(uri);

			if (response == null) {
                // 上位層にエラーを通知
                // 例外を投げると、上位層の OnError で捕捉される（はず）。
				throw new Exception("Response is null.");
			}
			
			int statusCode = (int)response.StatusCode;

			if (statusCode < 400) {
				string content = response.Content.ReadAsStringAsync().Result;
				// 上位層に変更を通知
				_source.OnNext(content);
            } else {
            	// 上位層にエラーを通知
            	throw new Exception(statusCode.ToString());
			}
		}
		
		public IDisposable Subscribe(IObserver<string> observer)
       {
       		return _source.Subscribe(observer);
       	}
	}
}
```

## 結果

![result.png](https://qiita-image-store.s3.amazonaws.com/0/169798/b79cddae-caa9-a592-0a65-2929c8043259.png "result.png")

ExecuteをタップしたらAPIからの情報が表示された。
メッセージは伝搬されている模様。

とりあえずは以上です。