Knowlbo 開発者ブログ

株式会社Knowlboの開発者ブログです。

自社Webアプリを勝手にハックしてXamarinアプリ化してみた

開発部の醍醐です。
今日はちょっとした遊びハックな記事を・・・

こんな遊びをしてみます

自社Webアプリケーションである「勤怠管理システム タイムカードEX」の一部機能を勝手にXamarin Formsアプリとして実装してみようと思います。
Webアプリケーションに対するHTTP GET / POSTをXamarinアプリケーション内部でシミュレートするのですが、Xamarin Formsにおいて以下のような技術トピックを利用して実現します。

  • ログイン情報設定用のモーダルフォームを表示する
  • ログイン情報をアプリケーション領域に保存・読み込みする
  • WebアプリケーションのHTTP GET / POST 処理を、Xamarin Fromsアプリでバックエンドに実装する
  • レスポンスで得られたHTML内容は XDocument でスクレイピングして必要な情報を抜き出す
  • 出勤・退勤処理時に使用する位置情報をスマホ機能により取得する

タイムカードEXはこんなシステム

弊社が提供しているサービスとして「タイムカードEX」というものがあります。
これは出退勤管理を行うシステムになります。勤務シフトや勤務時間、有給休暇日数の管理等、多彩な機能があるのですが、その中で、各社員が「出勤・退勤」を行う処理の部分にのみ注目します。
また、出勤・退勤処理については「ICカードによるタッチ」や「PCブラウザからの出勤・退勤処理」などのインターフェイスが用意されているのですが、その中で「スマホから利用するWebアプリによる出勤・退勤」部分を Xamarin Forms で実装してみようと思います。

Xamarin化する予定のモバイル用Webアプリ画面

今回 Xamarin Forms化する「元のWebアプリの画面は以下のようなものになります。

【ログイン画面】
f:id:daigo-knowlbo:20161129210253p:plain

ユーザーIDとパスワードを入力する、ごく一般的なWebアプリケーションのログイン画面になります。

【出勤・退勤処理画面】
f:id:daigo-knowlbo:20161129210242p:plain

出勤時間ラベル:
 すでに出勤処理が行われている場合に、出勤時間が表示されます。
出勤チェックボックス:
 出勤時にチェックを行います。チェックを行うとHTML上で位置情報(緯度・経度)を取得して出勤用HTTP POSTが行われます。
退勤時間ラベル:
 すでに退勤処理が行われている場合に、退勤時間が表示されます。
退勤チェックボックス:
 退勤時にチェックを行います。チェックを行うとHTML上で位置情報(緯度・経度)を取得して退勤用HTTP POSTが行われます。

タイムカードEX モバイル用Webアプリ のHTTP通信を解析

正規の「Xamarin Forms + Web」の開発においては、Web側にXamarin Formsクライアントが欲する「Web API」を実装するのが通常です。
ただし、今回は「勝手にハック」での実装の為、現行のWebアプリの HTTP GET/POST をXamarin側のバックエンド処理としてシミュレートします。
そこで、まずはFiddlerを使用して現行の「タイムカードEX モバイル用Webアプリ」のHTTP通信を解析することにします。

ログイン画面表示~ログイン処理(/LoginPage.aspx)

ログイン画面の表示は「/LoginPage.aspx」への HTTP GET で行われます。

f:id:daigo-knowlbo:20161130001246p:plain

続いてユーザーID・パスワードを入力の上、「ログイン」ボタンをクリックすると HTTP POST が行われます。
応答は「302 found」で「TimeCard.aspx」ページへのリダイレクト要求となります。

f:id:daigo-knowlbo:20161130001308p:plain

出勤・退勤画面表示~出勤・退勤処理(/TimeCardPage.aspx)

ログイン完了後、TimeCardPage.aspxにリダイレクトします。この画面が、「本日の出勤時間・退勤時間の表示」および「出勤・退勤処理を行う」画面になります。
「出勤チェックボックス」にチェックを付けると、以下のような HTTP POST が行われます。

f:id:daigo-knowlbo:20161130001332p:plain

また、「退勤チェックボックス」にチェックを付けると、以下のような HTTP POST が行われます。

f:id:daigo-knowlbo:20161130001348p:plain

ということで・・・

既存Webアプリケーションにおける「ログインから出勤・退勤」までのHTTP通信の内容を把握することができました。
では、Xamarin側での具体的な実装に進みます。

ログインID・パスワードの設定画面

ログイン処理を行うための「ログインID・パスワード」を設定する画面を用意する事とします。
画面実装はシンプルな「ログインID」「パスワード」の2つの入力領域、「OK」「キャンセル」の各ボタンを配しただけのものとなります。

f:id:daigo-knowlbo:20161130022001p:plain

実装は以下の通りです。

// AccountSetting.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="TimeCardExApp.AccountSetting"
    Padding="0,20,0,0">
  <ContentPage.Content>
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="40"/>
        <RowDefinition Height="40"/>
        <RowDefinition Height="40"/>
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
        <ColumnDefinition />
        <ColumnDefinition />
      </Grid.ColumnDefinitions>

      <Label Grid.Row="0" Grid.Column="0" Text="ログインID" />
      <Entry Grid.Row="0" Grid.Column="1" x:Name="LoginId" />

      <Label Grid.Row="1" Grid.Column="0" Text="パスワード" />
      <Entry Grid.Row="1" Grid.Column="1" IsPassword="true" x:Name="Password" />

      <Button Grid.Row="2" Grid.Column="0"  Text="OK" Clicked="OkButton_Clicked" />
      <Button Grid.Row="2" Grid.Column="1"  Text="Cancel" Clicked="CancelButton_Clicked" />
    </Grid>
  </ContentPage.Content>
</ContentPage>
// AccountSetting.xaml.cs
using System;
using System.Collections.Generic;

using Xamarin.Forms;

namespace TimeCardExApp
{
  public partial class AccountSetting : ContentPage
  {
    public AccountSetting()
    {
      InitializeComponent();

      if (Application.Current.Properties.ContainsKey("LoginId"))
        this.LoginId.Text = Application.Current.Properties["LoginId"].ToString();
      if (Forms.Application.Current.Properties.ContainsKey("Password"))
        this.LoginId.Text = Forms.Application.Current.Properties["Password"].ToString();
    }

    public void OkButton_Clicked(object sender, EventArgs e)
    {
      Forms.Application.Current.Properties["LoginId"] = this.LoginId.Text;
      Forms.Application.Current.Properties["Password"] = this.Password.Text;

      this.Navigation.PopModalAsync();
    }

    public void CancelButton_Clicked(object sender, EventArgs e)
    {
      this.Navigation.PopModalAsync();
    }
  }
}

コンストラクタでは、Application.Current.Propertiesを通じて LoginId / Password を取得しています。また、OKボタンクリック時には、同様にApplication.Current.Propertiesを通じて LoginId / Password を設定しています。
ApplicationクラスはXamarin.Forms名前空間に実装されたクラスであり、「Current」が現在のアプリケーションを指します。PropertiesプロパティにKey/Value形式で値を設定することで、スマホ側に設定データをシリアライズしておくことが可能です。
このストレージの仕組みはXamarin Forms側で組み込みで用意されている機能となります。アプリケーション終了後も維持される保存領域となります。

ログイン処理(認証Cookieの取得)

タイムカードEX モバイルWebアプリでは、ユーザーIDとパスワードによる「ログイン認証」を行っています。
モバイルWebアプリの「ログイン」ボタンクリック時の HTTP POST をXamarin側でシミュレートする事とします。
注意点として、一般的なWebアプリケーションと同様にタイムカードEXでは「Cookieベースのセッション管理」を行っています。
つまり、Cookieを伴った HTTP GET/POST をシミュレートする必要があります。
もう1点、調査する中で分かった事として、タイムカードEXは ASP.NET WebFormの Event Validation が有効であることが分かりました。
つまり、ログイン HTTP POSTの際に、直前で取得したLoginPage.aspxレスポンスHTMLから「VIEWSTATE / EVENTVALIDATION ...」等の一連のhiddennフィールドの値を一式合わせて HTTP POST する必要があります。

以上の事から、ログイン処理の為に、以下の処理を行う事としました。

  • LoginPage.aspxに対してCookieを有効にしたHTTP通信を行う
    → HttpClientHandler / HttpClient / CookieContainer(を利用する事で実装
  • LoginPage.aspxを HTTP GET した結果に対して、特定要素をスクレイピングする
    →XDocumentクラスを用いてHTMLをスクレイピングする。
  • 上記処理で得られた情報及びユーザー設定情報を元に LoginPage.asp への HTTP POSTを行う

結果として、以下のような実装となります(本実装を含め HTTP通信や位置情報関連の各種実装は TimeCardExOperatorクラス にまとめて実装する前提とします)。

// TimeCardExOperator.csの一部
public class TimeCardExOperator
{
  /// <summary>
  /// 認証クッキー
  /// </summary>
  private CookieContainer _authCookie = null;

  // ...省略

  public void Login(string loginId, string password)
  {
    // TimeCardExOperator.siteUrlは定数でサイトURLルートを定義しています。
    var uri = new Uri(TimeCardExOperator.siteUrl + "/LoginPage.aspx");
    this._authCookie = new CookieContainer();
   
    using (var handler = new HttpClientHandler()
    {
      CookieContainer = this._authCookie
    })
    using (var client = new HttpClient(handler)
    {
      BaseAddress = uri
    })
    {
      // LoginPage.aspxへGET
      var getResult = client.GetAsync("").Result;
      var getResHtml = getResult.Content.ReadAsStringAsync().Result;
  
      // XDocumentでスクレイピング
      var xDoc = System.Xml.Linq.XDocument.Parse(getResHtml);
      XNamespace ns = "http://www.w3.org/1999/xhtml";
      //  inputタグ要素のうち name属性値が「__VIEWSTATE / __VIEWSTATEGENERATOR / __EVENTVALIDATION」の要素のvalue値を取得
      string viewState = xDoc.Descendants(ns + "input").Where(d => d.Attribute("name").Value == "__VIEWSTATE").FirstOrDefault().Attribute("value").Value;
      string viewStateGenerator = xDoc.Descendants(ns + "input").Where(d => d.Attribute("name").Value == "__VIEWSTATEGENERATOR").FirstOrDefault().Attribute("value").Value;
      string eventValidation = xDoc.Descendants(ns + "input").Where(d => d.Attribute("name").Value == "__EVENTVALIDATION").FirstOrDefault().Attribute("value").Value;

      // LoginPage.aspxへPOST
      var content = new FormUrlEncodedContent(
        new Dictionary<string, string>
        {
          { "__LASTFOCUS", "" },
          { "__EVENTTARGET", "" },
          { "__EVENTARGUMENT", "" },
          { "__VIEWSTATE", viewState },
          { "__VIEWSTATEGENERATOR",  viewStateGenerator },
          { "__SCROLLPOSITIONX", "0" },
          { "__SCROLLPOSITIONY", "0" },
          { "__EVENTVALIDATION", eventValidation },
          { "textBox_UserName", loginId },
          { "textBox_Password", password },
          { "imageButton_Login.x", "0" },
          { "imageButton_Login.y", "0" },
          { "textBox_Lat", "" },
          { "textBox_Lng", "" },
        });

      var postResult = client.PostAsync("", content).Result;
      var postResHtml = postResult.Content.ReadAsStringAsync().Result;
    }
  }
}

cookieに関して「this._authCookie = new CookieContainer();」としていますが、クラスプロパティに値を保持し、後で出勤・退勤処理の際にも使いまわすことを想定しています。

現在の出勤・退勤状況の取得

ログイン後「TimeCardPage.aspx」ページを HTTP GET することで、本日の出勤時間・退勤時間情報を取得することができます。
TimeCardPage.aspxページは、仕様として以下のHTMLを出力します。

  • 既に出勤している場合
    その出勤時間を spanタグで id属性値が label_StartTime_toWork という要素として表示
  • 既に退勤している場合
    その退勤時間を spanタグで id属性値が label_EndTime_toWork という要素として表示

上記に従いXDocumentでスクレイピングを行い「現在の出勤時間・退勤時間状況」を取得します。

public class TimeCardExOperator
{

  // ...省略

  // TimeCardExOperator.csの一部
  public void GetCurrentStatus(out string startTimeToWork, out string endTimeToWork)
  {
    // TimeCardExOperator.siteUrlは定数でサイトURLルートを定義しています。
    var uri = new Uri(TimeCardExOperator.siteUrl + "/TimeCardPage.aspx");

    using (var handler = new HttpClientHandler()
    {
      CookieContainer = this._authCookie
    })
    using (var client = new HttpClient(handler)
    {
      BaseAddress = uri
    })
    {
      // TimeCardPage.aspxへGET
      var getResult = client.GetAsync("").Result;
      var getResHtml = getResult.Content.ReadAsStringAsync().Result;

      // XDocumentでスクレイピング
      var xDoc = System.Xml.Linq.XDocument.Parse(getResHtml);
      XNamespace ns = "http://www.w3.org/1999/xhtml";
      //  spanタグ要素のうち name属性値が「label_StartTime_toWork / label_EndTime_toWork」の要素の値を取得
      startTimeToWork = xDoc.Descendants(ns + "span").Where(d => d.Attribute("id") != null && d.Attribute("id").Value == "label_StartTime_toWork").FirstOrDefault().Value;
      endTimeToWork = xDoc.Descendants(ns + "span").Where(d => d.Attribute("id") != null && d.Attribute("id").Value == "label_EndTime_toWork").FirstOrDefault().Value;
    }
  }
}

位置情報の取得

出勤・退勤処理の際には、「何処で出勤・退勤処理を行ったか」を保持する為、緯度経度の位置情報をクライアントからサーバーに対して通知(HTTP POST)します。
スマホネイティブ機能による位置情報取得を行うため、「Geolocator Plugin for Xamarin and Windows」を利用します。
これにより、iOS / android共通の実装コードを利用することが可能になります。
Visual Studio for Macなどを使用している場合、NuGetで「Xam.Plugin.xxxx」を参照することになります。

具体的な実装は以下になります。

// TimeCardExOperator.csの一部

// ...省略
using Plugin.Geolocator;
using Plugin.Geolocator.Abstractions;

public class TimeCardExOperator
{
  // ...省略
  
  /// <summary>
  /// 現在位置(経度)
  /// </summary>
  private double currentLongitude = -1;

  /// <summary>
  /// 現在位置(緯度)
  /// </summary>
  private double currentLatitude = -1;

  // ...省略

  /// <summary>
  /// 位置情報取得を開始します
  /// </summary>
  public void StartGps()
  {
    IGeolocator locator = CrossGeolocator.Current;

    if (!locator.IsListening)
    {
      locator.PositionChanged += (object sender, PositionEventArgs posArgs) =>
      {
        this.currentLongitude = posArgs.Position.Longitude;
        this.currentLatitude = posArgs.Position.Latitude;
      };
      locator.StartListeningAsync(60, 5, false).Wait();
    }
  }

  /// <summary>
  /// 位置情報取得を停止します
  /// </summary>
  public void StopGps()
  {
    IGeolocator locator = CrossGeolocator.Current;

    locator.StopListeningAsync();
  }
}

StartGps()メソッドでGPSの補足を開始します。位置情報変更イベント時(PositionChanged)には、クラスプロパティ「currentLongitude / currentLatitude」にその値を反映させます。
StopGps()メソッドでGPSの補足を終了します。
本記事内説明では触れませんが、アプリケーションライフサイクル(「アプリの起動時(Appコンストラクタ)」「スリープ時(App.OnSleep())」「レジューム時(App.OnResume())」)の中で、Start() / Stop()の制御を行います。

出勤・退勤処理の実装

出勤ボタン・退勤ボタンがクリックされた際の実装を行います。
事前のHTTP解析により、以下の HTTP POST 処理を行う事が分かっています。

【出勤時のHTTP POST】 f:id:daigo-knowlbo:20161130013607p:plain

【退勤時のHTTP POST】 f:id:daigo-knowlbo:20161130013736p:plain

出勤・退勤処理時の HTTP POST は、一部を除いて重複しますので、共通メソッド「StartEndToWork(WorkOperationType workOperationType)」というものを用意します。
また、ログイン処理と同様に、HTTP POST前に「TimeCardPage.aspx」に対する HTTP GET & 応答HTMLのスクレイピングを行い、__VIEWSTATE等の必要hiddenフィールド値を取得します。スクレイピングで得られたこれらの値は、続いて行う HTTP POST で利用します。
実装は以下の通りです。

// TimeCardExOperator.csの一部
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Xml.Linq;
using System.Linq;
// ...省略

public class TimeCardExOperator
{
  // ...省略
  
  /// <summary>
  /// 出勤をキックします
  /// </summary>
  public void StartToWork()
  {
    this.StartEndToWork(WorkOperationType.Start);
  }
  
  /// <summary>
  /// 退勤をキックします
  /// </summary>
  public void EndToWork()
  {
    this.StartEndToWork(WorkOperationType.End);
  }
  
  /// <summary>
  /// 出勤もしくは退勤のHTTP POSTを実行します
  /// </summary>
  /// <param name="workOperationType">
  /// 出勤もしくは退勤の操作種別
  /// </param>
  private void StartEndToWork(WorkOperationType workOperationType)
  {
    // TimeCardExOperator.siteUrlは定数でサイトURLルートを定義しています。
    var uri = new Uri(TimeCardExOperator.siteUrl + "/TimeCardPage.aspx");
  
    using (var handler = new HttpClientHandler()
    { // this._authCookieにはログイン認証を経たCookieが保持されています。
      CookieContainer = this._authCookie
    })
    using (var client = new HttpClient(handler)
    {
      BaseAddress = uri
    })
    {
      // TimeCardPage.aspxへGET
      var getResult = client.GetAsync("").Result;
      var getResHtml = getResult.Content.ReadAsStringAsync().Result;
  
      // XDocumentでスクレイピング
      //  inputタグ要素のうち name属性値が「__VIEWSTATE / __VIEWSTATEGENERATOR / __EVENTVALIDATION」の要素のvalue値を取得
      var xDoc = System.Xml.Linq.XDocument.Parse(getResHtml);
      XNamespace ns = "http://www.w3.org/1999/xhtml";
      string viewState = xDoc.Descendants(ns + "input").Where(d => d.Attribute("name").Value == "__VIEWSTATE").FirstOrDefault().Attribute("value").Value;
      string viewStateGenerator = xDoc.Descendants(ns + "input").Where(d => d.Attribute("name").Value == "__VIEWSTATEGENERATOR").FirstOrDefault().Attribute("value").Value;
      string eventValidation = xDoc.Descendants(ns + "input").Where(d => d.Attribute("name").Value == "__EVENTVALIDATION").FirstOrDefault().Attribute("value").Value;
  
      // TimeCardPage.aspxへPOST
      var postParams = new Dictionary<string, string>
        {
        { "textBox_Lat", this.currentLatitude.ToString() },
        { "textBox_Lng", this.currentLongitude.ToString() },
        { "__EVENTARGUMENT", ""},
        {"__VIEWSTATE", viewState },
        { "__VIEWSTATEGENERATOR",  viewStateGenerator },
        { "__EVENTVALIDATION", eventValidation },
        {"__SCROLLPOSITIONX", "0" },
        {"__SCROLLPOSITIONY", "0" },
        {"__LASTFOCUS", "" },
      };
      if (workOperationType == WorkOperationType.Start)
      {
        postParams.Add("__EVENTTARGET", "checkBox_IsStart_toWork");
        postParams.Add("checkBox_IsStart_toWork", "on");
      }
      else
      {
        postParams.Add("__EVENTTARGET", "checkBox_IsEnd_toWork");
        postParams.Add("checkBox_IsEnd_toWork", "on");
      }
      var content = new FormUrlEncodedContent(postParams);
  
      var result = client.PostAsync("", content).Result;
      var text = result.Content.ReadAsStringAsync().Result;
    }
  }
}

メインフォームの実装

上述で、主なロジックである「TimeCardExOperatorクラス」の実装説明を行なってきました。
これを使用する画面部分の実装は以下のようになります。

// TimeCardExAppPage.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:local="clr-namespace:TimeCardExApp" 
    x:Class="TimeCardExApp.TimeCardExAppPage"
    Padding="0,20,0,0">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="90" />
      <RowDefinition Height="40" />
      <RowDefinition Height="40" />
      <RowDefinition Height="40" />
      <RowDefinition Height="40" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition />
      <ColumnDefinition />
    </Grid.ColumnDefinitions>

    <Image Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" x:Name="logo" />
    <Button Grid.Row="1" Grid.Column="0" Text="出勤" Clicked="StartToWorkButton_Clicked"/>
    <Label Grid.Row="1" Grid.Column="1" x:Name="StartTimeToWorkLabel" VerticalTextAlignment="Center" Text="" />
    <Button Grid.Row="2" Grid.Column="0" Text="退勤" Clicked="EndToWorkButton_Clicked"/>
    <Label Grid.Row="2" Grid.Column="1" x:Name="EndTimeToWorkLabel" VerticalTextAlignment="Center" Text="" />
    <Button Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" Text="refresh" Clicked="RefreshButton_Clicked"/>
    <Button Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" Text="ログイン設定" Clicked="SettingsButton_Clicked"/>
  </Grid>
</ContentPage>
// TimeCardExAppPage.xaml.cs
using System;
using Xamarin.Forms;

using TimeCardExLib;

namespace TimeCardExApp
{
  public partial class TimeCardExAppPage : ContentPage
  {
    private TimeCardExOperator _timeCardExOperator;

    public string LoginId
    {
      get
      {
        return (string)Xamarin.Forms.Application.Current.Properties["LoginId"];
      }
    }

    public string Password
    {
      get
      {
        return (string)Xamarin.Forms.Application.Current.Properties["Password"];
      }
    }

    public TimeCardExAppPage()
    {
      InitializeComponent();
      InitializeLogo();
    }

    public void SetTimeCardExOperator(TimeCardExOperator timeCardExOperator)
    {
      this._timeCardExOperator = timeCardExOperator;

      if (!Xamarin.Forms.Application.Current.Properties.ContainsKey("LoginId"))
      {
        this.Navigation.PushModalAsync(new AccountSetting()).Wait();
      }
      else
      {
        InitializeCurrentStatus();
      }
}

    public async void SettingsButton_Clicked(object sender, EventArgs e)
    {
      await this.Navigation.PushModalAsync(new AccountSetting());
      this._timeCardExOperator.Login(this.LoginId, this.Password);
      this.RefreshButton_Clicked(this, null);
    }

    public void RefreshButton_Clicked(object sender, EventArgs e)
    {
      string startTimeToWork, endTimeToWork;
      this._timeCardExOperator.Login(this.LoginId, this.Password);
      this._timeCardExOperator.GetCurrentStatus(out startTimeToWork, out endTimeToWork);
      this.StartTimeToWorkLabel.Text = startTimeToWork;
      this.EndTimeToWorkLabel.Text = endTimeToWork;
    }

    public async void StartToWorkButton_Clicked(object sender, EventArgs e)
    {
      this._timeCardExOperator.Login(this.LoginId, this.Password);
      this._timeCardExOperator.StartToWork();
      this.InitializeCurrentStatus();

      await DisplayAlert("タイムカードEX", "出勤記録を行いました。", "閉じる" );
    }
 
    public async void EndToWorkButton_Clicked(object sender, EventArgs e)
    {
      this._timeCardExOperator.Login(this.LoginId, this.Password);
      this._timeCardExOperator.EndToWork();
      this.InitializeCurrentStatus();

      await DisplayAlert("タイムカードEX", "退勤記録を行いました。", "閉じる");
    }

    private void InitializeCurrentStatus()
    {
      string startTimeToWork, endTimeToWork;

      this._timeCardExOperator.Login(this.LoginId, this.Password);
      this._timeCardExOperator.GetCurrentStatus(out startTimeToWork, out endTimeToWork);

      this.StartTimeToWorkLabel.Text = startTimeToWork;
      this.EndTimeToWorkLabel.Text = endTimeToWork;
    }

    private void InitializeLogo()
    {
      this.logo.WidthRequest = 322;
      this.logo.HeightRequest = 90;
      this.logo.Source = ImageSource.FromResource("TimeCardExApp.Resources.logo.png");
    }
  }
}

最終的なメイン画面は以下のような感じです。

f:id:daigo-knowlbo:20161130020743p:plain

また、App.xaml.csで初期化処理及びスリープ・レジューム時の処理を記述しています。

//App.xaml.cs
using Xamarin.Forms;

using TimeCardExLib;

namespace TimeCardExApp
{
  public partial class App : Application
  {
    private TimeCardExOperator _timeCardExOperator;

    public App()
    {
      InitializeComponent();

      TimeCardExAppPage timeCardExAppPage = new TimeCardExAppPage();

      this._timeCardExOperator = new TimeCardExOperator();
      this._timeCardExOperator.StartGps();
      timeCardExAppPage.SetTimeCardExOperator(_timeCardExOperator);

      MainPage = timeCardExAppPage;
    }

    protected override void OnStart()
    {
      // Handle when your app starts
    }

    protected override void OnSleep()
    {
      this._timeCardExOperator.StopGps();
    }

    protected override void OnResume()
    {
      this._timeCardExOperator.StartGps();
    }

    // iOS対策
    public void EnterBackground() => OnSleep();
    public void EnterForeground() => OnResume();
  }
}

それから、iOS版で上手いこと Sleep / Resume のイベントが上がるように AppDelegate.cs にも手を入れています。

// AppDelegate.cs
using System;
using System.Collections.Generic;
using System.Linq;

using Foundation;
using UIKit;

namespace TimeCardExApp.iOS
{
  [Register("AppDelegate")]
  public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
  {
    private App application;

    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    {
      global::Xamarin.Forms.Forms.Init();

      application = new App();
      LoadApplication(application);

      return base.FinishedLaunching(app, options);
    }

    public override void OnActivated(UIApplication uiApplication)
    {
    }
    public override void DidEnterBackground(UIApplication uiApplication)
    {
      base.DidEnterBackground(uiApplication);
      application.EnterBackground();
    }

    public override void WillEnterForeground(UIApplication uiApplication)
    {
      base.WillEnterForeground(uiApplication);
      application.EnterForeground();

    }
  }
}

ライフサイクルイベント辺りの処理は、以下の ゆ〜か さんの記事を参考にさせていただきました。

tamafuyou.hatenablog.com

まとめ

全体的に煩雑な記事になってしまいましたが、Webアプリケーションを無理やりハックしてXamarin Formsアプリ化して遊んでみたお話をつらつらと書かせていただきました。
プロジェクト一式のソースを見ないと、説明的には省略している部分が多々ありますので、追ってGithubにソース一式をあげようかな・・・検討中・・・
ということで、冒頭にもあげさせていただいた以下の技術トピックについては参考になるのではないかと思います。

  • ログイン情報設定用のモーダルフォームを表示する
    → Navigation.PushModalAsync()を使用

  • ログイン情報をアプリケーション領域に保存・読み込みする
    → Xamarin.Forms.Application.Current.Properties を使用

  • WebアプリケーションのHTTP GET / POST 処理を、Xamarin Fromsアプリでバックエンドに実装する
    → HttpClient / HttpClientHandler / CookieContainerを使用

  • レスポンスで得られたHTML内容は XDocument でスクレイピングして必要な情報を抜き出す
    → XDocumentを使用

  • 出勤・退勤処理時に使用する位置情報をスマホ機能により取得する
    → Geolocator Plugin for Xamarin and Windows を使用