PowerShell スクリプトで気をつけたいこと その2
開発部の本橋です。先日投稿した PowerShell スクリプトで気をつけたいこと の続編です。
親スコープの変数を子スコープのブロックから書き換えたい
スクリプト言語にはよくある話ですが、親スコープに存在する変数を書き換える際に気をつける必要があります。
そもそも PowerShell のスコープには次の5種類があります。*1
- Global
- その名の通りグローバルスコープです。スクリプトファイル外からでも参照可能です。
- Local
- カレントのスコープです。子スコープからも参照可能です。
- Script
- 同一スクリプトファイルの中でのみ参照可能です。
- Private
- カレントスコープでのみ参照可能です。
- Numbered Scopes
- カレントブロックからの相対数を指定してスコープを参照します。
何も考えずに変数を定義すると Local
スコープになります。以下はうまくいかない例です。
# このスコープに変数 $a を定義 (Local スコープ) $a = 'A' # スクリプトブロックを定義して子スコープを作る $fn = { Write-Host $a # ==> A # 親スコープの変数 $a を書き換えたつもりだが、 # このスコープに変数 $a を定義 (Local スコープ) $a = 'B' Write-Host $a # ==> B } # スクリプトブロックを呼び出し & $fn # このスコープの $a は書き換わっていない Write-Host $a # ==> A
$a
をスクリプトブロックの中で書き換えたつもりでも、スクリプトブロックを抜けると $a
はもとに戻ってしまいます。コメントにも書きましたが、$fn
の Local
スコープに $a
を定義してしまっているためです。
この場合、スコープを指定して $a
に代入します。
# このスコープに変数 $a を定義 (Local スコープ) $a = 'A' # スクリプトブロックを定義して子スコープを作る $fn = { Write-Host $a # ==> A # 1つ上のスコープを指定して変数 $a を書き換える Set-Valiable -Name a -Scope 1 -Value 'B' # もしくは Script スコープの変数 $a を定義する #$script:a = 'B' Write-Host $a # ==> B } # スクリプトブロックを呼び出し & $fn # $a は書き換わっている Write-Host $a # ==> B
Set-Variable
コマンドレットで一つ上のスコープを指定して変数の値を書き換えてあげればうまくいきます。また、上記コメント中に書いたように $script:a = 'B'
とすると Script
スコープの変数に代入する形になります。意図しない上書きをしてしまう場合があるので気をつけましょう。
自社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アプリの画面は以下のようなものになります。
【ログイン画面】
ユーザーIDとパスワードを入力する、ごく一般的なWebアプリケーションのログイン画面になります。
【出勤・退勤処理画面】
出勤時間ラベル:
すでに出勤処理が行われている場合に、出勤時間が表示されます。
出勤チェックボックス:
出勤時にチェックを行います。チェックを行うと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 で行われます。
続いてユーザーID・パスワードを入力の上、「ログイン」ボタンをクリックすると HTTP POST が行われます。
応答は「302 found」で「TimeCard.aspx」ページへのリダイレクト要求となります。
出勤・退勤画面表示~出勤・退勤処理(/TimeCardPage.aspx)
ログイン完了後、TimeCardPage.aspxにリダイレクトします。この画面が、「本日の出勤時間・退勤時間の表示」および「出勤・退勤処理を行う」画面になります。
「出勤チェックボックス」にチェックを付けると、以下のような HTTP POST が行われます。
また、「退勤チェックボックス」にチェックを付けると、以下のような HTTP POST が行われます。
ということで・・・
既存Webアプリケーションにおける「ログインから出勤・退勤」までのHTTP通信の内容を把握することができました。
では、Xamarin側での具体的な実装に進みます。
ログインID・パスワードの設定画面
ログイン処理を行うための「ログインID・パスワード」を設定する画面を用意する事とします。
画面実装はシンプルな「ログインID」「パスワード」の2つの入力領域、「OK」「キャンセル」の各ボタンを配しただけのものとなります。
実装は以下の通りです。
// 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】
【退勤時のHTTP POST】
出勤・退勤処理時の 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"); } } }
最終的なメイン画面は以下のような感じです。
また、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(); } } }
ライフサイクルイベント辺りの処理は、以下の ゆ〜か さんの記事を参考にさせていただきました。
まとめ
全体的に煩雑な記事になってしまいましたが、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 を使用
PowerShell スクリプトで気をつけたい事
開発部の本橋です。 Windows でバッチ処理といえば cmd.exe ですが、cmd.exe で物足りない時に PowerShell を使う機会も増えてきました。PowerShell スクリプトを書く時に気をつけたい事をいくつか書いてみます。
推奨される Verb
PowerShell 標準のコマンドレットには 動詞-名詞
という命名規則があります。自分で function
を作る時にもこの命名規則に従っておくと他の人が使う時にもわかりやすくていいです。
さらに、動詞(Verb)の部分には推奨される単語があります。Get-Verb
コマンドレットで推奨される Verb を確認できます。
> Get-Verb Verb Group ---- ----- Add Common Clear Common Close Common Copy Common Enter Common Exit Common ...(略) # 使いたい Verb を引数に取ることもできる。 > Get-Verb Save Verb Group ---- ----- Save Data
動詞-名詞
の命名規則に従わなくても、さらには推奨される Verb を使わなくても別に良いのですが、ルールに則っておくと後々変な名前に悩まされることが少なくなります。
カレントディレクトリが2つある(ように見える)
pwd
(Get-Location
コマンドレットのエイリアス)で取得できるカレントディレクトリと、 .NET 側の API である[System.IO.Directory]::GetCurrentDirectory()
で取得できるカレントディレクトリが違います。
# カレントディレクトリは C:\Users\motohashi > pwd Path ---- C:\Users\motohashi # .NET で用意されている API を使っても同じ > [System.IO.Directory]::GetCurrentDirectory() C:\Users\motohashi # Cドライブの直下に移動するとカレントディレクトリは変わる > cd C:\ > pwd Path ---- C:\ # .NET で用意されている API を使うと変わっていない > [System.IO.Directory]::GetCurrentDirectory() C:\Users\motohashi
どうやらpwd
は現在のセッションのカレントディレクトリを返し、[System.IO.Directory]::GetCurrentDirectory()
は PowerShell プロセスのカレントディレクトリを返す、ということのようです。*1*2
これで何が困るかというと、.NET 側のクラスを使って相対パスでファイルを保存するようなケースです。
# 相対パスを指定してXMLファイルを読み込み # pwd で取得できるディレクトリからの相対パスとなる。 $xmldoc = [xml](cat 'path/to/xml') # ... xmlを変更 ... # 相対パスを指定してXMLファイルを書き込み # [System.IO.Directory]::GetCurrentDirectory() で取得できるディレクトリからの相対パスとなる。 $xmldoc.save('path/to/xml')
相対パスを使わずに絶対パスを使うようにするか、[System.IO.Directory]::SetCurrentDirectory()
で PowerShell プロセスのカレントディレクトリを変更するようにするといいですね。