Xamarin.FormsでもF#
序文
こちらはF# Advent Calender 2015の23日目の記事です。
JXUGでXamarin.FormsでStopWatchアプリを作ってMVVMを説明する会(JXUGC #9 Xamarin.Forms Mvvm 実装方法 Teachathon)がありまして、先生側として参加したのですが、せっかくなのでF#を使ってサンプルアプリを作ってみました。
コードはこちらになります。注意点としてStopwatchMVVMAppプロジェクトでFSharpCoreを参照しているのですが、Nugetを使わず絶対パスで記述してるため環境によってはビルドが通らないかもしれません…そのうちNugetに変えたいと思います(;´∀`)
仕様
・ストップ状態でStartボタンを押すと計測を開始。
・経過時刻を秒単位もしくはミリセク単位で表示。切り替えはスイッチで行う。
・Lapボタンを押すとラップをリストビューに追加。ラップの値もスイッチの切り替えで単位を切り替え。
・Stopを押すと最後のラップを追加するとともに計測を終了。ラップの最大値・最小値・平均値などをアラートで表示。
・スタート/ストップをするボタンは状態に合わせてStart->Stop->Restart(以下Stop-Restartを繰り返し)となるように。
Model
せっかくF#で書くのでModel側をActorで非同期な状態遷移マシン風に書いてみました。
ストップ状態でMsgStart_Stopを受け取るとスタート状態に、スタート状態でMsgLapを受け取るとその時のLapを記録しつつスタート状態を継続、MsgStart_Stopを受け取ると最後のラップを記録しつつラップの最大値や最小値などを計測してそれを投げながらストップ状態へ遷移します。
その過程で後述するSubjectやObservableCollectionに値を設定することでVM側に通知がおこなわれます。
普通に書いた方が短くなるだろうけど、いつかActorで書いた方がすっきりする時が来るのでいいのです…
let newModel (rSrcSub:Subject<_>)= //modelの値 let tSub=reRaiseSub rSrcSub 0L let statSub=sub MSNone let lObCol=ObservableCollection<_>() let toLapV rSrcSub t={No=lObCol.Count+1;TimeSub=reRaiseSub rSrcSub t} //状態遷移 let actor=Actor<_>.Start(fun mbox-> //start状態 let rec startLoop (sw:Stopwatch) lastEMSec= lObCol.Clear()//前の残りあれば消す let timerD=spawnTimer 16<|fun ()->tSub.Value<-sw.ElapsedMilliseconds let rec loop lastEMSec= async{ let! msg=mbox.Receive() match msg with |MsgStart_Stop-> //###stopの処理 timerD.Dispose()//ほんとは最後に送った値を見てとかしないとだけど手抜き let eMSec=sw.ElapsedMilliseconds onCtx<|fun()-> lObCol.Add<|toLapV rSrcSub (eMSec-lastEMSec) let r=lObCol|>Seq.map(lapTime>>double)|>toResult statSub.Value<-MSStopped r //stop状態へ return! stopLoop() |MsgLap-> //###Lap追加してstart状態続行 let eMSec=sw.ElapsedMilliseconds onCtx<|fun()->lObCol.Add<|toLapV rSrcSub (eMSec-lastEMSec) return! loop eMSec } loop lastEMSec //stop状態 and stopLoop()= async{ let! msg=mbox.Receive() match msg with |MsgStart_Stop-> //###start状態へ onCtx<|fun()->statSub.Value<-MSStarted let sw=Stopwatch.StartNew() return! startLoop sw 0L |v-> Log.logErr<|sprintf"unknown msg=%A" v return! stopLoop() } stopLoop() ) actor,tSub,statSub,lObCol
ViewModel
ViewModel側はIPropertyChangedやIObservableを実装するSubjectというtypeを使うことでViewModelを実装するクラス自体はINotifyPropertyChangedは実装してません。SubjectのValueに値を設定することでPropertyChangedイベントが発生したり、IObservableのOnNextが呼ばれてReactive的につながっていきます。
なぜReactivePropertyを使わないかというのは、前XamarinのF#のPCL側でRxがうまくビルド通らずそのころに作ってそのまま今に至るという経緯なので、今はもう余計なことをしなくてもいいかもしれません…
Commandは関数書いて記述が楽になるようにしてるぐらいでたいしたことはしてません。
type SWatchVM()= //値再送出用のsub let reRaiseSrcSub=sub false //model let actor,tSub,statSub,lObCol=newModel reRaiseSrcSub //ui related let vEvt=Event<_>()//messengerがわり let startStopCmd=toEverCmd<|fun _->actor.Post MsgStart_Stop let lapCmd,lapCmdCanE=toCmd (fun _->statSub.Value=MSStarted) (fun _->actor.Post MsgLap) (*stateによりlapボタンの状態変更。 StoppedだったらResult表示*) let _=statSub.AsObservable() |>Observable.subscribe(fun stat-> lapCmdCanE() match stat with |MSStopped v->vEvt.Trigger v |_->() ) //stateによりボタンの表示変更 let btnSSub,_=statSub.AsObservable() |>Observable.map toBtnS |>strongObToSub [<CLIEvent>] member x.ViewEvt=vEvt.Publish member x.BtnSSub=btnSSub member x.LapCmd=lapCmd member x.StartStopCmd=startStopCmd member x.TSub=tSub member x.LObCol=lObCol member x.ReRaiseSrcSub=reRaiseSrcSub
View
View側はおおむねXAML側でBindingしてるだけです。
仕様のスイッチでフォーマット桁数が変わるはスイッチにVMのbool値のSubjectをBindingするとともに、その値をValueConverterで見れるようにしています。
スイッチを切り替えるとそれを受けてVM側で値が再発火されるのでValueConverterが呼ばれ、その時にフォーマットをどちらで行うかを切り替えています。
本当はVMの値をValueConverterのコンストラクタの引数で渡したかったのですが、XAMLでの書き方がわからなかったのでStaticとして渡すという残念なやり方で逃げています…Codeビハインドで書くならいけそうです。
あとVMからAlertを表示するのはメッセンジャーを使わずVMのイベントをView側のコードビハインドで受けて表示という形にしています。メッセンジャーはイベントハブとして使うにはいいんですが、VM->Viewの連絡手段として使うのは個人的に嫌いというだけなのでそこらへんはお好みで。
<?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:StopwatchMVVMApp;assembly=StopwatchMVVMApp" x:Class="StopwatchMVVMApp.ViewPage"> <ContentPage.Resources> <ResourceDictionary> <local:TToTimeSVConverter x:Key="TTSVConverter"/> </ResourceDictionary> </ContentPage.Resources> <StackLayout> <Label Text="{Binding TSub.Value,Converter={StaticResource TTSVConverter}}" HorizontalOptions="Center" FontSize="30"/> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Button Text="{Binding BtnSSub.Value}" Command="{Binding StartStopCmd}"/> <Button Text="Lap" Grid.Column="1" Command="{Binding LapCmd}"/> </Grid> <Grid> <Label Text="switch" HorizontalOptions="Start"/> <Switch IsToggled="{Binding ReRaiseSrcSub.Value,Mode=TwoWay}" HorizontalOptions="End"/> </Grid> <ListView ItemsSource="{Binding LObCol}"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <Grid> <Label HorizontalOptions="Start" Text="{Binding No}"/> <Label HorizontalOptions="End" Text="{Binding TimeSub.Value,Converter={StaticResource TTSVConverter}}}"/> </Grid> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView> </StackLayout> </ContentPage>
まとめ
XamarinでもF#は便利に使えます。ほんとはView側もPetzoldさんの言われるように全部F#でいけるのですが、IDEサポートとかプロジェクトテンプレートとかもろもろ考えるとすなおにView側はC#、VMより上はF#という書き方がいい気がします。
けどうまく書けばView側はほとんどXAMLだけですし、VM側より上はF#で書けてiOS、Androidでも共有できるのでF#の便利さを十分享受できます。
Xamarin.FormsはiOSやAndroid、はてはUWPまでをUI含めたかなりの部分をコードを共有化してプロダクト作れそうなよい感じのツールです。そこでもF#を使えることでいろいろと幸せになれる気がします。
ちなみにこの会でF#の普及をもくろみましたが、おそらく失敗しています。あいすみません…
来年は実案件でXamarin.Forms+F#を使ってみるのでうまくいったらまたどこかで報告できればと思います…(´・ω・`)