rksoftware

Visual Studio とか C# とかが好きです

MessagingCenter で DisplayAlert を表示する。

MVVM モデルでは、VM は V を意識しないこと(VM は V を知らないこと)が理想とされています。
すなわち、View が行うべき処理を、ViewModel に書いたり、ViewModel から View のメソッドを呼び出すことはしない、ということです。
しかし、現実問題、ViewModel での処理中または処理結果として View が行う処理をしたい場合はあります。

・画面遷移
・ポップアップメッセージ

あたりは、どうしても View の処理を呼ばなくてはならない典型でしょう。

■ MessagingCenter

こういった場合、MessagingCenter によるメッセージによる通知が使えます。
参考

■ 実行イメージ

ボタン押すと

ポップアップメッセージ表示

「移動する」を押すと画面遷移

■ コード

ボタンを押すと、ボタンにバインドされた ViewModel の Command が実行され、その中で View へメッセージを送ります。
送るメッセージは2回で、1回目はポップアップメッセージの表示、ポップアップで「移動する」が選択された場合、2回目の画面遷移のメッセージを送ります。
App.cs (変更)

using Xamarin.Forms;

namespace MessagingCenterSample
{
    public partial class App : Application
    {
        public App ()
        {
            InitializeComponent();

            MainPage = new NavigationPage(new MainPage());
        }

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

        protected override void OnSleep ()
        {
            // Handle when your app sleeps
        }

        protected override void OnResume ()
        {
            // Handle when your app resumes
        }
    }
}

MainPage.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:MessagingCenterSample"
             xmlns:vm="clr-namespace:MessagingCenterSample.ViewModels"
             x:Class="MessagingCenterSample.MainPage"
             Appearing="MainPage_Appearing"
             Disappearing="MainPage_Disappearing"
             Title="MainPage"
             >
    <ContentPage.BindingContext>
        <vm:MainPageViewModel />
    </ContentPage.BindingContext>
    <ContentPage.Content>
        <StackLayout>
            <Button Text="次画面へ移動する" Command="{Binding GoNextCommand}"/>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

SecondPage.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:MessagingCenterSample"
             xmlns:vm="clr-namespace:MessagingCenterSample.ViewModels"
             x:Class="MessagingCenterSample.MainPage"
             Appearing="MainPage_Appearing"
             Disappearing="MainPage_Disappearing"
             Title="MainPage"
             >
    <ContentPage.BindingContext>
        <vm:MainPageViewModel />
    </ContentPage.BindingContext>
    <ContentPage.Content>
        <StackLayout>
            <Button Text="次画面へ移動する" Command="{Binding GoNextCommand}"/>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

MainPage.xaml.cs (変更)

using MessagingCenterSample.MessagingParameter;
using MessagingCenterSample.ViewModels;
using System;
using Xamarin.Forms;

namespace MessagingCenterSample
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
        }

        private void MainPage_Appearing(object sender, EventArgs e)
        {
            MessagingCenter.Subscribe<MainPageViewModel, AlertParameter>(this, "DisplayAlert", DisplayAlert);
            MessagingCenter.Subscribe<MainPageViewModel>(this, "GoNext", GoNextPage);
        }

        private void MainPage_Disappearing(object sender, EventArgs e)
        {
            MessagingCenter.Unsubscribe<MainPageViewModel, AlertParameter>(this, "DisplayAlert");
            MessagingCenter.Unsubscribe<MainPageViewModel>(this, "GoNext");
        }

        private async void DisplayAlert<T>(T sender, AlertParameter arg)
        {
            var isAccept = await DisplayAlert(arg.Title, arg.Message, arg.Accept, arg.Cancel);
            arg.Action?.Invoke(isAccept);
        }

        private void GoNextPage<T>(T sender)
        {
            this.Navigation.PushAsync(new SecondPage());
        }
    }
}

MainPageViewModel.cs (追加)

using MessagingCenterSample.MessagingParameter;
using Xamarin.Forms;

namespace MessagingCenterSample.ViewModels
{
    public class MainPageViewModel
    {
        public Command GoNextCommand { get; }

        public MainPageViewModel()
        {
            GoNextCommand = new Command(() =>
            {
                MessagingCenter.Send(this, "DisplayAlert", new AlertParameter()
                {
                    Title = "確認",
                    Message = "次画面に移動します。よろしいですか?",
                    Accept = "移動する",
                    Cancel = "移動しない",
                    Action = result =>
                    {
                        if (result)
                            MessagingCenter.Send(this, "GoNext");
                    }
                });
            });
        }
    }
}

AlertParameter (追加)

using System;

namespace MessagingCenterSample.MessagingParameter
{
    class AlertParameter
    {
        public string Title { get; set; }
        public string Message { get; set; }
        public string Accept { get; set; }
        public string Cancel { get; set; }
        public Action<bool> Action { get; set; }
    }
}

コードのポイント

■ メッセージの送信・購読

まず、メッセージの送信です。
今回は ViewModel からメッセージを送信するので、MainPageViewModel.cs に送信のコードを書いています。

MessagingCenter.Send(this, "DisplayAlert", new AlertParameter()
{
  Title = "確認",
  ・・・・

メッセージの購読は、View が行うので、MainPage.xaml.cs です。

MessagingCenter.Subscribe<MainPageViewModel, AlertParameter>(this, "DisplayAlert", DisplayAlert);

これで、MainPageViewModel から "DisplayAlert" というメッセージが送信された際に、DisplayAlert メソッドが実行されます。
また、メッセージを送信する際にパラメータを送りたい場合には、 Send メソッドの 3 つ目の引数に設定し、Subscribe の 2 つ目のジェネリックパラメータにもパラメータの型を指定します。
今回は、ポップアップメッセージに表示する文言等をパラメータで渡しています。

■ メッセージ購読の登録・解除

View 側ではメッセージを Subscribe しますので、対として不要になったら Unsubscribe しなければなりません。
そのタイミングですが、AppearingDisappearing のイベントで行っています。
Appearing は画面がアクティブになった際、Disappearing は画面が閉じられたり別の画面が上に表示された際のイベントです。

■ ポップアップメッセージでの選択

参考
今回は、ポップアップメッセージでの選択結果に応じて画面遷移を行ったり行わなかったりしたい点が厄介です。
DisplayAlert が非同期メソッドですが、MessagingCenter.Send メソッドが同期メソッドであるために結果を待つことができません。
そのため、AlertParameter パラメーターに Action というプロパティでコールバックを設定できるようにしています。
DisplayAlert の終了後に、View 側から Action に設定された処理を呼び出すことで、ViewModel へ処理を返しています。

少々複雑ではありますが、これで望み通りの「ポップアップメッセージを表示し、選択に応じて画面遷移したりしなかったりする」ことが実現できました。

Visual Studio 15.5.5 がリリースされました。

15.5.5 がリリースされました。

最新のリリースは基本的に英語版だけで公開されるので英語版を参照するのがおすすめです。
日本語版は、英語版 というリンクで英語版に誘導されるだけです。

■ 更新内容

バグフィックスになっています。Xamarin に関係するものばかりです。

・Xamarin アプリでエラーが出ることがある問題の修正。
・Xamarin.Android でエラーが出ることがある問題の修正。
・アップグレード時に環境が壊れることがある問題の修正。
・ダウンロードに失敗しないよう JDK 8 最新バージョンにした。

基本的には、起きない人には起きない問題の様です。今の段階で特に問題がないなら急いでアップデートすることもなさそうですね。
環境が壊れるからと、古いバージョンで止めていた方は、同じく急ぐ必要はないかもしれませんがアップデートしてよさそうです。

それでもアップデートしない意味もなさそうなので、とりあえず早めにあぷでーとしておきましょう。

■ 更新方法

Visual Studio の更新はメニューの ツール > ツールと機能を取得 で開くインストーラーから行えます。

JXUG Xamarin もくもく会 東京 1月 を開催しました。

■ JXUG Xamarin もくもく会 東京

JXUG Xamarin もくもく会 東京 1月 を開催しました。

Xamarin もくもく会 は もくもくしたり、情報交換したりする会です。
今回は、参加していただいた皆様、あまり進捗を出していただけなかった様子でした。
次回までに、もっと集中して進捗が出せる環境を考えたいと思います。

不定期ではありますが、ぼちぼち開催していきますので、ご興味があれば是非参加してみてください。

秋葉原 C# もくもく会 #23 勉強会を開催しました。

■ C# もくもく会

C# もくもく会 #23 を開催しました。

C# もくもく会 は東京の秋葉原で毎週木曜日に開催している .NET 系の勉強会です。
もくもく自習を基本とし、分からないことを教えあったり情報共有したりしている会です。

最近、参加者の方のもくもくの幅も増えて会に厚みが出てきた気がします。

定期開催していますので、お時間のある時に遊びに来ていただければと思います。
ちょっと詰まった時、ネット上で聞くのははずかしいなぁ、という課題のできた時などにも思い出していただけると嬉しいです。

■ 次回予定

次回は 2018/02/01 に開催予定です。

C# に関心のある方、是非遊びに来てください。

タイマーの精度について (2)

Xamarin.Forms で一定間隔の時間毎に何か処理をしたい場合、Xamarin.Forms.Device クラスの StartTimer メソッドが使えます。
しかし、精度はそれほど高くはありません。
※ Xamarin.Forms に限らず、他の環境でも普通精度は高くないです
比較的シンプルな1 秒( 1,000 ミリ秒)毎に経過ミリ秒をコンソールに表示するコードを以前に書きました。

上記コードは、十分な動作はするものの端末への負荷をもう少し下げられそうです。
というわけで、少々難しいコードになりますが、もう一つの戦略です。

概要としては毎回タイマー間隔を計算していい感じの間隔設定し続けるものです。

■ コード

public MainPage()
{
    InitializeComponent();

    var count = 0;
    var stopWatch = System.Diagnostics.Stopwatch.StartNew();
    var lastElapsed = 0L;

    Func<bool> func = null;
    func = () =>
    {
         ++count;
         var millisec = stopWatch.ElapsedMilliseconds;
         System.Console.WriteLine($"> {count.ToString("000")} {millisec.ToString("000,000")} +{(millisec - lastElapsed).ToString()}  ");

         if (millisec > 100_000) return false;
         lastElapsed = millisec;

         var span = millisec % 1_000;
         Device.StartTimer(TimeSpan.FromMilliseconds(1_000 - span), func);
         return false;
    };
    func();
}

タイマーで実行したい処理を func にとり、再帰的にタイマーを起動し実行しています。
タイマーは毎回一回しか動作しないので、return 値は必ず false になっています。
ポイントは毎回のタイマーの TimeSpan の設定で、計算により次の秒までの間隔を求めて設定しています。

■ 実行結果

────────────────────
回  経過   前回からの経過
────────────────────
001 000,001 +1
002 001,053 +1052
003 002,002 +949
004 003,004 +1002
005 004,005 +1001
006 005,004 +999
007 006,005 +1001
008 007,004 +999
009 008,005 +1001
010 009,003 +998
011 010,006 +1003
012 011,006 +1000
013 012,005 +999
014 013,005 +1000
015 014,006 +1001
016 015,005 +999
017 016,005 +1000
018 017,005 +1000
019 018,006 +1001
020 019,005 +999
021 020,005 +1000
022 021,006 +1001
023 022,006 +1000
024 023,005 +999
025 024,006 +1001
026 025,005 +999
027 026,005 +1000
028 027,005 +1000
029 028,006 +1001
030 029,005 +999
031 030,004 +999
032 031,005 +1001
033 032,005 +1000
034 033,004 +999
035 034,005 +1001
036 035,006 +1001
037 036,007 +1001
038 037,002 +995
039 038,002 +1000
040 039,004 +1002
041 040,005 +1001
042 041,006 +1001
043 042,005 +999
044 043,005 +1000
045 044,004 +999
046 045,002 +998
047 046,006 +1004
048 047,006 +1000
049 048,004 +998
050 049,006 +1002
051 050,004 +998
052 051,005 +1001
053 052,004 +999
054 053,006 +1002
055 054,005 +999
056 055,005 +1000
057 056,004 +999
058 057,006 +1002
059 058,005 +999
060 059,005 +1000
061 060,005 +1000
062 061,005 +1000
063 062,005 +1000
064 063,004 +999
065 064,006 +1002
066 065,006 +1000
067 066,005 +999
068 067,006 +1001
069 068,004 +998
070 069,005 +1001
071 070,005 +1000
072 071,005 +1000
073 072,005 +1000
074 073,004 +999
075 074,005 +1001
076 075,005 +1000
077 076,005 +1000
078 077,005 +1000
079 078,005 +1000
080 079,004 +999
081 080,006 +1002
082 081,005 +999
083 082,005 +1000
084 083,006 +1001
085 084,005 +999
086 085,006 +1001
087 086,005 +999
088 087,005 +1000
089 088,005 +1000
090 089,005 +1000
091 090,004 +999
092 091,002 +998
093 092,002 +1000
094 093,006 +1004
095 094,005 +999
096 095,006 +1001
097 096,005 +999
098 097,005 +1000
099 098,005 +1000
100 099,005 +1000
101 100,005 +1000

かなり良い結果が得られました。
しかし、理屈が難しく、処理が複雑であることが難点です。一度はうまく動いても後のメンテナンスで不慣れなプログラマーが手を入れた際にバグを出してしまう可能性は決して低くはないと思います。

■ 状況により適した方法を

どの方法にも良いところはあります。状況に合わせてその時々最適な方法を選択することがおすすめです。

タイマーの精度について

Xamarin.Forms で一定間隔の時間毎に何か処理をしたい場合、Xamarin.Forms.Device クラスの StartTimer メソッドが使えます。
しかし、精度はそれほど高くはありません。
※ Xamarin.Forms に限らず、他の環境でも普通精度は高くないです
というわけで、確認してみましょう。
次のコードは、1 秒( 1,000 ミリ秒)毎に経過ミリ秒をコンソールに表示するコードです。

■ コード

public MainPage()
{
    InitializeComponent();

    var count=0;
    var stopWatch = System.Diagnostics.Stopwatch.StartNew();
    var lastElapsed = 0L;

    Device.StartTimer(TimeSpan.FromSeconds(1), () =>
    {
        ++count;
        var millisec = stopWatch.ElapsedMilliseconds;
        System.Console.WriteLine($"{count.ToString("000")} {millisec.ToString("000,000")} +{(millisec-lastElapsed).ToString()}");
        lastElapsed = millisec;

        if (count > 100) return false;
        return true;
    });
}

■ 実行結果

────────────────────
回  経過   前回からの経過
────────────────────
001 001,019 +1019
002 002,108 +1089
003 003,114 +1006
004 004,120 +1006
005 005,125 +1005
006 006,131 +1006
007 007,137 +1006
008 008,141 +1004
009 009,143 +1002
010 010,149 +1006
011 011,154 +1005
012 012,159 +1005
013 013,165 +1006
014 014,171 +1006
015 015,177 +1006
016 016,183 +1006
017 017,188 +1005
018 018,194 +1006
019 019,199 +1005
020 020,204 +1005
021 021,209 +1005
022 022,215 +1006
023 023,219 +1004
024 024,225 +1006
025 025,230 +1005
026 026,234 +1004
027 027,240 +1006
028 028,246 +1006
029 029,252 +1006
030 030,258 +1006
031 031,264 +1006
032 032,270 +1006
033 033,276 +1006
034 034,280 +1004
035 035,283 +1003
036 036,287 +1004
037 037,290 +1003
038 038,295 +1005
039 039,300 +1005
040 040,305 +1005
041 041,311 +1006
042 042,316 +1005
043 043,322 +1006
044 044,327 +1005
045 045,331 +1004
046 046,337 +1006
047 047,343 +1006
048 048,349 +1006
049 049,354 +1005
050 050,360 +1006
051 051,366 +1006
052 052,371 +1005
053 053,377 +1006
054 054,382 +1005
055 055,388 +1006
056 056,394 +1006
057 057,400 +1006
058 058,406 +1006
059 059,411 +1005
060 060,416 +1005
061 061,420 +1004
062 062,425 +1005
063 063,430 +1005
064 064,436 +1006
065 065,441 +1005
066 066,445 +1004
067 067,451 +1006
068 068,457 +1006
069 069,461 +1004
070 070,467 +1006
071 071,473 +1006
072 072,477 +1004
073 073,481 +1004
074 074,484 +1003
075 075,488 +1004
076 076,494 +1006
077 077,499 +1005
078 078,503 +1004
079 079,508 +1005
080 080,513 +1005
081 081,519 +1006
082 082,524 +1005
083 083,529 +1005
084 084,535 +1006
085 085,540 +1005
086 086,544 +1004
087 087,548 +1004
088 088,554 +1006
089 089,560 +1006
090 090,565 +1005
091 091,569 +1004
092 092,574 +1005
093 093,579 +1005
094 094,584 +1005
095 095,617 +1033
096 096,619 +1002
097 097,622 +1003
098 098,625 +1003
099 099,631 +1006
100 100,637 +1006
101 101,640 +1003

毎回、1,000 ミリ秒ではなく、おおむね 5 ミリ秒程度の誤差が出ていることが確認できます。
それらの誤差がたまり続けて、最終的には実際の経過時間と 640 ミリ秒の誤差になっています。

というわけで、時間を正確に刻みたい場合は少し手をかけてやる必要があります。

■ コード

public MainPage()
{
    InitializeComponent();

    var count = 0;
    var stopWatch = System.Diagnostics.Stopwatch.StartNew();
    var lastElapsed = 0L;
    var lastSeconds = 0L;

    Device.StartTimer(TimeSpan.FromMilliseconds(33), () =>
    {
        try
        {
            var millisec = stopWatch.ElapsedMilliseconds;
            var seconds = (millisec / 1_000);
            if (seconds <= lastSeconds) return true;

            ++count;
            System.Console.WriteLine($"> {count.ToString("000")} {millisec.ToString("000,000")} +{(millisec - lastElapsed).ToString()}  ");
            lastElapsed = millisec;
            lastSeconds = seconds;

            if (millisec > 100_000) return false;
            return true;
        }
        catch (Exception ex)
        {
            System.Console.WriteLine(ex.Message);
            return false;
        }
    });
}

動作としては 1 秒ごとに画面更新をすればよいのですが、それより遥かに短い時間(ここでは 33 ミリ秒)でタイマーを動かしています。
そうして数多くのタイマー処理の中から経過秒が変わったいい感じのタイミングでだけ処理をすることで、総合のズレが少ない安定した時刻の刻みが得られます。

■ 実行結果

────────────────────
回  経過   前回からの経過
────────────────────
001 001,015 +1015
002 002,025 +1010
003 003,010 +985
004 004,029 +1019
005 005,014 +985
006 006,004 +990
007 007,026 +1022
008 008,017 +991
009 009,005 +988
010 010,026 +1021
011 011,012 +986
012 012,034 +1022
013 013,024 +990
014 014,013 +989
015 015,034 +1021
016 016,024 +990
017 017,016 +992
018 018,004 +988
019 019,029 +1025
020 020,024 +995
021 021,017 +993
022 022,007 +990
023 023,006 +999
024 024,033 +1027
025 025,022 +989
026 026,012 +990
027 027,002 +990
028 028,027 +1025
029 029,021 +994
030 030,011 +990
031 031,032 +1021
032 032,022 +990
033 033,009 +987
034 034,031 +1022
035 035,021 +990
036 036,012 +991
037 037,804 +1792
038 038,009 +205
039 039,017 +1008
040 040,028 +1011
041 041,015 +987
042 042,030 +1015
043 043,020 +990
044 044,028 +1008
045 045,006 +978
046 046,029 +1023
047 047,018 +989
048 048,004 +986
049 049,027 +1023
050 050,017 +990
051 051,034 +1017
052 052,020 +986
053 053,010 +990
054 054,032 +1022
055 055,025 +993
056 056,018 +993
057 057,005 +987
058 058,030 +1025
059 059,015 +985
060 060,007 +992
061 061,034 +1027
062 062,019 +985
063 063,022 +1003
064 064,012 +990
065 065,000 +988
066 066,025 +1025
067 067,016 +991
068 068,003 +987
069 069,023 +1020
070 070,016 +993
071 071,007 +991
072 072,033 +1026
073 073,021 +988
074 074,016 +995
075 075,000 +984
076 076,028 +1028
077 077,015 +987
078 078,034 +1019
079 079,022 +988
080 080,011 +989
081 081,000 +989
082 082,027 +1027
083 083,014 +987
084 084,001 +987
085 085,053 +1052
086 086,015 +962
087 087,035 +1020
088 088,024 +989
089 089,013 +989
090 090,004 +991
091 091,029 +1025
092 092,018 +989
093 093,009 +991
094 094,034 +1025
095 095,024 +990
096 096,009 +985
097 097,006 +997
098 098,018 +1012
099 099,024 +1006
100 100,010 +986

一回ずつの誤差はより大きくなっていますが、最終的には 10 ミリ秒の誤差ということでほぼ正しく時刻が刻まれています。
例えば今回は 37 回目で大きなズレが出ていますが、38 回目で復帰していることも確認できます。

■ デメリットと調整

上記例では、もともと必要な画面更新回数のおおよそ 30 倍の回数のイベント処理を行っています。
当然その分、負荷は上がっています。バッテリーの持ちにも影響があるかもしれません。
そのあたり、必要な精度を考えて最適な間隔を設定してください。例えば上記例でいえば 200 ミリ秒間隔程度でよさそうです。

■ もう一つの戦略

今回は大目に動かしていい感じのところを抽出する戦略をとりました。
しかし、実現方法は他にもあります。
概要としては毎回タイマー間隔を計算していい感じの間隔設定し続けるものです。
こちらはまた後日。

秋葉原 C# もくもく会 #22 勉強会を開催しました。

■ C# もくもく会

C# もくもく会 #22 を開催しました。

C# もくもく会 は東京の秋葉原で毎週木曜日に開催している .NET 系の勉強会です。
もくもく自習を基本とし、分からないことを教えあったり情報共有したりしている会です。

定期開催していますので、お時間のある時に遊びに来ていただければと思います。
ちょっと詰まった時、ネット上で聞くのははずかしいなぁ、という課題のできた時などにも思い出していただけると嬉しいです。

■ 次回予定

次回は 2018/01/25 に開催予定です。

.NET に関心のある方、是非遊びに来てください。