Xamarin.Forms でテキスト入力可能なダイアログを表示する


Xamarin.Forms にはシンプルなポップアップダイアログを表示する手段が標準で提供されています。

[Pop-ups | Xamarin](http://developer.xamarin.com/guides/cross-platform/xamarin-forms/working-with/pop-ups/)

とてもシンプルであるため、この方法で取得できるのは"Yes/No"、または"複数ある内のどのボタンが押されたか"だけです。

そこで、この記事では`DependencyService`を使って"パスワード入力などに利用可能なダイアログを表示する方法"をご紹介します。


## プロジェクトの作成

新規にソリューションを作成します。種類は`Blank Apps (Xamarin.Forms Portable)`、名前は`EntryAlertSample`にします。

## PCLプロジェクト

EntryAlertSampleプロジェクトに空のインターフェース`IEntryAlertService.cs`を新規作成し、中身を以下のように書きます。`Show`メソッドの`isPassword`パラメータの設定で入力テキストを隠せるようにします。

`EntryAlertResult`は押されたボタンと入力テキストを返すためのデータクラスです。

```c#:IEntryAlertService.cs
using System.Threading.Tasks;

namespace EntryAlertSample
{
	public interface IEntryAlertService
	{
		Task<EntryAlertResult> Show(string title, string message,
			string accepte, string cancel, bool isPassword = false);
	}

	public class EntryAlertResult
	{
		public string PressedButtonTitle { get; set; }
		public string Text { get; set; }
	}
}
```

`MainPage.cs`を追加し、動作検証用のボタンとラベルを持った`MainPage`クラスを定義します。

```c#:MainPage.cs
using Xamarin.Forms;

namespace EntryAlertSample
{
	public class MainPage : ContentPage
	{
		public MainPage ()
		{
			var label = new Label ();
			var plainTextButton = new Button { Text = "Show plain text dialog" };
			var passwordButton = new Button { Text = "Show password dialog" };

			plainTextButton.Clicked += async (sender, e) => {
				var result = await DependencyService.Get<IEntryAlertService>().Show(
					"Prain text", "Please enter text.", "OK", "Cancel", false);
				label.Text = string.Format("{0}:{1}", result.PressedButtonTitle, result.Text);
			};

			passwordButton.Clicked += async (sender, e) => {
				var result = await DependencyService.Get<IEntryAlertService>().Show(
					"Password", "Please enter password.", "OK", "Cancel", true);
				label.Text = string.Format("{0}:{1}", result.PressedButtonTitle, result.Text);
			};

			Content =  new StackLayout {
				Orientation = StackOrientation.Vertical,
				VerticalOptions = LayoutOptions.CenterAndExpand,
				HorizontalOptions = LayoutOptions.CenterAndExpand,
				Children = { label, plainTextButton, passwordButton }
			};
		}
	}
}
```

最後に`App.cs`ファイルを次のように修正します。

```c#:App.cs
...省略
public class App
{
	public static Page GetMainPage()
	{   
		return new MainPage();
	}
}
```


## iOSプロジェクト

EntryAlertSample.iOSプロジェクトに`EntryAlertService.cs`を新規作成し、EntryAlertSampleプロジェクトで定義した`IEntryAlertService`インターフェースを実装します。`[assembly: Dependency (typeof(EntryAlertService))]`の記述により、PCLプロジェクトから`DependencyService`経由で`EntryAlertService`のインスタンスを取得することが可能になります。

```c#:EntryAlertService.cs
using EntryAlertSample;
using EntryAlertSample.iOS;
using Xamarin.Forms;
using MonoTouch.UIKit;
using System.Threading.Tasks;

[assembly: Dependency (typeof(EntryAlertService))]

namespace EntryAlertSample.iOS
{
	public class EntryAlertService : IEntryAlertService
	{
		public Task<EntryAlertResult> Show (string title, string message,
		                                    string accepte, string cancel, bool isPassword = false)
		{
			var tcs = new TaskCompletionSource<EntryAlertResult> ();

			UIApplication.SharedApplication.InvokeOnMainThread (() => {
				var alert = new UIAlertView (title, message, null, cancel, new[]{ accepte });
				
				if (isPassword) {
					alert.AlertViewStyle = UIAlertViewStyle.SecureTextInput;
				} else {
					alert.AlertViewStyle = UIAlertViewStyle.PlainTextInput;
				}

				alert.Clicked += (sender, e) => tcs.SetResult (new EntryAlertResult {
					PressedButtonTitle = alert.ButtonTitle (e.ButtonIndex),
					Text = alert.GetTextField (0).Text
				});
				alert.Show ();
			});

			return tcs.Task;
		}
	}
}
```

ポイントは`TaskCompletionSource<T>`を使用していることです。ボタンが押された際に`TaskCompletionSource<T>.SetResult(T)`を実行することにより、`IEntryAlertService.Show`の呼び出し側でダイアログの入力完了を待機可能になります。ダイアログの表示には`UIAlertView`を使います。


## Androidプロジェクト

EntryAlertSample.AndroidプロジェクトもiOSプロジェクトと同様に`EntryAlertService.cs`を新規作成し、EntryAlertSampleプロジェクトで定義した`IEntryAlertService`インターフェースを実装します。


```c#:EntryAlertService.cs
using System.Threading.Tasks;
using Android.App;
using Android.Widget;
using Xamarin.Forms;

using EntryAlertSample;
using EntryAlertSample.Android;

[assembly: Dependency (typeof(EntryAlertService))]

namespace EntryAlertSample.Android
{
	public class EntryAlertService : IEntryAlertService
	{
		public Task<EntryAlertResult> Show (string title, string message,
		                                    string accepte, string cancel, bool isPassword = false)
		{
			var tcs = new TaskCompletionSource<EntryAlertResult> ();

			var editText = new EditText (Forms.Context);
			if (isPassword) {
				editText.InputType = global::Android.Text.InputTypes.TextVariationPassword
				| global::Android.Text.InputTypes.ClassText;
			}

			new AlertDialog.Builder (Forms.Context)
				.SetTitle (title)
				.SetMessage (message)
				.SetView (editText)
				.SetNegativeButton (cancel, (o, e) => tcs.SetResult (new EntryAlertResult {
				PressedButtonTitle = cancel,
				Text = editText.Text
			}))
				.SetPositiveButton (accepte, (o, e) => tcs.SetResult (new EntryAlertResult {
				PressedButtonTitle = accepte,
				Text = editText.Text
			}))
				.Show ();

			return tcs.Task;
		}
	}
}
```

`EditText`や`AlertDialog.Builder`のコンストラクタで必要なコンテキストは`Forms.Context`から取得します。`Android`から始まる名前空間が`EntryAlertSample.Android`と認識されてしまうため`global::`から始める必要があります。


## 実行結果

実行すると次の画面のようになります。

### iOS

![スクリーンショット 2014-07-02 22.30.52.jpg](https://qiita-image-store.s3.amazonaws.com/0/40121/06aa935c-e75d-bad9-e74c-ed4ab995229e.jpeg "スクリーンショット 2014-07-02 22.30.52.jpg")

### Android

![スクリーンショット 2014-07-02 22.26.15.jpg](https://qiita-image-store.s3.amazonaws.com/0/40121/9876eb36-c600-4e88-4cfa-cd532212711b.jpeg "スクリーンショット 2014-07-02 22.26.15.jpg")