前回はMVVMを使ってみました。

Problem

Xamlを使ってみたので、色々簡単にできると思います。
なので、Timerを使ってストップウォッチを作ってみました。

ソースは https://github.com/takuya-takeuchi/Demo/tree/master/Xamarin/02_Xamarin.Forms.Portable2です。

この手の話題はエクセルソフトの田淵様JXUGC #9 Xamarin.Forms Mvvm 実装方法 Teachathon を開催しましたで実施済みですが、あえて自分でやってみます。

課題はTimerをどうするか、ですが普通にXamarinのページにXamarin.Forms.Device.StartTimerがありました。
けど、WinFormsやWPFでお世話になったSystem.Timers.Timerとかと違います。
インスタンスを作ってそこで管理するのではなく、Staticメソッドにコールバックを渡して、指定した間隔で処理をするようです。

タイマーを停止する方法は

While the callback returns true, the timer will keep recurring.

訳:コールバックがtrueを返す間は、タイマーは繰り返し発生し続けます。

とあるので、第二引数のコールバックで不要になったらfalseを返せば良いだけのようです。

Resolution

Timerさえわかれば、あとはちょいちょい、って感じかと思ったそうでもありませんでした。
Xamlで作れるし、WPF、UWPとクラスが似ているので直感的に作れますが、細かいところが違います。
まず、WidthやHeightがReadOnlyなので、要素をおおまかなおおきさにして配置するのが面倒です。
あと、Xamlのインテリセンスが少し残念です。プロパティ要素構文でインテリセンスの推測が効かないので、最初は焦りました。
最もきついのは、Xaml構文でエラーになってもビルドが通ることです。
最初、間違えてResoueceプロパティの子要素に、下記のようにリソース要素を追加していました。

1
2
3
<ContentPage.Resources>
<converters:BooleanInvertConverter x:Key="BooleanInvertConverter" />
</ContentPage.Resources>

でも、これでビルドが通って、長い時間かけて起動したiPhone Simulatorで実行時エラー、となると悲しいです。
正しくは当然下記のよう。

1
2
3
4
5
<ContentPage.Resources>
<ResourceDictionary>
<converters:BooleanInvertConverter x:Key="BooleanInvertConverter" />
</ResourceDictionary>
</ContentPage.Resources>

これは改善して欲しいです。
辛いのはこれくらいですが、あとは普通です。WPFやUWPがきちんと使えればどうとでもなります。
VisiblityプロパティがIsVisibleでbool型なのはWinFormsを思い出して、(´ω`)ホッコリしました。

最終的に、

MainPage.xaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage x:Class="Xamarin.Forms.Portable2.Views.MainPage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:converters="clr-namespace:Xamarin.Forms.Portable2.Converters;assembly=Xamarin.Forms.Portable2"
xmlns:viewModels="clr-namespace:Xamarin.Forms.Portable2.ViewModels;assembly=Xamarin.Forms.Portable2">
<ContentPage.BindingContext>
<viewModels:MainPageViewModel />
</ContentPage.BindingContext>

<ContentPage.Resources>
<ResourceDictionary>
<converters:BooleanInvertConverter x:Key="BooleanInvertConverter" />
<Style x:Key="LabelStyle" TargetType="Label">
<Setter Property="FontSize" Value="48" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="VerticalOptions" Value="Center" />
</Style>
</ResourceDictionary>
</ContentPage.Resources>

<Grid VerticalOptions="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Label Grid.Row="0"
Grid.Column="1"
Style="{StaticResource LabelStyle}"
Text="{Binding Minute}" />
<Label Grid.Row="0"
Grid.Column="2"
Style="{StaticResource LabelStyle}"
Text=":" />
<Label Grid.Row="0"
Grid.Column="3"
Style="{StaticResource LabelStyle}"
Text="{Binding Second}" />
<Label Grid.Row="0"
Grid.Column="4"
Style="{StaticResource LabelStyle}"
Text="." />
<Label Grid.Row="0"
Grid.Column="5"
Style="{StaticResource LabelStyle}"
Text="{Binding Millisecond}" />
<Button Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="5"
Command="{Binding StartCommand}"
HorizontalOptions="Center"
IsVisible="{Binding IsRunning,
Converter={StaticResource BooleanInvertConverter}}"
Text="Start"
VerticalOptions="Center" />
<Button Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="5"
Command="{Binding StopCommand}"
HorizontalOptions="Center"
IsVisible="{Binding IsRunning}"
Text="Stop"
VerticalOptions="Center" />
</Grid>
</ContentPage>

MainPageViewModel.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
using System;
using System.Windows.Input;
using Microsoft.Practices.Prism.Mvvm;

namespace Xamarin.Forms.Portable2.ViewModels
{
public sealed class MainPageViewModel : BindableBase
{

public MainPageViewModel()
{
this.IsRunning = false;
this.TimeSpan = new TimeSpan(0, 0, 0, 0, 0);
}

private TimeSpan _TimeSpan;

public TimeSpan TimeSpan
{
get
{
return this._TimeSpan;
}
private set
{

this.Minute = this.TimeSpan.Minutes.ToString("00");
this.Second = this.TimeSpan.Seconds.ToString("00");

var ms = this.TimeSpan.Milliseconds / 10;
this.Millisecond = ms.ToString("00");
this.SetProperty(ref this._TimeSpan, value);
}
}

private string _Minute;

public string Minute
{
get
{
return this._Minute;
}
private set
{
this.SetProperty(ref this._Minute, value);
}
}

private string _Second;

public string Second
{
get
{
return this._Second;
}
private set
{
this.SetProperty(ref this._Second, value);
}
}

private string _Millisecond;

public string Millisecond
{
get
{
return this._Millisecond;
}
private set
{
this.SetProperty(ref this._Millisecond, value);
}
}

private bool _IsRunning = false;

public bool IsRunning
{
get
{
return this._IsRunning;
}
private set
{
this.SetProperty(ref this._IsRunning, value);
}
}

private ICommand _StartCommand;

public ICommand StartCommand
{
get
{
if (this._StartCommand == null)
{
this._StartCommand = new Command(() =>
{
this.IsRunning = true;

Device.StartTimer(new TimeSpan(0, 0, 0, 0, 10),
() =>
{
this.TimeSpan = this.TimeSpan.Add(new TimeSpan(0, 0, 0, 0, 10));
return this.IsRunning;
});
});
}

return this._StartCommand;
}
}

private ICommand _StopCommand;

public ICommand StopCommand
{
get
{
if (this._StopCommand == null)
{
this._StopCommand = new Command(() =>
{
this.IsRunning = false;
});
}

return this._StopCommand;
}
}

}

}

になりました。
BooleanInvertConverterとかは省略します。簡単すぎますので。
出来上がった結果がこれ。

どこかでみたようなストップウォッチ

シンプルなストップウォッチ…

Startを押せば、Stopボタンに変わり、経過時間が変化します。
が、このストップウォッチ問題があって、非常に精度が悪いです。10ms間隔がまずいのか非常に遅い。
ひょっとしたらTimerは別のAPIがあるのかもしれません。
ネイティブのAPIを直接呼ぶ方がよいか?

Conclusion

実用度と精度はともかくアプリと呼べるアプリを作ってみました。
XAMLでの開発経験があるならこれほど取っつきやすいものはないと思います。

Source Code

https://github.com/takuya-takeuchi/Demo/tree/master/Xamarin/02_Xamarin.Forms.Portable2