Quantcast
Channel: かずきのBlog@hatena
Viewing all 1388 articles
Browse latest View live

Azure MobileApps + Xamarin.Forms開発の始め方(.NETバックエンド + Prism.Forms)「プッシュ通知(Androidだけ)」

$
0
0

過去記事

プッシュ通知

そろそろ、プッシュ通知してみたいと思います。 先日、このためにGoogleの開発者登録しました。流石にMac持ってないのでAppleの開発者登録はしてないのでiOS側はプッシュ通知試せてないです。すいません。

ということでXamarin.FormsのAndroidのプッシュ通知を試してみたいと思います。

通知ハブの作成

通知を使うには通知ハブを作成しないといけません。Azure上に作成したMobile AppsのPushを選んで作ります。

f:id:okazuki:20160924071028p:plain

名前とネームスペースを入力して作成します。

f:id:okazuki:20160924071221p:plain

Web.configのAppSettingsにあるMS_NotificationHubNameに作成したNotificationHubの名前を入れておきます。

<add key="MS_NotificationHubName"value="okazuki0920-hub" />

ライブラリのインストール

サーバーサイドのプロジェクトにNuGetから以下のライブラリをインストールします。

  • Microsoft.Azure.NotificationHubs

プッシュ通知処理の追加

データが追加されたらプッシュ通知をしてみましょう。InsertAsyncお呼び出しの後にコードを追加します。

// POST tables/TodoItempublic async Task<IHttpActionResult> PostTodoItem(TodoItem item)
{
    item.UserId = (this.User as ClaimsPrincipal)?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    TodoItem current = await InsertAsync(item);

    await PushAsync(item);

    return CreatedAtRoute("Tables", new { id = current.Id }, current);
}

private async Task PushAsync(TodoItem item)
{
    var settings = this.Configuration.GetMobileAppSettingsProvider().GetMobileAppSettings();
    var hubName = settings.NotificationHubName;
    var connectionString = settings.Connections[MobileAppSettingsKeys.NotificationHubConnectionString].ConnectionString;
    var hub = NotificationHubClient.CreateClientFromConnectionString(connectionString, hubName);

    var templateParams = new Dictionary<string, string>
    {
        { "messageParam",$"{item.Text}が追加されました" }
    };
    
    try
    {
        await hub.SendTemplateNotificationAsync(templateParams);
    }
    catch (Exception ex)
    {
        this.Configuration.Services.GetTraceWriter().Error(ex);
    }
}

そして、Azureへ発行しておきます。

Androidプロジェクトの編集

Googleのデベロッパーコンソールというやつに移動します。

console.developers.google.com

そして、プロジェクトを作ります。

f:id:okazuki:20160924073428p:plain

プロジェクト名と、質問事項にこたえて作成します。

そして、ここに行きます。

console.firebase.google.com

プロジェクトを作成するボタンを押して、先ほど作成したプロジェクトにFireBaseを追加してください。

設定を選びます。クラウドメッセージングを選びます。 f:id:okazuki:20160924080851p:plain

送信者IDとサーバーキーが表示されるので控えておきます。

Azureのポータルで作成したMobile AppsからPushを選択してGMSを選ぶとAPIキーの入力を求められるので入力を行います。

f:id:okazuki:20160924074839p:plain

そして、DroidプロジェクトのComponentsからGet More ComponentでGoogle Cloud Messaging Clientをインストールします。 (Xamarinのアカウントでのログインを求められたけど、これって新規で使ってる人とかどうするんだろう…)

コードを書こう

設定が終わったのでコードを書いていきます。 まず、送信者IDの定数を定義しておきましょう。

namespace PrismMobileApp.Droid
{
    publicstaticclass Consts
    {
        publicstaticstring[] SenderIds { get; } = new[] { "送信者ID" };
    }
}

そして、MainActivityのLoadApplicationの呼び出しの後に以下のコードを追加します。

try
{
    GcmClient.CheckDevice(this);
    GcmClient.CheckManifest(this);
    GcmClient.Register(this, Consts.SenderIds);
}
catch (Java.Net.MalformedURLException)
{
    Log.Debug(nameof(MainActivity), "Error");
}
catch (Exception ex)
{
    Log.Debug(nameof(MainActivity), ex.ToString());
}

GcmServiceというクラスを作成して、usingとnamespaceの間に以下のコードを追加します。

[assembly: Permission(Name = "@PACKAGE_NAME@.permission.C2D_MESSAGE")]
[assembly: UsesPermission(Name = "@PACKAGE_NAME@.permission.C2D_MESSAGE")]
[assembly: UsesPermission(Name = "com.google.android.c2dm.permission.RECEIVE")]
[assembly: UsesPermission(Name = "android.permission.INTERNET")]
[assembly: UsesPermission(Name = "android.permission.WAKE_LOCK")]
//GET_ACCOUNTS is only needed for android versions 4.0.3 and below
[assembly: UsesPermission(Name = "android.permission.GET_ACCOUNTS")]

次に、PushHandlerBoroadcastReceiverというクラスを定義します。

[BroadcastReceiver(Permission = Gcm.Client.Constants.PERMISSION_GCM_INTENTS)]
[IntentFilter(newstring[] { Gcm.Client.Constants.INTENT_FROM_GCM_MESSAGE }, Categories = newstring[] { "@PACKAGE_NAME@" })]
[IntentFilter(newstring[] { Gcm.Client.Constants.INTENT_FROM_GCM_REGISTRATION_CALLBACK }, Categories = newstring[] { "@PACKAGE_NAME@" })]
[IntentFilter(newstring[] { Gcm.Client.Constants.INTENT_FROM_GCM_LIBRARY_RETRY }, Categories = newstring[] { "jp.okazuki.pushsample" })]
publicclass PushHandlerBroadcastReceiver : GcmBroadcastReceiverBase<GcmService>
{
}

そして、GcmServiceBaseクラスを継承したGcmServiceクラスを作成します。

[Service]
publicclass GcmService : GcmServiceBase
{
    public GcmService() : base(Consts.SenderIds)
    {
    }

    protectedoverridevoid OnMessage(Context context, Intent intent)
    {
        thrownew NotImplementedException();
    }

    protectedoverridevoid OnError(Context context, string errorId)
    {
        thrownew NotImplementedException();
    }

    protectedoverridevoid OnRegistered(Context context, string registrationId)
    {
        thrownew NotImplementedException();
    }

    protectedoverridevoid OnUnRegistered(Context context, string registrationId)
    {
        thrownew NotImplementedException();
    }
}

このOnRegisteredで、渡されたregistrationIdを使って登録処理をしないといけないのですが、認証が有効になってると、ここで登録処理を行ってもアクセス権がないと怒られます。認証が終わった後にプッシュ通知に登録を行わないといけません。今回は、認証処理を行うIAuthenticatorに認証後に何かやるという処理を登録できる仕組みを追加しました。

using System;
using System.Threading.Tasks;

namespace PrismMobileApp.Models
{
    publicinterface IAuthenticator
    {
        Task<bool> AuthenticateAsync();

        void RegisterPostProcess(Func<Task> action);
    }
}

こんな感じにインターフェースを変えて

using Android.Util;
using Microsoft.WindowsAzure.MobileServices;
using PrismMobileApp.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace PrismMobileApp.Droid.Models
{
    publicclass Authenticator : IAuthenticator
    {
        private MobileServiceClient Client { get; }

        private List<Func<Task>> PostProcesses { get; } = new List<Func<Task>>();

        public Authenticator(MobileServiceClient client)
        {
            this.Client = client;
        }

        public async Task<bool> AuthenticateAsync()
        {
            try
            {
                var user = await this.Client.LoginAsync(Forms.Context, MobileServiceAuthenticationProvider.Twitter);
                if (user != null)
                {
                    await Task.WhenAll(
                        this.PostProcesses.Select(x => x()));
                }

                return user != null;
            }
            catch (Exception ex)
            {
                Log.Debug(nameof(Authenticator), ex.ToString());
                returnfalse;
            }
        }

        publicvoid RegisterPostProcess(Func<Task> action)
        {
            this.PostProcesses.Add(action);
        }
    }
}

こんな感じに認証に成功したらPostProcessesを全部実行するという感じにしました。

下準備ができたので各オーバーライドメソッドを実装します。

[Service]
publicclass GcmService : GcmServiceBase
{
    public GcmService() : base(Consts.SenderIds)
    {
    }

    protectedoverridevoid OnMessage(Context context, Intent intent)
    {
        var message = intent.Extras.GetString("message");
        if (!string.IsNullOrEmpty(message))
        {
            this.CreateNotification("New todo item!", $"Todo item: {message}");
            return;
        }
    }

    privatevoid CreateNotification(string title, string description)
    {
        var notificationManager = this.GetSystemService(Context.NotificationService) as NotificationManager;
        var uiIntent = new Intent(this, typeof(MainActivity));

        var builder = new NotificationCompat.Builder(this);
        var notification = builder.SetContentIntent(PendingIntent.GetActivity(this, 0, uiIntent, 0))
            .SetSmallIcon(Android.Resource.Drawable.SymActionEmail)
            .SetTicker(title)
            .SetContentTitle(title)
            .SetContentText(description)
            .SetSound(RingtoneManager.GetDefaultUri(RingtoneType.Notification))
            .SetAutoCancel(true)
            .Build();

        notificationManager.Notify(1, notification);
    }

    protectedoverridevoid OnError(Context context, string errorId)
    {
    }

    protectedoverridevoid OnRegistered(Context context, string registrationId)
    {
        ((App)App.Current).Container.Resolve<IAuthenticator>().RegisterPostProcess(() =>
        {
            ((Activity)Forms.Context).RunOnUiThread(async () =>
            {
                try
                {
                    var client = ((App)App.Current).Container.Resolve<MobileServiceClient>();
                    var push = client.GetPush();

                    conststring templateBodyGCM = @"{ ""data"": { ""message"":""$(messageParam)"" }}";
                    var template = new JObject();
                    template["genericMessage"] = new JObject
                    {
                        { "body", templateBodyGCM },
                    };

                    await push.RegisterAsync(registrationId, template);
                }
                catch (Exception ex)
                {
                    Log.Error(nameof(GcmService), ex.ToString());
                }
            });
            return Task.CompletedTask;
        });
    }

    protectedoverridevoid OnUnRegistered(Context context, string registrationId)
    {
    }
}

OnRegisteredメソッドで先ほどの認証処理の後処理にプッシュの登録処理を追加しています。

動作確認

ローカルデバッグ用に構成してた、App.xaml.csの中身をクラウドように書き換えます。

protectedoverridevoid RegisterTypes()
{
    this.Container.RegisterTypeForNavigation<MainPage>();
    // MobileServiceClientをシングルトンで登録this.Container.RegisterType<MobileServiceClient>(
        new ContainerControlledLifetimeManager(),
        new InjectionConstructor(
            new Uri("https://okazuki0920.azurewebsites.net"),
            new HttpMessageHandler[] { }));
    //this.Container.RegisterType<MobileServiceClient>(//    new ContainerControlledLifetimeManager(),//    new InjectionConstructor(//        new Uri("http://169.254.80.80:1782/"),//        new HttpMessageHandler[] { }),//    new InjectionProperty(nameof(MobileServiceClient.AlternateLoginHost), new Uri("https://okazuki0920.azurewebsites.net")));this.Container.RegisterType<TodoApp>(new ContainerControlledLifetimeManager());
    this.Container.RegisterType<IDataSynchronizer, DataSynchronizer>(new ContainerControlledLifetimeManager());
    this.Container.RegisterType<ITodoItemRepository, TodoItemRepository>(new ContainerControlledLifetimeManager());
}

VSのAndroidエミュレータだとプッシュ通知できない?っぽいので実機デバッグします。

何か適当にデータを追加してPullToRefreshかけるとプッシュ通知が飛んできます。

f:id:okazuki:20160924102451j:plain

まとめ

認証とセットでプッシュ通知をやるときは、プッシュ通知の登録タイミングを気を付けよう。


モバイルアプリなどの練習用WebAPIを作成しました

$
0
0

モバイルアプリやUWPの練習でたたける簡単なAPIがほしいかも?ということで作ってAzureに置いてみました。一応認証のかかったシンプルなAPIです。

OAuth2で、ユーザー名admin、パスワードp@ssw0rdでトークンが取得できます。 Web APIは以下のような感じです。

ルート:https://okazukisampleapi.azurewebsites.net/

  • GET
    • api/People?page=数字
      • 1ページ50件でデータを返す。pageを省略した場合は最初のページのデータを返す。
    • api/People/数字
      • 数字で指定したIDのデータを1件取得する
  • PUT
    • api/People/数字
      • BODYに渡した内容でデータを更新する
  • POST
    • api/People
      • BODYに渡した内容でデータを作成する
  • DELETE
    • api/People/数字
      • 数字で指定したIDのデータを削除する

JSONは基本的に以下のデータを返します。

1件のデータを返す系

{"id":2,"name":"okazuki","birthday":"2016-09-26T12:34:05.9874877+00:00"}

複数のデータを返す系

[{"id":1,"name":"tanaka","birthday":"2016-09-26T12:32:46.1740543+00:00"},{"id":2,"name":"okazuki","birthday":"2016-09-26T12:34:05.9874877+00:00"}]

以下のようなC#のコードで認証通してAPIを呼ぶことができます。

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication2
{
    class Program
    {
        staticvoid Main(string[] args)
        {
            var client = new HttpClient();
            client.BaseAddress = new Uri("https://okazukisampleapi.azurewebsites.net/");

            var authContent = new StringContent("grant_type=password&username=admin&password=p@ssw0rd");
            authContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
            var authRes = client.PostAsync("/Token", authContent).Result;

            authRes.EnsureSuccessStatusCode();

            var token = JsonConvert.DeserializeObject<AuthResult>(authRes.Content.ReadAsStringAsync().Result).AccessToken;
            Console.WriteLine(token);
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

            var p = new Person
            {
                Name = "okazuki",
                Birthday = DateTimeOffset.UtcNow,
            };

            var postContent = new StringContent(JsonConvert.SerializeObject(p));
            postContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
            var postRes = client.PostAsync("api/People", postContent).Result;
            postRes.EnsureSuccessStatusCode();

            Console.WriteLine(postRes.Content.ReadAsStringAsync().Result);

            var getRes = client.GetAsync("api/People").Result;
            getRes.EnsureSuccessStatusCode();
            Console.WriteLine(getRes.Content.ReadAsStringAsync().Result);
        }
    }

    class AuthResult
    {
        [JsonProperty(PropertyName = "access_token")]
        publicstring AccessToken { get; set; }
    }

    class Person
    {
        [JsonProperty(PropertyName = "id")]
        publicint Id { get; set; }
        [JsonProperty(PropertyName = "name")]
        publicstring Name { get; set; }
        [JsonProperty(PropertyName = "birthday")]
        public DateTimeOffset Birthday { get; set; }
    }
}

とりあえず無償プランでDBも月500円くらいなんで放置しておこうと思います。

XamarinとかでAPIたたくアプリの練習などにどうぞ。

サーバー側ソース

github.com

Bot Frameworkでenumの選択肢を日本語にしたい

$
0
0

デフォルト英語フレンドりーなのでEnumの選択肢はEnumの値がそのまま出てきます。

Formを定義するときに以下のようにFieldReflectorを使うと、日本語で表示させることができます。

returnnew FormBuilder<Order>()
    .Message("注文をお伺いします")
    .Field(new FieldReflector<Order>(nameof(Order.Drink))
        .SetDefine((state, field) =>
        {
            field.AddDescription(Size.S, "Sサイズ");
            field.AddDescription(Size.M, "Mサイズ");
            field.AddDescription(Size.L, "Lサイズ");
            return Task.FromResult(true);
        }))
    .Build();

OrderSizeの定義は以下のような感じ。

using Microsoft.Bot.Builder.FormFlow;
using System;

namespace FormDialogSample.Models
{
    publicenum Size
    {
        S = 1, M, L
    }

    [Serializable]
    publicclass Order
    {
        [Prompt("飲み物のサイズは何にしますか?{||}")]
        public Size Drink { get; set; }
    }
}

流行のボットをMicrosoft Bot Framework(.NET SDK)で作ってみよう

$
0
0

MicrosoftのBot作成用FrameworkのBot Framework入門してみましょう。公式サイトは以下になります。

Bot Framework

導入

導入は至って簡単です。公式の導入手順は以下です。

Getting started with the Connector | Bot Builder SDK C# Reference Library | Bot Framework

以下に手順を示します。

プロジェクトテンプレートのインストール

Bot Framework用のC#のプロジェクトテンプレートが以下からダウンロードできます。

http://aka.ms/bf-bc-vstemplate

zipファイルをダウンロードしたら、ドキュメントフォルダの下のVisual Studio 2015\Templates\ProjectTemplates\Visual C#にコピーします。

エミュレータのダウンロード

デバッグに便利なエミュレータも入れておきます。以下からインストールできます。

Bot Framework Channel Emulator

Hello world

ではHello worldをしてみます。BotのHello worldということで、名前を入力したら「こんにちは〇〇さん」と表示するBotでも作ってみようと思います。

プロジェクトの新規作成からBot Applicationを選択します。プロジェクト名はHelloBotAppにしました。

f:id:okazuki:20160927200650p:plain

とりあえず、エミュレータの動作を見るため、このままF5を押して実行します。以下のような画面が出れば起動は完了です。

f:id:okazuki:20160927200949p:plain

URLを控えておいてエミュレータを起動しましょう。赤線の部分のURLをhttp://localhost:起動したポート番号/api/messagesと入力します。(Bot Applicationのプロジェクトテンプレートはデフォルトで3979番ポートを使っていて、エミュレータはデフォルトで、そのポート番号を指定されてると思うので、何もしてなければそのままでいいと思います)

f:id:okazuki:20160927201242p:plain

画面下部の入力欄に「こんにちは」と打ち込んでEnterを押します。するとボットにメッセージが送られて「You sent こんにちは which was 5 characters」というレスポンスが返ってきます。メッセージを選択すると、そのときのJSONも見ることができます。

f:id:okazuki:20160927201443p:plain

では、プログラムを見ていきます。ControllersフォルダにMessagesControllerがいます。これがボットの本体です。コードは以下のようになっています。

using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Description;
using Microsoft.Bot.Connector;
using Newtonsoft.Json;

namespace HelloBotApp
{
    [BotAuthentication]
    publicclass MessagesController : ApiController
    {
        /// <summary>/// POST: api/Messages/// Receive a message from a user and reply to it/// </summary>public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
        {
            if (activity.Type == ActivityTypes.Message)
            {
                ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl));
                // calculate something for us to returnint length = (activity.Text ?? string.Empty).Length;

                // return our reply to the user
                Activity reply = activity.CreateReply($"You sent {activity.Text} which was {length} characters");
                await connector.Conversations.ReplyToActivityAsync(reply);
            }
            else
            {
                HandleSystemMessage(activity);
            }
            var response = Request.CreateResponse(HttpStatusCode.OK);
            return response;
        }

        private Activity HandleSystemMessage(Activity message)
        {
            if (message.Type == ActivityTypes.DeleteUserData)
            {
                // Implement user deletion here// If we handle user deletion, return a real message
            }
            elseif (message.Type == ActivityTypes.ConversationUpdate)
            {
                // Handle conversation state changes, like members being added and removed// Use Activity.MembersAdded and Activity.MembersRemoved and Activity.Action for info// Not available in all channels
            }
            elseif (message.Type == ActivityTypes.ContactRelationUpdate)
            {
                // Handle add/remove from contact lists// Activity.From + Activity.Action represent what happened
            }
            elseif (message.Type == ActivityTypes.Typing)
            {
                // Handle knowing tha the user is typing
            }
            elseif (message.Type == ActivityTypes.Ping)
            {
            }

            returnnull;
        }
    }
}

見てわかる通り、ボット用の認証属性のついた、ただのWebAPIです。その中でBot FrameworkのAPIを呼び出して色々やります。

PostメソッドのわたってきているActivityがいろいろな情報が詰まっています。そのTypeMessageの時にチャットの応答を返せばOKです。デフォルトでは入力文字列(ActivityTextプロパティで取得可能)の長さを算出してレスポンスを返しています。

ConnectorClientがクライアントとの接続を表していて、そこのConversationsプロパティのReplyToActivityAsyncActivityCreateReplyメソッドで作れるActivityを渡すことで返事を返せます。 ということで、最初に書いた通り名前を入力してもらうとして「こんにちは〇〇さん」と返すようにしましょう。

以下のようなPostメソッドになります。

public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
    if (activity.Type == ActivityTypes.Message)
    {
        var connector = new ConnectorClient(new Uri(activity.ServiceUrl));
        var reply = activity.CreateReply($"こんにちは{activity.Text}さん");
        await connector.Conversations.ReplyToActivityAsync(reply);
    }
    else
    {
        HandleSystemMessage(activity);
    }
    var response = Request.CreateResponse(HttpStatusCode.OK);
    return response;
}

実行してエミュレータに、名前を入力して送ってみましょう。以下のようになります。

f:id:okazuki:20160927202227p:plain

ダイアログ

次に、よく使うと思われるダイアログについて説明します。ダイアログは簡単に言ってしまえばGUIで言うところのウィザード形式のダイアログみたいなものです。IDialog<T>インターフェースを実装したクラスがダイアログになります。簡単に名前を入力するダイアログを作ってみようと思います。

DialogAppという名前でプロジェクトを作ります。Dialogsフォルダを作成して、そこにInputNameDialogクラスを作成します。ダイアログはIDialogインターフェースを実装します。IDialogインターフェースはStartAsyncメソッドがあるので、それを実装します。StartAsyncメソッドの引数のIDialogContextがダイアログの処理が色々詰まっています。あと、ダイアログはシリアル化可能でなければならないという制約があるのでSerializable属性をつけておきます。

using Microsoft.Bot.Builder.Dialogs;
using System;
using System.Threading.Tasks;

namespace DialogApp.Dialogs
{
    [Serializable]
    publicclass InputNameDialog : IDialog
    {
        public Task StartAsync(IDialogContext context)
        {
            
        }
    }
}

では、StartAsyncを実装していきます。ここでも名前を入力したら「こんにちは〇〇さん」と出すだけのダイアログを作りたいと思います。ダイアログの作り方は基本的にcontextWaitメソッドにメッセージを処理するメソッドを渡す形で定義します。こんな感じになります。

using Microsoft.Bot.Builder.Dialogs;
using System.Threading.Tasks;
using System;
using Microsoft.Bot.Connector;

namespace DialogApp.Dialogs
{
    publicclass InputNameDialog : IDialog
    {
        public Task StartAsync(IDialogContext context)
        {
            context.Wait(this.ReceiveMessageAsync);
            return Task.CompletedTask;
        }

        private Task ReceiveMessageAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
        {
            thrownew NotImplementedException();
        }
    }
}

ReceiveMessageAsyncメソッドに処理を書いていきます。基本的には第二引数をawaitしてメッセージの入ったIMessageActivityを取得して、そのTextプロパティを見て処理を行います。レスポンスの返し方は、IDialogContextPostAsyncというメソッドがあるので、そこに文字列を渡せばOKです。最初より簡単ですね。ということでコードは以下のようになります。

using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using System;
using System.Threading.Tasks;

namespace DialogApp.Dialogs
{
    [Serializable]
    publicclass InputNameDialog : IDialog
    {
        public Task StartAsync(IDialogContext context)
        {
            context.Wait(this.ReceiveMessageAsync);
            return Task.CompletedTask;
        }

        private async Task ReceiveMessageAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
        {
            var activity = await result;
            await context.PostAsync($"こんにちは{activity.Text}さん");
        }
    }
}

ではMessagesControllerでダイアログを使うように変更します。ConversationクラスのSendAsyncを使うことで出来ます。SendAsyncは第一引数にActivityを受け取り、第二引数にダイアログを生成するデリゲートを受け取ります。ということで、先ほど作成したInputNameDialogを使うようにするコードは以下のようになります。

public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
    if (activity.Type == ActivityTypes.Message)
    {
        await Conversation.SendAsync(activity, () => new InputNameDialog());
    }
    else
    {
        HandleSystemMessage(activity);
    }
    var response = Request.CreateResponse(HttpStatusCode.OK);
    return response;
}

実行結果は先ほどと一緒になります。

複数の値を入力させてみよう

ダイアログはウィザードみたいなものだと言いました。ウィザードといえば複数の入力値を受け付けるのが一般的だと思います。ダイアログを改造して名前と年齢を受け取るようにしたいと思います。やり方は簡単で1回のやり取りが終わったら次の入力処理をcontext.Waitで渡してやればいいだけです。あと、一連のダイアログの処理が終わったらcontext.Doneを呼んでやる感じでいけます。コードは以下のようになります。

using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using System;
using System.Threading.Tasks;

namespace DialogApp.Dialogs
{
    [Serializable]
    publicclass InputNameDialog : IDialog
    {
        publicstring Name { get; set; }
        publicint Age { get; set; }

        public async Task StartAsync(IDialogContext context)
        {
            context.Wait(this.InputNameAsync);
        }

        private async Task InputNameAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
        {
            var activity = await result;
            this.Name = activity.Text;
            await context.PostAsync($"こんにちは{this.Name}さん。続けて年齢を入力してください");
            context.Wait(this.InputAgeAsync);
        }

        private async Task InputAgeAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
        {
            var activity = await result;
            int age;
            if (int.TryParse(activity.Text ?? "", out age))
            {
                this.Age = age;
                await context.PostAsync($"こんにちは{this.Age}歳の{this.Name}さん");
                context.Done((object)null);
            }
            else
            {
                await context.PostAsync($"年齢は数字で入力してください");
                context.Wait(this.InputAgeAsync);
            }
        }
    }
}

実行すると以下のような感じになります。

f:id:okazuki:20160927205629p:plain

ダイアログの連携

ウィザードといえば一連の入力したデータが終わったら後続の処理を実行するようなイメージがあります。Bot Frameworkにも、そんな感じの機能があります。Chainというものを使ってダイアログを連結することで実現できます。ダイアログ間の値の受け渡しは、context.Doneメソッドで渡したものが後続に渡るイメージです。

ということで値受け渡し用のデータの入れ物のPersonクラスを用意します。これもSerializableにしておきます。

using System;

namespace DialogApp.Models
{
    [Serializable]
    publicclass Person
    {
        publicstring Name { get; set; }
        publicint Age { get; set; }
    }
}

そして、InputPersonInfoDialogというクラスを作成して以下のように実装します。結果を返すダイアログはIDialog<T>を実装する点がこれまでのダイアログと異なっています。

using DialogApp.Models;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using System;
using System.Threading.Tasks;

namespace DialogApp.Dialogs
{
    [Serializable]
    publicclass InputPersonInfoDialog : IDialog<Person>
    {
        private Person Person { get; set; } = new Person();

        public Task StartAsync(IDialogContext context)
        {
            context.Wait(this.InputNameAsync);
            return Task.CompletedTask;
        }

        private async Task InputNameAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
        {
            var activity = await result;
            this.Person.Name = activity.Text;
            await context.PostAsync($"こんにちは{this.Person.Name}さん。続けて年齢を入力してください");
            context.Wait(this.InputAgeAsync);
        }

        private async Task InputAgeAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
        {
            var activity = await result;
            int age;
            if (int.TryParse(activity.Text ?? "", out age))
            {
                this.Person.Age = age;
                context.Done(this.Person);
            }
            else
            {
                await context.PostAsync("年齢は数字で入れてください");
                context.Wait(this.InputAgeAsync);
            }
        }
    }
}

後続の処理を行うダイアログを作成します。といっても入力された値をダンプするだけのシンプルなダイアログです。以下のように実装します。

using DialogApp.Models;
using Microsoft.Bot.Builder.Dialogs;
using System;
using System.Threading.Tasks;

namespace DialogApp.Dialogs
{
    [Serializable]
    publicclass CompletedDialog : IDialog<object>
    {
        public Person Person { get; set; }

        public CompletedDialog(Person person)
        {
            this.Person = person;
        }

        public async Task StartAsync(IDialogContext context)
        {
            await context.PostAsync($"こんにちは{this.Person.Age}歳の{this.Person.Name}さん");
            context.Done((object)null);
        }
    }
}

そして、ダイアログの連携部分を書いていきます。 MessagesControllerクラスにダイアログをChainするファクトリメソッドを作成します。以下のようなメソッドです。

privatestatic IDialog<object> CreateDialog()
{
    return Chain.From(() => new InputPersonInfoDialog())
        .ContinueWith<Person, object>(async (ctx, r) =>
        {
            var p = await r;
            returnnew CompletedDialog(p);
        });

}

ContinueWithメソッドでダイアログを連結するようなイメージです。型引数は、前のダイアログが返す型と自分が返す型です。今回は最初のダイアログがPersonを返して、次のダイアログは何も返さないのでobjectとしています。ContinueWithIBotContextIAwaitable<第二型引数>なのでawaitして前のダイアログの結果を受け取ります。そして、それをもとに次のダイアログを作ります。

MessagesControllerPostメソッドを以下のように書き換えてChainメソッドでつないだダイアログを使うようにしましょう。

public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
    if (activity.Type == ActivityTypes.Message)
    {
        await Conversation.SendAsync(activity, CreateDialog);
    }
    else
    {
        HandleSystemMessage(activity);
    }
    var response = Request.CreateResponse(HttpStatusCode.OK);
    return response;
}

実行すると以下のようになります。繰り返し入力もいけるようになりました。

f:id:okazuki:20160927211654p:plain

フォーム入力に特化したダイアログ

さて、クラスに定義されたプロパティの値を埋めていくという処理は結構やることが多いと思います。FormDialogというクラスがこの処理をやってくれます。FormDialogIForm<T>から作ることができます。IForm<T>FormBuilder<T>というビルダークラスがいて、これを使って細かなカスタマイズが可能になっています。とりあえず一番簡単な使い方は以下のようになります。

privatestatic IForm<Person> CreateForm()
{
    returnnew FormBuilder<Person>()
        .Build();
}

privatestatic IDialog<object> CreateDialog()
{
    return Chain.From(() => FormDialog.FromForm(CreateForm))
        .ContinueWith<Person, object>(async (ctx, r) =>
        {
            var p = await r;
            returnnew CompletedDialog(p);
        });

}

InputPersonInfoDialogはいらなくなったので消してください。今までの挙動の違いとしては、最初に何かを話しかけるまで入力シーケンスが始まらないという点です。なので最初に、こんにちはとかhiとかと話しかけてあげましょう。実行すると以下のようになります。

f:id:okazuki:20160927212336p:plain

ちなみにenum型を持たせると、それを選択するようにできます。Personクラスを以下のように書き換えると(= 1重要)

using System;

namespace DialogApp.Models
{
    [Serializable]
    publicclass Person
    {
        publicstring Name { get; set; }
        publicint Age { get; set; }
        public Gender Gender { get; set; }
    }

    publicenum Gender
    {
        Man = 1,
        Woman
    }
}

こんな選択肢が追加されます。

f:id:okazuki:20160927212806p:plain

フォームのカスタマイズ

色々カスタマイズが可能ですが、いくつかのカスタマイズ例を示したいと思います。

メッセージのカスタマイズ

プロパティ名がそのまま出るのって日本語では不自然ですよね。FormBuilderを使うと色々メッセージや値の範囲のチェックなどができるようになっています。細かく解説はしませんが、以下のような感じのコードが可能です。

using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Description;
using Microsoft.Bot.Connector;
using Newtonsoft.Json;
using Microsoft.Bot.Builder.Dialogs;
using DialogApp.Dialogs;
using DialogApp.Models;
using Microsoft.Bot.Builder.FormFlow;
using Microsoft.Bot.Builder.FormFlow.Advanced;

namespace DialogApp
{
    [BotAuthentication]
    publicclass MessagesController : ApiController
    {
        privatestatic IForm<Person> CreateForm()
        {
            returnnew FormBuilder<Person>()
                .Message("あなたの情報を入力してください")
                .Field(nameof(Person.Name), "名前を入力してください", validate: (p, value) =>
                    // nullっていう名前の人はいないという無理な条件
                    Task.FromResult(((string)value) == "null" ?
                        new ValidateResult { Feedback = "nullという名前はダメです" } : 
                        new ValidateResult { IsValid = true, Value = value }))
                .Field(new FieldReflector<Person>(nameof(Person.Age))
                    .SetValidate((p, value) => Task.FromResult((long)value< 0 ? 
                        new ValidateResult { Feedback = "0歳以下はダメです" } :
                        new ValidateResult { IsValid = true, Value = value }))
                    .SetPrompt(new PromptAttribute("年齢を入力してください")))
                .Field(new FieldReflector<Person>(nameof(Person.Gender))
                    .SetPrompt(new PromptAttribute("性別を入力してください{||}"))
                    .SetDefine((p, field) =>
                    {
                        field.AddDescription(Gender.Man, "男性");
                        field.AddDescription(Gender.Woman, "女性");
                        return Task.FromResult(true);
                    }))
                .OnCompletion(async (context, p) =>
                {
                    await context.PostAsync($"こんにちは{p.Gender}の{p.Age}歳の{p.Name}さん");
                })
                .Confirm(p =>
                {
                    return Task.FromResult(new PromptAttribute($@"入力した情報は以下でいいですか?(はい/いいえ)名前: {p.Name}年齢: {p.Age}歳性別: {(p.Gender == Gender.Man ? "男性" : "女性")}"));
                })
                .Build();
        }

        privatestatic IDialog<object> CreateDialog()
        {
            return Chain.From(() => FormDialog.FromForm(CreateForm))
                .ContinueWith<Person, object>(async (ctx, r) =>
                {
                    var p = await r;
                    returnnew CompletedDialog(p);
                });

        }
        /// <summary>/// POST: api/Messages/// Receive a message from a user and reply to it/// </summary>public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
        {
            if (activity.Type == ActivityTypes.Message)
            {
                await Conversation.SendAsync(activity, CreateDialog);
            }
            else
            {
                HandleSystemMessage(activity);
            }
            var response = Request.CreateResponse(HttpStatusCode.OK);
            return response;
        }

        private Activity HandleSystemMessage(Activity message)
        {
            if (message.Type == ActivityTypes.DeleteUserData)
            {
                // Implement user deletion here// If we handle user deletion, return a real message
            }
            elseif (message.Type == ActivityTypes.ConversationUpdate)
            {
                // Handle conversation state changes, like members being added and removed// Use Activity.MembersAdded and Activity.MembersRemoved and Activity.Action for info// Not available in all channels
            }
            elseif (message.Type == ActivityTypes.ContactRelationUpdate)
            {
                // Handle add/remove from contact lists// Activity.From + Activity.Action represent what happened
            }
            elseif (message.Type == ActivityTypes.Typing)
            {
                // Handle knowing tha the user is typing
            }
            elseif (message.Type == ActivityTypes.Ping)
            {
            }

            returnnull;
        }
    }
}

プロパティのPromptAttribute{||}は選択肢を出すものです。PromptAttribute内で改行するには、改行2つで改行できます。

こんな感じになります。

f:id:okazuki:20160927220728p:plain

複雑なダイアログ

Chainには様々なメソッドがあります。

Dialogs | Bot Builder SDK C# Reference Library | Bot Framework

PostToChainで始まり、LINQを使って処理を組み立てることができます。Switchを使って以下のように分岐をすることもできます。

privatestatic IDialog<object> CreateDialog()
{
    return Chain.PostToChain()
        .Select(x => x.Text)
        .Switch(
            new RegexCase<string>(new Regex("^助けて$"), (ctx, value) => "食べたいものを言ってね。"),
            new Case<string, string>(x => x.Contains("寿司"), (ctx, value) => $"かずあきさんの財布で{value}"),
            new DefaultCase<string, string>((ctx, x) => x))
        .PostToUser()
        .Loop();
}

f:id:okazuki:20160927222726p:plain

Switchでダイアログを返すこともできます。そのときはUnwrapとセットで使う感じです。

privatestatic IDialog<object> CreateDialog()
{
    return Chain.PostToChain()
        .Select(x => x.Text)
        .Switch(
            Chain.Case(new Regex("^助けて$"), (ctx, x) => Chain.Return("食べたいものを言ってね")),
            Chain.Case((string x) => x == "寿司", (ctx, x) => Chain
                .Return($"かずあきさんの財布で{x}が食べたい?(はい/いいえ)")
                .PostToUser()
                .WaitToBot()
                .Select(y => y.Text)
                .Select(y => y == "はい" ? "かずあきさんの財布で寿司が食べたい!!" : "そうはいっても本音では食べたい!!")),
            Chain.Default<string, IDialog<string>>((ctx, x) => Chain.Return($"{x}が食べたい")))
        .Unwrap()
        .PostToUser()
        .Loop();
}

f:id:okazuki:20160927224124p:plain

Botのデプロイ

普通にAzureのApp Serviceにデプロイできます。 仮に、https://okazukibottest.azurewebsites.net/にデプロイしたものとして、公開手順を説明します。

Bot FrameworkのサイトでRegister Botを選択します。

f:id:okazuki:20160927224558p:plain

必要事項を入れていきます。Nameは適当に、bothandleはボットのハンドルネームです。

Messaging endpointは、URL/api/messagesです。今回の場合はhttps://okazukibottest.azurewebsites.net/api/messagesになります。

Configurationの項目の下にある「Create Microsoft App ID and password」をクリックして、アプリを登録します。 「アプリシークレットを作成して続行」をクリックしてアプリのシークレットを控えておきます。アプリIDが入力されます。あとで使うので控えておきます。

Publisher Profileを適当に入力したら完成です。同意してRegisterしましょう。

控えてアプリIDとシークレットをWeb.configのappSettingsのMicrosoftAppIdとMicrosoftAppPasswordに設定して再デプロイします。

Botの画面でTestを押して成功すれば準備完了です。

f:id:okazuki:20160927225343p:plain

Add To Skypeを押すと自分のスカイプに登録して試すことができます。そのほかにも、このページからSlackなど様々なチャットサービスに登録ができます。

f:id:okazuki:20160927225638p:plain

それでは良いBot生活を!

この先

LUISとの連携機能もあったりするので、それを使えば、より自然な文章を解析することができるようになります。

www.luis.ai

Language | Documentation | Bot Framework

LUISで自然言語処理を試してみよう

$
0
0

Bot Frameworkは自然言語解析エンジンは提供してくれていません。 ただし、LUISとの連携機能は提供しています。

ということでBot Frameworkで自然言語解析をしたければLUISを使うのが一番自然な流れになります。

LUIS

LUISはLanguage Understanding Intelligent Serviceの略で、以下のページからアカウントが作成できます。

LUIS: Language Understanding Intelligent Service (beta)

アプリケーションを作りましょう、気を付けるのはJapaneseを選ぶところくらいですかね。

f:id:okazuki:20161001142210p:plain

アプリケーションを作成したら、そこにIntentとEntityを作っていきます。

  • Intent: 認識結果の種類みたいなの。
  • Entity: 文章から抜き出すパラメータみたいなもの。

ということで、基本的にIntentにはEntityが含まれてます。

今回は、「こんにちは〇〇」というというとhelloというIntentになって〇〇の部分をnameというEntityとして抜き出すようにしてみたいと思います。

まずnameというEntityを追加します。

f:id:okazuki:20161001142502p:plain

次に、helloというIntentを追加します。例題となるサンプル文章を打ち込んで作ります。

f:id:okazuki:20161001142625p:plain

New utterancesに画面が遷移して文章が文節ごとに区切られた感じに表示されます。

f:id:okazuki:20161001142735p:plain

ここにEntityを指定していきます。今回は世界の部分をnameとしたいので世界を選択してnameを割り当てます。

f:id:okazuki:20161001142857p:plain

「こんにちは〇〇」以外にも「こんばんは〇〇」とか「おはようございます〇〇」もついでに同じ要領で覚えさせてみましょう。

f:id:okazuki:20161001143103p:plain

こういう風に延々と満足いくまで学習させていきます。

publishをするとWebAPIになります。

f:id:okazuki:20161001143508p:plain

URLにidとsubscription-keyが含まれてます。こいつがアクセスするのに必要になります。テキストボックスに何か文字列を入れてEnterを押すと結果のJSONが取得できます。

f:id:okazuki:20161001144306p:plain

{"query": "こんにちは世界",
  "intents": [{"intent": "hello",
      "score": 0.9999995
    },
    {"intent": "None",
      "score": 0.0215021987
    }],
  "entities": [{"entity": "世界",
      "type": "name",
      "startIndex": 5,
      "endIndex": 6,
      "score": 0.9617552
    }]}

intentがhelloでentityのnameが世界になってることが確認できます。

Bot Frameworkとの連携

LUISで言語解析するAPIができたのでBot Frameworkと連携してみたいと思います。 「こんにちは世界」という感じで話しかけると、「世界さん!!ようこそ!!」と返答するようにしたいと思います。 わからないケースの場合は、「すいません。わかりません」と回答したいと思います。

LuisDialog<T>というクラスが提供されているので、これを継承することでLUISと連携ができます。LuisDialogの型引数には、このダイアログが返す値の型になります。 今回は、LuisDialogで延々と処理を回したいと思うのでobjectを指定しています。 ダイアログにはLuisModel属性を指定して、先ほどのLUISのURLに埋め込まれてるidとsbscription-keyの値を設定します。 Intentに応答したかったらLuisIntent属性にIntent名を指定することで、そのメソッドがIntentに応答するようにできます。

コードは以下のようになります。

using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Luis;
using Microsoft.Bot.Builder.Luis.Models;
using System;
using System.Threading.Tasks;

namespace HelloBot.Dialogs
{
    [LuisModel("idの値", "subscription-keyの値")]
    [Serializable]
    publicclass HelloLuisDialog : LuisDialog<object>
    {
        // noneの場合
        [LuisIntent("")]
        public Task None(IDialogContext context ,LuisResult result)
        {

        }

        // helloの場合
        [LuisIntent("hello")]
        public Task Hello(IDialogContext context, LuisResult result)
        {

        }
    }
}

LuisIntentに空文字を渡すとnoneの場合になります。

簡単なNoneメソッドから作っていきましょう。

// noneの場合
[LuisIntent("")]
public async Task None(IDialogContext context ,LuisResult result)
{
    await context.PostAsync("すいません。わかりません");
    context.Wait(this.MessageReceived);
}

メッセージを返した後に、MessageReceivedメソッドの処理を待つことで再度LUISとの連携待ちに入ります。

次に、Helloメソッドです。こいつは、nameというEntityを抜き出す必要があります。 抜き出すにはLuisResultTryFindEntityメソッドで抜き取れます。

// helloの場合
[LuisIntent("hello")]
public async Task Hello(IDialogContext context, LuisResult result)
{
    EntityRecommendation nameEntity;
    if (result.TryFindEntity("name", out nameEntity))
    {
        await context.PostAsync($"こんにちは!!{nameEntity.Entity}さん!!");
    }
    else
    {
        await context.PostAsync("すいません。わかりません");
    }

    context.Wait(this.MessageReceived);
}

EntityRecommendationを使って抜き出したEntityを操作できます。Entityプロパティでテキストにアクセスできます。 なので、基本的に文字列を組み立ててユーザーにフィードバックをしています。 名前がとれなかった場合は、すいません。わかりませんと言っています。

そして、context.Wait(this.MessageReceived);でLUISとの連携を待ってます。

最後にMessagesControllerで作成したDialogを使うように変更します。

public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
    if (activity.Type == ActivityTypes.Message)
    {
        await Conversation.SendAsync(activity, () => new HelloLuisDialog());
    }
    else
    {
        HandleSystemMessage(activity);
    }
    var response = Request.CreateResponse(HttpStatusCode.OK);
    return response;
}

動作確認

実行してエミュレータでつないでみましょう。

f:id:okazuki:20161001151422p:plain

ちゃんと動いてますね!ということでLUISを使って自然言語で最初のトリガーを識別して、そのあとFormFlowを使って必要な情報を集めてアクションをするといった流れとかができますね。

Azure Mobile AppsでテーブルのクエリにIN句を使いたい

$
0
0

ODataはin句をサポートしてないらしい?ので出来ません。(完) こんな定義らしい。

filter = '$filter' EQ boolCommonExpr

boolCommonExpr = ( isofExpr 
                 / boolMethodCallExpr 
                 / notExpr  
                 / commonExpr
                   [ eqExpr 
                   / neExpr 
                   / ltExpr  
                   / leExpr  
                   / gtExpr 
                   / geExpr 
                   / hasExpr 
                   ]
                 / boolParenExpr
                 ) [ andExpr / orExpr ]

というのではあんまりなので代替案を。 渡された配列に対して1要素ずつ比較するのをORでつないでやればOKという方法で代替できます。 ただLINQでORをやるのは苦行で式木に踏み込まないといけません。

ということで、以下のようなTodoItemクラスに対して

using System;

namespace PrismMobileApp.Models
{
    publicclass TodoItem
    {
        publicstring Id { get; set; }

        publicstring Text { get; set; }

        publicbool Complete { get; set; }

        publicbyte[] Version { get; set; }

        public DateTimeOffset? CreatedAt { get; set; }

        public DateTimeOffset? UpdatedAt { get; set; }
    }
}

IMobileServiceTable<TodoItem>型のTodoItemTableがある前提で引数で渡された文字列配列に対して一致するIDを持つTodoItemを抜き出す処理を書いてみましょう。

public Task<IEnumerable<TodoItem>> GetIds(paramsstring[] ids)
{
    // x => false or id[0] == x.Id or id[1] == x.Id ...
    var x = Expression.Parameter(typeof(TodoItem), "x");
    Expression expression = Expression.Constant(false);
    foreach (var id in ids)
    {
        expression = Expression.OrElse(
            expression,
            Expression.Equal(Expression.Property(x, nameof(TodoItem.Id)), Expression.Constant(id)));
    }

    var lambda = Expression.Lambda<Func<TodoItem, bool>>(expression, x);
    returnthis.TodoItemTable.CreateQuery()
        .Where(lambda)
        .OrderBy(y => y.Text)
        .ToEnumerableAsync();
}

ということでこういう感じになります。式木の初歩的な感じですね。コメントに書いてあるような式を組み立ててます。式の中身を組み立てて最後にラムダ式にしてWhereに渡すという流れです。

では、よいLINQライフを。

UWPのコンパイル時データバインディングでString.Formatを使う

$
0
0

使えます。

こんなコードビハインドを書いて…

using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace App5
{
    publicsealedpartialclass MainPage : Page
    {
        public DateTime Now => DateTime.Now;

        public MainPage()
        {
            this.InitializeComponent();
        }

        privatevoid Button_Click(object sender, RoutedEventArgs e)
        {
            this.Bindings.Update();
        }
    }
}

そして、XAMLでこんな感じで書けます。

<Page x:Class="App5.MainPage"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="using:App5"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:System="using:System"mc:Ignorable="d"><StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"><TextBlock Text="{x:Bind System:String.Format('{0:yyyy/MM/dd HH:mm:ss.fff}', Now)}" /><Button Content="Update"Click="Button_Click" /></StackPanel></Page>

ただまぁ、何故かエディタ上はエラーになるのですが…。実行すると動きます。

f:id:okazuki:20161005000837p:plain

まぁ、毎回フォーマットを指定するのもだるいので、書式出力系共通関数みたいなのを用意しておいて

using System;

namespace App5
{
    publicstaticclass Utils
    {
        publicstaticstring Format(DateTime dateTime)
        {
            return dateTime.ToString("yyyy/MM/dd HH:mm:ss.fff");
        }
    }
}

こういう感じで使うのがいいでしょう。

<Page x:Class="App5.MainPage"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="using:App5"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:System="using:System"mc:Ignorable="d"><StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"><TextBlock Text="{x:Bind local:Utils.Format(Now)}" /><Button Content="Update"Click="Button_Click" /></StackPanel></Page>

ちなみに、これでもデザイナは表示されますがエディタ上はエラーになります…。 ツール類しっかりしてくれ。

Visual Studio Team ServicesからXamarin Test Cloudを使ってみよう(Android編)

$
0
0

iOS編はあるのかって?Mac持ってないのでできないのでMac持ってる人だれかお願いします。

Xamarinプロジェクトの作成

ということで、テスト対象のプロジェクトを作りましょう。 今回は、Prismのテンプレートパックを入れたら作られるプロジェクトテンプレートで行ってみたいと思います。

visualstudiogallery.msdn.microsoft.com

AndroidとiOSのプロジェクトを一応作っておきます。

f:id:okazuki:20161011125930p:plain

UITestプロジェクトの作成

プロジェクトの新規作成でCross-Platformの中にあるUI Test App (Xamarin.UITest | Cross-Platform)を作成します。今回はUITestの書き方の説明の回ではないので、さくっとデフォルトのままでいきます。ちょっとだけ中身を見ると、スクリーンショットをとっているだけのテストになります。

using System;
using System.IO;
using System.Linq;
using NUnit.Framework;
using Xamarin.UITest;
using Xamarin.UITest.Queries;

namespace UITest1
{
    [TestFixture(Platform.Android)]
    [TestFixture(Platform.iOS)]
    publicclass Tests
    {
        IApp app;
        Platform platform;

        public Tests(Platform platform)
        {
            this.platform = platform;
        }

        [SetUp]
        publicvoid BeforeEachTest()
        {
            app = AppInitializer.StartApp(platform);
        }

        [Test]
        publicvoid AppLaunches()
        {
            app.Screenshot("First screen.");
        }
    }
}

Xamarin Test Cloud

次は、VSTSにプロジェクトを上げる前にXamarin Test Cloudの環境を整えておきます。 Xamarin Test Cloudは、会社のメアドを使えば30日間トライアルできます。そうじゃなければ、ちょっと高いです。

Xamarin Test Cloud

画面上部のNew Test Runを選ぶとテスト対象の機種を選ぶ画面になります。+ New Android appから好きなものを選びましょう。私は3種類くらい選びました。

f:id:okazuki:20161011131152p:plain

次に言語とかえらびます。

f:id:okazuki:20161011131339p:plain

そうすると、こういうコマンドうってねっていう画面が出てきます。このコマンドを控えておきます。

f:id:okazuki:20161011131709p:plain

Visual Stduio Team Servicesの作成

VSTSでプロジェクトを作成します。https://oooooo.visualstudio.comからプロジェクトを作成しましょう。

f:id:okazuki:20161011131847p:plain

Codeのタブに行くとGitのリポジトリのURLが取得できるので、先ほどVisual Studioで作成したプロジェクトをPushしておきましょう。

f:id:okazuki:20161011132146p:plain

ビルド定義の作成

次にBuild & Releaseを開きます。New definitionを選択しましょう。

f:id:okazuki:20161011132321p:plain

Xamarin.Androidというのがあるので、それを選びましょう。

f:id:okazuki:20161011132502p:plain

次に、ビルドするリポジトリとかを選びましょう。GitHubとかも選べますね。今回はVSTSのリポジトリなのでそのままでいいです。

f:id:okazuki:20161011132705p:plain

作成するとTest Cloudのところだけ赤くなってるので、さきほど採取したコマンドから該当パラメータをコピペします。 赤で塗りつぶしてたのがそれにあたります。

f:id:okazuki:20161011132940p:plain

Saveして名前をつけます。ここではXamarinTestCloudという名前にしました。

Queue New Buildを押すとビルドが始まります。

こんなビルドエラーが出たら

f:id:okazuki:20161011133622p:plain

VSTS上のXamarinのバージョンと、ローカルでのXamarinのバージョンが食い違ってる(主にVSTSのほうが古いとか)とビルドでこける可能性があります。こんなときはつかえねーなーとあきらめるんじゃなくて、ビルドを自分のビルドマシンでやることができます。httpsで外部サイトが見える状態ならできるみたいなので敷居はわりと低いでしょう(認証プロキシは未検証)

画面上部の歯車を押してAgent Queuesを押すとDownload agentというのがあるので、そこからagentをダウンロードします。

f:id:okazuki:20161011133915p:plain

ダウンロードしながら、認証に必要なキーをとってきます。画面右上のほうからSecurityを選びます。

f:id:okazuki:20161011134142p:plain

Personal access tokan(PATとか略されてる)でAddをおして適当に情報を入れます。

f:id:okazuki:20161011134449p:plain

アクセス用のキーは控えておきます。(消すときにも必要になるので大事にとっておきましょう)

f:id:okazuki:20161011134709p:plain

ダウンロードしたagentをc:\agentあたりに展開します。

config.cmdを実行して構成します。URLは、自分のVSTSのURLでPATはさきほど入手したものです。あとはデフォルトでいいでしょう。サービスとして動かしたいひとはそのようにしてください。

PS C:\agent> .\config.cmd

>> Connect:

Enter server URL > https://okazuki.visualstudio.com
Enter authentication type (press enter for PAT) > PAT
Enter personal access token > ****************************************************
Connecting to server ...

>> Register Agent:

Enter agent pool (press enter for default) >
Enter agent name (press enter for KAZUKI-SFB) >
Scanning for tool capabilities.
Connecting to the server.
Successfully added the agent
Testing agent connection.
Enter work folder (press enter for _work) >
2016-10-11 04:51:23Z: Settings Saved.
Enter run agent as service? (Y/N) (press enter for N) >

run.cmdを実行するとエージェントが動き始めます。

PS C:\agent> .\run.cmd
Scanning for tool capabilities.
Connecting to the server.
2016-10-11 04:53:23Z: Listening for Jobs

この状態でビルド定義を編集してGeneralのタブにあるDefault agent queueをHostedからDefaultに変えることでagentをつかうようになります。

f:id:okazuki:20161011135638p:plain

SaveしてQueue new buildしましょう。

ビルド結果の確認

こんな感じでビルドが通れば成功です。

f:id:okazuki:20161011140652p:plain

Xamarin Test Cloud側にもこんな感じで結果が表示されます。

f:id:okazuki:20161011140845p:plain

f:id:okazuki:20161011140959p:plain

今後の課題

  • ローカルでUIテストしたいね
  • iOSどうなるの?

JXUG #18 「Azure Mobile Apps入門」資料公開

$
0
0

JXUG #18で発表してきました。 デモが想定の半分くらいしかできなかったのが心残り(XAMLインテリセンスないと書けない体になってる…)ですが、資料を公開しておきます。

Azure Mobile Appsのローカル同期機能で作成されるDBの中身を見てみよう(Android版)

$
0
0

さて、ローカル同期したAndroidのデータをひっこぬいてBrowser for SQLIteで中身を見てみたいと思います。 今回は、エミュレータで実行してる前提でコマンドを書いています。

まずadbのshellでつなぎます。

adb -e shell

アプリパッケージ名を調べます。 アプリパッケージ名はVisual StudioでDroidプロジェクトのプロパティを開いてAndroid ManifestのタブにあるPackage nameがそれにあたります。仮にここではcom.xamarin.sample.sample1030という前提で話を進めます。

run-asコマンドで、アプリのフォルダに入り込みます。

run-as com.xamarin.sample.sample1030

PersonalフォルダのパスをMobileServiceSQLiteStoreに渡していたら、filesフォルダにDBができているはずです。

cd files
ls

localstore.dbという名前で今回はDBを作ったのでこれを取得します。 いったん適当なフォルダに避難させます(ここではダウンロードフォルダ)

cp localstore.db /sdcard/Download

exitでadbを抜けてpullしてきます

adb pull /sdcard/Download/localstore.db

そうすると、カレントディレクトリにlocalstore.dbができます。 あとはDB Browser for SQLIteで除くと中身が以下のように見えます。

f:id:okazuki:20161030135719p:plain

Surface DialをUWPアプリから使う

$
0
0

Surface Studioと同時に発表されて何かと話題のSurface Dialですが、こいつのAPIはWindows 10 AUでこっそりと入っていたみたいです。 ということで簡単にですが使い方を書いてみようと思います。

何もしないという選択肢

まず、Surface Dialですが何もしなくてもデフォルトの挙動だけで乗り切るというのもあります。 その場合は、スクロールや拡大縮小(システムのメニューでカスタマイズ可能)などの操作ができるという感じになります。

f:id:okazuki:20161111161723p:plain

ということで、何もしなくてもSurface Dialでアプリを動かすことはできます。

そうはいっても

そうはいってもオリジナルの挙動をさせたいというのがあると思います。

メニューを追加したい

Surface Dialを長押ししたときに出るメニューに自分の項目を追加することができます。 まず、Surface Dialを使うには、RadialControllerクラスのインスタンスを取得する必要があります。 これはRadialControllerクラスのCreateForCurrentViewというstaticメソッドで作成可能です。

次にメニュー項目を作成します。 メニュー項目はRadialControllerMenuItemクラスで、これもstaticなファクトリメソッドが用意されてるので、それで作ることができます。

CreateFromKnownIconメソッドは名前の通りRadialControllerMenuKnownIcon列挙体に定義されたアイコンを持ったものが作成できます。

RadialControllerMenuKnownIcon enumeration - Windows app development

RadialControllerMenuItemクラスが出来たら、Invokedイベントを購読することで選択されたときの処理を書くことができます。

コード例を以下に示します。

publicsealedpartialclass MainPage : Page
{
    private RadialController RadialController { get; set; }
    public MainPage()
    {
        this.InitializeComponent();
    }

    protectedoverridevoid OnNavigatedTo(NavigationEventArgs e)
    {
        base.OnNavigatedTo(e);
        this.RadialController = RadialController.CreateForCurrentView();
        var item1 = RadialControllerMenuItem.CreateFromKnownIcon("Item1", RadialControllerMenuKnownIcon.InkColor);
        item1.Invoked += Item1_Invoked;
        this.RadialController.Menu.Items.Add(item1);
    }

    private async void Item1_Invoked(RadialControllerMenuItem sender, object args)
    {
        var dlg = new MessageDialog("Item1 invoked");
        await dlg.ShowAsync();
    }
}

実行してItem1を選択すると以下のようになります。

f:id:okazuki:20161111164311p:plain

RadialControllerMenuItemCreateFromIconメソッドを使うことでカスタムのアイコンを使うこともできます。

var item1 = RadialControllerMenuItem.CreateFromIcon("Item1", 
    RandomAccessStreamReference.CreateFromUri(new Uri("ms-appx:///Assets/CustomIcon.png")));

押し込みを検出したい

メニューを選択すると、イベントが反応するようになります。 一番簡単なのが押し込まれたときのイベントです。 RadialControllerクラスのButtonClickedイベントを購読することで、そのメニュー選択時のクリック動作を処理することができます。

RadialControllerクラスのMenuプロパティからGetSelectedMenuItemメソッドで現在選択されてるメニューが取得できるので、それを見るか、その中のTagを見て 動作をカスタマイズするか、自分でInvokedイベントで今ステータスがどういう状況にあるのか管理するフラグでも持たせておくのがいいと思います。

先ほどのコードにButtonClickedを追加したものをいかに示します。

publicsealedpartialclass MainPage : Page
{
    private RadialController RadialController { get; set; }
    public MainPage()
    {
        this.InitializeComponent();
    }

    protectedoverridevoid OnNavigatedTo(NavigationEventArgs e)
    {
        base.OnNavigatedTo(e);
        this.RadialController = RadialController.CreateForCurrentView();
        var item1 = RadialControllerMenuItem.CreateFromKnownIcon("Item1", RadialControllerMenuKnownIcon.InkColor);
        item1.Invoked += Item1_Invoked;
        this.RadialController.Menu.Items.Add(item1);

        this.RadialController.ButtonClicked += RadialController_ButtonClicked;
    }

    private async void RadialController_ButtonClicked(RadialController sender, RadialControllerButtonClickedEventArgs args)
    {
        var item = sender.Menu.GetSelectedMenuItem();
        var dlg = new MessageDialog($"Selected menu is {item.DisplayText}");
        await dlg.ShowAsync();
    }

    private async void Item1_Invoked(RadialControllerMenuItem sender, object args)
    {
        var dlg = new MessageDialog("Item1 invoked");
        await dlg.ShowAsync();
    }
}

実行して、Item1を選択した状態でSurface Dialを押し込むと以下のようにダイアログが出てきます。

f:id:okazuki:20161111165139p:plain

回転の検出

次に回転の検出方法です。 Surface Dialは名前のとおりくるくる回ります。

どれくらい回ったらイベントは発火するかという値の設定がRotationResolutionInDegressプロパティで設定できて、RotationChangedイベントで回転時の処理が書けます。 イベント引数から、どれくらい回転したかという情報もとることができるので、それを見て処理をする形になると思います。

クリック時と同じように、今どんなメニューが選択されてる状態なのかというのもとることができます。

publicsealedpartialclass MainPage : Page
{
    private RadialController RadialController { get; set; }
    public MainPage()
    {
        this.InitializeComponent();
    }

    protectedoverridevoid OnNavigatedTo(NavigationEventArgs e)
    {
        base.OnNavigatedTo(e);
        this.RadialController = RadialController.CreateForCurrentView();
        var item1 = RadialControllerMenuItem.CreateFromKnownIcon("Item1", RadialControllerMenuKnownIcon.InkColor);
        item1.Invoked += Item1_Invoked;
        this.RadialController.Menu.Items.Add(item1);

        this.RadialController.ButtonClicked += RadialController_ButtonClicked;
        // 回転this.RadialController.RotationResolutionInDegrees = 10;
        this.RadialController.RotationChanged += RadialController_RotationChanged;
    }

    privatevoid RadialController_RotationChanged(RadialController sender, RadialControllerRotationChangedEventArgs args)
    {
        Debug.WriteLine($"{args.RotationDeltaInDegrees} rotated");
    }

    private async void RadialController_ButtonClicked(RadialController sender, RadialControllerButtonClickedEventArgs args)
    {
        var item = sender.Menu.GetSelectedMenuItem();
        var dlg = new MessageDialog($"Selected menu is {item.DisplayText}");
        await dlg.ShowAsync();
    }

    private async void Item1_Invoked(RadialControllerMenuItem sender, object args)
    {
        var dlg = new MessageDialog("Item1 invoked");
        await dlg.ShowAsync();
    }
}

実行するとデバッグウィンドウにこんな表示がされます。

10 rotated
10 rotated
10 rotated
10 rotated
10 rotated
10 rotated
10 rotated
10 rotated
-10 rotated
-10 rotated
-10 rotated
-10 rotated
-10 rotated
-10 rotated
-10 rotated
-10 rotated

プログラムからのメニューの設定

今まではユーザーがSurface Dialを長押ししてくるくると回してメニューを選ぶという操作をしていましたが、プログラムからメニューを設定することもできます。

これはMenuプロパティのSelectMenuItemメソッドにRadialControllerMenuItemを渡すことでできます。

例えば、ボタンクリック時にメニューを切り替えるのは以下のようになります。

privatevoid Button_Click(object sender, RoutedEventArgs e)
{
    // プログラムからメニューの選択this.RadialController.Menu.SelectMenuItem(
        this.RadialController.Menu.Items.First());
}

こうすることで、コンテキストに応じてメニューを動的にプログラムから切り替えるといったことができます。

未確認事項

Surface Dialの位置とか

Surface Dialをクリックしたときや回転したときのイベント引数にContractというプロパティがあります。 これは現時点でnullが入ってきてるのですが、Positionなどが取れるので、おそらく画面上にSurface Dialを置いたときの位置などが取れるのだと思います。 Surface Studioか、今後Bookなどでもファームアップデートで動くといいな。

Surface Dialを画面に置いたときの動作など

それっぽいイベントがあるのですが、上記の理由で試せていません。

まとめ

APIは単純で簡単に使えますが、マウスと競合するのでおそらくデモのようにペンと組み合わせて使うことになるデバイスだと思います。 アイデア勝負っぽいですね。

UWPアプリでSurface Dialのデフォルトメニューを消す

$
0
0

Surface Dial対応のアプリを作ろうと思ったときにデフォルトのメニューが邪魔なときがあります。

f:id:okazuki:20161113182926p:plain

これは簡単に消すことができます。

// デフォルトメニューを消す
var config = RadialControllerConfiguration.GetForCurrentView();
config.SetDefaultMenuItems(Enumerable.Empty<RadialControllerSystemMenuItemKind>());

RadialControllerConfigurationを取得してSetDefaultMenuItemsRadialControllerSystemMenuItemKindの空のIEnumerableを渡してやればOKです。

注意点が1つあって本当に空のメニューは作れないという点です。 SetDefaultMenuItemsで空を指定しても、プログラムから他のメニューを追加してない場合は、デフォルトの表示になってしまいます。

ということで、自前のメニューを1つ追加してSetDefaultMenuItemsで空を指定した場合の見た目を以下に示します。

f:id:okazuki:20161113183440p:plain

当然っちゃ当然だけどXamarin.FormsでHoloLens対応アプリ作れます

$
0
0

HoloLensではUWPが動く。 Xamarin.FormsではUWPの開発が可能。

ということで当然ですが動きます。HoloLensでXamarin.Formsで作ったアプリが!! iOS, Android対応アプリをXamarin.Formsで作ってる人は、やってみてもいいのではと思いました。

実験

Prism.FormsのテンプレートパックでUWPを対象にしたXamarin.Formsのプロジェクトを作ります。 作ったら、HoloLensのエミュレータで起動を試みてみます。

f:id:okazuki:20161113190705p:plain

無事起動しました!

ということで

Xamarinはいいぞ!

.NET Coreと.NET Standardメモ

$
0
0

復習の時間だ。

.NET Core

日本語だとこのページがわかりやすいと思いました。

.NET Core とオープン ソース

要約するとこんな感じ。

  • .NET Frameworkのサブセット
  • マルチプラットフォーム対応
    • Mac
    • Linux
    • Windows
  • OSS!!

ASP.NET Core

.NET Frameworkと.NET Coreで動くMVC, WebAPI, WebPagesが入ったフレームワークです。

.NET Coreで動くということで、WindowsやMacやLinuxで動くという点が特徴です。またASP.NETの初期バージョンからあったWebFormが無かったり、WCFといったものが無いという点も特徴ですかね。

EntityFramework Core

EntityFrameworkの新バージョン!

.NET Frameworkと.NET Coreで動くORマッパー。 パフォーマインス改善とかされてるけど、前身のEntityFramework 6.0と比べると機能面では現時点で劣ってる。

.NET Standard

.NET Framework, Mono, .NET Coreと乱立してる.NETファミリーで共通で使えるAPIセットを提供しようというもの。

今まではPCLで各プラットフォームの最大公約数のAPIしか使えないようにしようという方法だったのを、きちんと.NETの基本クラスライブラリの整理整頓していこうという流れですかね。

.NET Standardはバージョンに応じてサポートされてるプラットフォームが決まっています。 例えば1.2だと.NET Core 1.0, .NET 4.5.1, Mono, UWP 10.0, Windows 8.1, Windows Phone 8.1に対応しています。ここらへんはキレイに表で整理されていて、以下のページの.NET Platforms Supportのページで確認できます。

docs.microsoft.com

まとめ

持ったより短い。きっと書き損じてることはいっぱいあるんだろう。

ReactiveProperty v3.3.1をリリースしました

$
0
0

ReactiveProperty v3.3.1をリリースしました。

www.nuget.org

新機能はありませんが、依存しているライブラリを最新版にあげました。

  • System.Reactive: v3.0.0 → v3.1.0
  • UWPのビヘイビア: 1.?.? → 2.0.0

今後ともよろしくお願いします。


クライアントサイドから見たConnect(); 2016はどうだったかという感想

$
0
0

UWPに関しては華麗にスルーされてました。(悲しい) ただ、Visual Studio 2017 RCでは、試してないけどUWP開発のための機能強化も結構含まれてるのでさみしくなんかありません。 デモアプリもUWP多かったですしね!

ということで本題です。

クライアントサイドで発表されたこと

ほぼ、Xamarinに絡んでいる点が特徴です。

  • Visual Studio Mobile CenterのPreview
    • モバイルアプリ(Xamarinだよね)開発のための便利なものの組み合わせ。XamarinテストクラウドやHockeyAppとか
  • Visual Studio for Mac
    • Xamarin Studioどうなるの?という感じのXamarinによるモバイルアプリ開発とサーバーサイド開発もできるできる子
  • TizenがまさかのXamarin.Formsで開発可能
  • UI Automationのテストが改善されたみたい(コードの見たいところが見えなかったので詳細は不明)

所感

盛りだくさんなイベントでしたが、クライアントサイドに絞ると意外とあっさりしてます。

Visual Studio Mobile Center Preview

Mobile Centerは、今までばらばらだったものがまとまるという点で歓迎すべきことですね。 実際に使って評価してみて、Blog書けたらいいなと思います(お金かからなかったら…)

Visual Studio for Mac

Visual Studio for Macは、事前お漏らしが無ければ最高に沸いた発表だったと思うのですが、ちょっと残念でしたね。Macでもモバイルアプリに限定されますが、Webアプリ側も含めたすべての開発ができる環境(Xamarin Studioでも出来てたみたいだけど)がきちんと皆に認識されたのは大きな変化だと思います。 .NET Core自体も1.1でパフォーマンス改善なども入って、よりよくなっているのでMacだけで、.NETを使ってモバイルアプリのフロントエンドからバックエンドまで開発するというプロジェクトが出てくる感じがしますね。

UI Automation

UI Automationは、使いやすいといいな。詳細に期待。

Tizen

個人的に一番びっくりしたのがこれです。

Tizenといえば日本国内の展開がどうなったかというのは置いておいて、HTML5ベースでのアプリ開発を行うというのが1つのウリだったと記憶しています。 それが、ここにきてVisual Studioを使って.NETで開発を行う方向に舵を切ったのは興味深いなと思いました。

個人的には、.NET + Visual Studioという組み合わせは最高の開発生産性を出せる組み合わせの1つだと思っているのでHTML5をエディタでちまちま書いてるのに比べるとダントツで作りやすいし、Xamarin.Formsベースということは、基本的なUIを構築するための(ここ大事)コントロールが揃ってるということになります。

Twitterでも呟きましたが

  • Mac
  • Android
  • iOS
  • Windows Phone
  • Windows 8.1
  • UWP
  • Tizen

というプラットフォームに対応できるほどUIという難しいレイヤを抽象化できているXamarin.Formsというフレームワークが本当に素晴らしいと思いました。XAMLも書けるしね!

Xamarin.Formsは、やっぱりアツイテクノロジですね。

まとめ

Tizenちょっと興味出てきた。MacBook Pro買いたいな…。

Microsoft Azure Mobile AppsのQuickstartのTodoアプリがUWP版だと例外で落ちることがある

$
0
0

最近、Microsoft Azure Mobile AppsでQuickstartのTodoアプリのひな型にUWPが追加されました。 小躍りするくらいうれしいことなのですが、このTodoアプリですが追加した複数Todoを追加したあとに一番末尾のTodoをクリックしてCompletedにすると落ちるという残念なやつだったりします。(2016/11/18現在)

確認してみると、ItemSelectedイベントで最後の要素を選択してるときに、今のリストサイズよりも小さい要素数のコレクションをItemsSourceに設定すると再現しました。 Xamarin.Formsのバグですね。

解決方法

バグレポートまでは見つけてませんが、既知の問題だったらしく。Xamarin.Formsのバージョンを最新の安定板にアップすることで解決します。 (デフォルトでは2.0.xが設定されてる)

まとめ

Xamarin.Formsは、まだ枯れた技術ではないので最新版を使うのがやっぱりよさそうですね。

Xamarin.Formsで笑顔測定をしよう(AndroidとUWP)

$
0
0

最近笑顔してますか?こんな感じの笑顔測定アプリを作ってみようと思います。これならボッチでも笑顔の練習できるよ!

左がニュートラルな顔、右が笑顔の場合です!スコアが変わってますね。

f:id:okazuki:20161121163608p:plain

ということで、Cognitive ServiceのFace APIを使って笑顔判定するものをXamarin.Formsで作ってみようと思います。 プロジェクトテンプレートは安定のPrism Tmeplate Packで作ります。 これだとおぜん立てとかがあって楽なので。

ということで、今回のお題を達成するにはカメラで写真を撮るということとCognitive ServiceのAPIを呼び出すという2つの段階を踏まないといけません。順番に見ていきましょう。

カメラにアクセスする

Xamarinには、Pluginという便利なクロスプラットフォームで各種プラットフォーム固有の機能にアクセスするためのライブラリがあったりします。 カメラもPluginがあったりします。

github.com

ということで、NuGetでXam.Plugin.Mediaをインストールしましょう。 するとreadme.txtが表示されます。

そこに書いてある通りに、以下の属性の追加とMainActivityのメソッドのオーバーライドを行います。

[assembly: UsesFeature("android.hardware.camera", Required = false)]
[assembly: UsesFeature("android.hardware.camera.autofocus", Required = false)]
publicoverridevoid OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
{
    PermissionsImplementation.Current.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}

UWPは、Webカメラの機能にマニフェストでチェックを入れておきます。

カメラの下準備が整ったら以下のような初期化コードをApp.xaml.csに追加します。

Plugin.Media.CrossMedia.Current.Initialize();

これで下準備は完了です。 以下のようなコードで画像を取得してImageSource(画像のもと)を生成できます。

var file = await CrossMedia.Current.TakePhotoAsync(new Plugin.Media.Abstractions.StoreCameraMediaOptions
{
    DefaultCamera = Plugin.Media.Abstractions.CameraDevice.Front,
    AllowCropping = false,
});
if (file == null) { return; }

this.ImageSource = ImageSource.FromStream(() => file.GetStream());

Cognitive Serviceの呼び出し

次にCongnitive Serviceの呼び出しです。 Azureポータルから、Face APIとかで検索するとCongnitive Serviceが作成できるのでFace APIを選んでFreeプランあたりで作成しておきましょう。

ポータルのKeyからアクセスに必要になるキーを取得します。

f:id:okazuki:20161121155113p:plain

キーを取得したら、NuGetからMicrosoft.ProjectOxford.Faceをインストールしましょう。OxfordというのはCongnitive Serviceのプレビュー時代の名前になります。

Face APIの呼び出し自体は簡単で、返して欲しい属性(今回の場合は笑顔判定なのでスマイル)を指定してDetectAsyncを呼び出すだけです。クライアントの生成時に先ほど取得したキーを指定するのを忘れずに。

var client = new FaceServiceClient("ここにキーを設定する");
var result = await client.DetectAsync(file.GetStream(), returnFaceAttributes: new[]
{
    FaceAttributeType.Smile,
});

if (!result.Any()) { return; }

await this.PageDialogService.DisplayAlertAsync("Smile point", $"Your smile point is {result.First().FaceAttributes.Smile * 100}", "OK");

実行結果

UWPとAndroidしか持ってないのでiOSは試せてない(iOSはiOSでプラグインの設定が必要そう)のですがばっちり動きました。

Android

f:id:okazuki:20161121163922j:plain

UWP

f:id:okazuki:20161121163949p:plain

まとめ

ということでXamarinとMediaのPluginとCongnitive Serviceを使って簡単笑顔測定アプリが出来ました。 Xamarin.Formsで作ったからUWPでもAndroidでも動くので素敵ですね。

ということで、ソースコードは以下にあげています。

github.com

Xamarinはいいぞ。

Xamarinで処理中を表すインジケーターを出したい

$
0
0

先日Cognitive Serviceを使った笑顔判定機を作りました。

こいつですが、Web上のAPIを呼び出して回線状況に応じては、そこそこ時間がかかるにも関わらず、処理中を示すUIが表示されていませんでした。

Xamarin.Formsでは、ActivityIndicatorクラスを使うことで、簡単にインジケーターを出すことができます。

写真はおっさんの顔になってしまうので省きますが…

使い方は簡単です。画面の全体を覆うようにGridを置いて、そこにメインコンテンツとActivityIndicatorを置くだけです。そして、ActivityIndicatorIsRunningプロパティにTrueかFalseを設定することで表示・非表示を切り替えます。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"xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"prism:ViewModelLocator.AutowireViewModel="True"x:Class="SmileXamarinApp.Views.MainPage"Title="MainPage"><ContentPage.ToolbarItems><ToolbarItem Text="写真"Command="{Binding TakePhotoCommand}" /></ContentPage.ToolbarItems><Grid><StackLayout HorizontalOptions="Center"VerticalOptions="Center"><Image Source="{Binding ImageSource}" /></StackLayout><ActivityIndicator IsRunning="{Binding IsBusy}" /></Grid></ContentPage>

IsBusyがVMに追加したプロパティでAPI呼び出し中だけTrueになるように制御しておきます。

using Microsoft.ProjectOxford.Face;
using Plugin.Media;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using Prism.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace SmileXamarinApp.ViewModels
{
    publicclass MainPageViewModel : BindableBase, INavigationAware
    {
        private ImageSource imageSource;

        public ImageSource ImageSource
        {
            get { returnthis.imageSource; }
            set { this.SetProperty(refthis.imageSource, value); }
        }

        private IPageDialogService PageDialogService { get; }

        public DelegateCommand TakePhotoCommand { get; }

        privatebool isBusy;

        publicbool IsBusy
        {
            get { returnthis.isBusy; }
            set { this.SetProperty(refthis.isBusy, value); }
        }

        public MainPageViewModel(IPageDialogService pageDialogService)
        {
            this.PageDialogService = pageDialogService;
            this.TakePhotoCommand = new DelegateCommand(async() => await this.TakePhotoAsync());
        }

        private async Task TakePhotoAsync()
        {
            var file = await CrossMedia.Current.TakePhotoAsync(new Plugin.Media.Abstractions.StoreCameraMediaOptions
            {
                DefaultCamera = Plugin.Media.Abstractions.CameraDevice.Front,
                AllowCropping = false,
            });
            if (file == null) { return; }

            this.ImageSource = ImageSource.FromStream(() => file.GetStream());

            try
            {
                this.IsBusy = true;
                var client = new FaceServiceClient("Your API Key");
                var result = await client.DetectAsync(file.GetStream(), returnFaceAttributes: new[]
                {
                    FaceAttributeType.Smile,
                });
                if (!result.Any()) { return; }
                await this.PageDialogService.DisplayAlertAsync("Smile point", $"Your smile point is {result.First().FaceAttributes.Smile * 100}", "OK");
            }
            finally
            {
                this.IsBusy = false;
            }

        }

        publicvoid OnNavigatedFrom(NavigationParameters parameters)
        {
        }

        publicvoid OnNavigatedTo(NavigationParameters parameters)
        {
        }
    }
}

これで、処理中なのかなどうなのかな?という疑問を持たなくてすむようになりました。コードは同じくGitHubのリポジトリにあげています。

github.com

ReactiveProperty 3.4.0をリリースしました

$
0
0

www.nuget.org

  • Reactive Extensionsを3.1.0から3.1.1に更新しています。
  • DependencyObject(UWP & WPF)に以下の拡張メソッドを追加しました
    • ObserveDependencyProperty: DependencyPropertyの変更を監視するIObservable<Unit>を返します。
    • ToReadOnlyReactiveProperty: DependencyPropertyからReadOnlyReactivePropertyを生成します。
    • ToReactiveProperty: DependencyPropertyからReactivePropertyを生成します。
Viewing all 1388 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>