takataka430’s blog

.NET系を中心に勉強したことのまとめを書きます

Visual Studio 2019でXamarin.Formsを使ってみました

Visual Studio 2019がそろそろリリースされますね。それに先立ちプレビュー版をダウンロードできるようなので、使ってみました。
この記事ではVisual Studio 2019でXamarin.Formsを少し使ってみて気づいたことを書いてみようと思います。
Visual Studio 2017 for Macとの比較です)

プロジェクト作成後、XAMLプレビューがビルドする前から使える

2017だとビルドしてからでないとプレビューが表示されなかったと思うのですが、2019だとビルド前からプレビューに表示されるようです。ただ、自分の場合なぜかAndroidはエラーになります(ビルドすればちゃんと表示されます)。

XAMLプレビューで機種が選べる

下の画像のようにプレビュー画面の上に機種を選択するバーが設置されています。ここからスマートフォンタブレットの機種を選べます。何気に便利そうだと思いました。

f:id:takataka430:20190329220811p:plain:w300

XAMLの編集画面で縦線が表示される

下の画像のように要素の左端から細い点線が出ています。2017ではなかったですよね? 要素の縦の位置を揃えるのに便利だと思いました。

f:id:takataka430:20190329221421p:plain


まだ少し触っただけなので気づいたのはこのくらいです。ぜひみなさんも使ってみてください!

【Xamarin.Forms】ViewModelでContentViewとFrameを操作してDisplayAlertの代わりにできないか試してみた

ViewModelで悩むことの一つとして「DisplayAlertを呼び出せない」というのがあると思います。色々解決方法を考えていて、「もしかしてContentViewとFrameを使えばDisplayAlertの代わりになるのでは??」と思いついたのでやってみたメモです。
(ライブラリやフレームワークは使わずに素のXamarin.Formsでやってみました)

コード

次のように書いてみました。(コードビハインドはInitializeComponentのみなので省略します)

画面

<?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:AlertFromViewModel" 
             x:Class="AlertFromViewModel.MainPage">
    <ContentPage.BindingContext>
        <local:MainPageViewModel/>
    </ContentPage.BindingContext>
    <AbsoluteLayout>
        <Button Text="Alert" 
                AbsoluteLayout.LayoutFlags="PositionProportional"
                AbsoluteLayout.LayoutBounds="0.5, 0.5, AutoSize, AutoSize"
                HorizontalOptions="Center" 
                VerticalOptions="CenterAndExpand" 
                Command="{Binding OnAlert}"/>
        <ContentView AbsoluteLayout.LayoutFlags="All"
                     AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
                     Opacity="0.4"
                     BackgroundColor="Black"
                     IsVisible="{Binding IsVisible}"/>
        <Frame AbsoluteLayout.LayoutFlags="PositionProportional"
               AbsoluteLayout.LayoutBounds="0.5, 0.5, AutoSize, AutoSize"
               IsVisible="{Binding IsVisible}">
            <StackLayout>
                <Label Text="アラートです"
                       HorizontalTextAlignment="Center"/>
                <Button Text="OK"
                        Command="{Binding OnOK}"/>
            </StackLayout>
        </Frame>        
    </AbsoluteLayout>
</ContentPage>

  
ビューモデル

using System.ComponentModel;
using Xamarin.Forms;

namespace AlertFromViewModel
{
    public class MainPageViewModel : INotifyPropertyChanged
    {
        public MainPageViewModel()
        {
            OnOK = new Command(() =>
            {
                IsVisible = false;
            });

            OnAlert = new Command(() =>
            {
                IsVisible = true;
            });
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private bool isVisible;
        public bool IsVisible
        {
            get { return isVisible; }
            set
            {
                if(isVisible != value)
                {
                    isVisible = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsVisible)));
                }
            }
        }

        public Command OnOK { get; }

        public Command OnAlert { get; }
    }
}

画面の「Alert」ボタンを押すとContentViewとFrameが表示されます。その後Frameの「OK」ボタンを押すとContentViewとFrameが非表示になり、元の画面に戻ります。

動きはこんな感じです!

f:id:takataka430:20190324230137g:plain:w200

アラートっぽくなりましたね!
  
・・・でも正直ちょっと面倒なので、素直にコードビハインドに書くかライブラリを使った方がいいのかなと思いました。

【Xamarin.Forms】QRコードの読み取りをする方法

Xamarin.FormsにはQRコードやバーコードを読み取るためのZXing.Net.Mobileというライブラリがあります。

GitHub - Redth/ZXing.Net.Mobile: Zxing Barcode Scanning Library for MonoTouch, Mono for Android, and Windows Phone

今回はこのライブラリを使用して、Xamarin.FormsでQRコードをスキャンして表示する簡単なアプリを作ってみました。

環境

Visual Studio Community 2017 for Mac
Xamarin.Forms (3.6.0.220655)
ZXing.Net.Mobile (2.4.1)
ZXing.Net.Mobile.Forms (2.4.1)

手順

準備

それぞれのプロジェクト(.Net Standard、iOSAndroid)に以下のNuGet Packageをインストールします。

  • ZXing.Net.Mobile
  • ZXing.Net.Mobile.Forms   
      

次にAndroidのMainActivity.csとiOSのAppDelegate.csにコードの追加を行います。   

MainActivity.cs

public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
    protected override void OnCreate(Bundle savedInstanceState)
    {
        (省略)

        //追加
        ZXing.Net.Mobile.Forms.Android.Platform.Init();

        LoadApplication(new App());
    }

    //追加
    public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
    {
       global::ZXing.Net.Mobile.Android.PermissionsHandler.OnRequestPermissionsResult(requestCode, permissions, grantResults);
       base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

  
AppDelegate.cs  

[Register("AppDelegate")]
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    {
        global::Xamarin.Forms.Forms.Init();

        //追加
        ZXing.Net.Mobile.Forms.iOS.Platform.Init();

        LoadApplication(new App());
        return base.FinishedLaunching(app, options);
    }
}

次に端末でカメラを使用するための準備をします。
  
Android:AndroidManifest.xmlの「必要なアクセス許可」の「カメラ」にチェックを入れる

iOS:info.plistを開き、以下の項目を追加する
* プロパティ:プライバシー-カメラの利用状況の説明
* 値:"カメラを利用してスキャンします"

最後にナビゲーションページを使うためにApp.xaml.csに以下の変更を行います。

public App()
{
    InitializeComponent();
    
    //ここを変更
    MainPage = new NavigationPage(new MainPage());
}

  
準備はこれでOKです。それでは画面を作っていきましょう。


画面作成と処理の追加

それではアプリを作っていきましょう。ページは次の2つだけです。

  • MainPage:中央にボタンがあって、それを押すとスキャンするページ(QRScanPage)に移動
  • QRScanPage:読み取りを行うと画面中央に読み取った値をアラートに表示する  OKボタンを押すと読み取りを再開

  
最初にMainPageです。このページはボタンを押して次のページに行くだけです。
  
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:QRCodeStudy" 
             x:Class="QRCodeStudy.MainPage">
    <StackLayout>
        <Button Text="QRコード読み取り" 
                Clicked="OnQR"
                HorizontalOptions="Center" 
                VerticalOptions="CenterAndExpand" />
    </StackLayout>
</ContentPage>

  

MainPage.xaml.cs

using System;
using Xamarin.Forms;

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

        void OnQR(object sender, EventArgs e)
        {
            Navigation.PushAsync(new QRScanPage());
        }
    }
}

次に読み取りページを作っていきます。   

QRScanPage.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="QRCodeStudy.QRScanPage"
             xmlns:zxing="clr-namespace:ZXing.Net.Mobile.Forms;assembly=ZXing.Net.Mobile.Forms">
    <ContentPage.Content>
        <Grid>
            <zxing:ZXingScannerView x:Name="zxing"
                                    OnScanResult="Handle_OnScanResult"/>
            <zxing:ZXingDefaultOverlay />
        </Grid>
    </ContentPage.Content>
</ContentPage>

ZXingScannerViewのOnScanResultイベントで読み取った値をアラートに表示するように設定します。   

QRScanPage.xaml.cs

using Xamarin.Forms;

namespace QRCodeStudy
{
    public partial class QRScanPage : ContentPage
    {
        public QRScanPage()
        {
            InitializeComponent();
        }

        void Handle_OnScanResult(ZXing.Result result)
        {
            Device.BeginInvokeOnMainThread(async () =>
            {
                zxing.IsAnalyzing = false;  //読み取り停止
                await DisplayAlert("通知","次の値を読み取りました:" + result.Text,"OK");
                zxing.IsAnalyzing = true;   //読み取り再開
            });
        }

        protected override void OnAppearing()
        {
            base.OnAppearing();
            zxing.IsScanning = true;
        }

        protected override void OnDisappearing()
        {
            zxing.IsScanning = false;
            base.OnDisappearing();
        }
    }
}

動きは以下のような感じです。
(手元にQRコードがなかったのでバーコードを読み取っています。QRコードも同様に読み取る事ができます。)

f:id:takataka430:20190321140723g:plain:w200

ViewModelで処理を行う

上のコードは値を読み取った時の処理をコードビハインドに書いています。これをビューモデルに書いてみましょう。
QRScanPageを書き換えて、ビューモデルを追加します。コードは以下の通りです。
  
QRScanPage.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="QRCodeStudy.QRScanPage"
             xmlns:zxing="clr-namespace:ZXing.Net.Mobile.Forms;assembly=ZXing.Net.Mobile.Forms"
             xmlns:local="clr-namespace:QRCodeStudy">
    <ContentPage.BindingContext>
        <local:QRScanPageViewModel/>
    </ContentPage.BindingContext>
    <ContentPage.Content>
        <AbsoluteLayout>
            <Grid AbsoluteLayout.LayoutFlags="All"
                  AbsoluteLayout.LayoutBounds="0.5, 0.5, 1, 1">
                <zxing:ZXingScannerView x:Name="zxing"
                                        ScanResultCommand="{Binding OnScan}"
                                        IsAnalyzing="{Binding IsAnalyzing}"/>
                <zxing:ZXingDefaultOverlay />
            </Grid>
            <Frame AbsoluteLayout.LayoutFlags="PositionProportional"
                   AbsoluteLayout.LayoutBounds="0.5, 0.5, AutoSize, AutoSize"
                   IsVisible="{Binding FrameVisible}">
                <StackLayout>
                    <Label Text="読み取った値"
                           HorizontalTextAlignment="Center"/>
                    <Label Text="{Binding ScannedCode}"
                           HorizontalTextAlignment="Center"/>
                </StackLayout>
            </Frame>
        </AbsoluteLayout>
    </ContentPage.Content>
</ContentPage>

ZXingScannerViewはScanResultCommandというプロパティを持っているようなのでこれをビューモデルのCommandにバインドしてみます。また、ビューモデルからDisplayAlertを呼び出す方法がわからないので、Frameにスキャン結果を記述するようにします。   
  
QRScanPage.xaml.cs

using Xamarin.Forms;

namespace QRCodeStudy
{
    public partial class QRScanPage : ContentPage
    {
        public QRScanPage()
        {
            InitializeComponent();
        }

        protected override void OnAppearing()
        {
            base.OnAppearing();
            zxing.IsScanning = true;
        }

        protected override void OnDisappearing()
        {
            zxing.IsScanning = false;
            base.OnDisappearing();
        }
    }
}

  

QRScanPageViewModel

using System.ComponentModel;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace QRCodeStudy
{
    public class QRScanPageViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public QRScanPageViewModel()
        {
            OnScan = new Command<ZXing.Result>((result)=>
            {
                Device.BeginInvokeOnMainThread( async () =>
                {
                    this.IsAnalyzing = false;  //読み取り停止
                    FrameVisible = true;       //Frameを表示
                    ScannedCode = result.Text;
                    await Task.Delay(1000);    //1秒待機
                    this.IsAnalyzing = true;   //読み取り再開
                    FrameVisible = false;      //Frameを非表示
                });
            });
        }

        public Command OnScan { get; }

        private bool isAnalyzing;
        public bool IsAnalyzing
        {
            get { return isAnalyzing; }
            set
            {
                if (isAnalyzing != value)
                {
                    isAnalyzing = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsAnalyzing)));
                }
            }
        }

        private string scannedCode;
        public string ScannedCode
        {
            get { return scannedCode; }
            set
            {
                if (scannedCode != value)
                {
                    scannedCode = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ScannedCode)));
                }
            }
        }

        private bool frameVisible;
        public bool FrameVisible
        {
            get { return frameVisible; }
            set
            {
                if (frameVisible != value)
                {
                    frameVisible = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FrameVisible)));
                }
            }
        }
    }
}

コードを読み取ったら読み取った値が記載されているFrameを1秒間表示し、その間は読み取りをしないようにします。Frameが非表示になったら読み取りを再開するようにしています。
  
動きを見てみましょう。

f:id:takataka430:20190321153800g:plain:w200

うまく動きました!

参考にしたページ

ZXing.Net Mobile を使ってみた - Android 編 - - Xamarin 日本語情報

ZXing.Net Mobile を使ってみた - iOS、UWP 編 - - Xamarin 日本語情報

GitHub - Redth/ZXing.Net.Mobile: Zxing Barcode Scanning Library for MonoTouch, Mono for Android, and Windows Phone

Is there a way to render the Zxing ScannerPage in Xaml? — Xamarin Community Forums

【Xamarin.Forms】ページ間での値の受け渡し方法を考えてみた(その2)

以前、ページ間の値の受け渡しをコードビハインドに記述してやってみるという記事を投稿しました。

takataka430.hatenablog.com

今回はMVVMっぽく、ViewModelとModelを使ってやる方法を考えてみました。
1ページ目と2ページ目それぞれのVidwModelを作り、それぞれがModelを参照・更新するというような感じです。

環境

Visual Studio Community 2017 for Mac
Xamarin.Forms (3.4.0.1008975)

手順

まずは以下のようなStaticなPersonクラスを作ります。これで複数のクラスから参照することができます。

namespace XF_NavigationMVVM.Models
{
    public static class Person
    {
        public static string Name { get; set; }
    }
}

  

1ページ目の画面、コードビハインド、ビューモデルは以下のようになります。Entryに値が入力されるとビューモデルのNameプロパティと、PersonクラスのNameプロパティに値が設定されます。   

画面

<?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="XF_NavigationMVVM.Views.FirstPage">
    <ContentPage.Content>
        <StackLayout VerticalOptions="Center">
            <Entry Text="{Binding Name}"
                   Placeholder="名前を入力してください"/>
            <Button Text="次のページへ"
                    Clicked="ToSecondPage"/>
        </StackLayout> 
    </ContentPage.Content>
</ContentPage>

  
コードビハインド

using System;

using Xamarin.Forms;
using XF_NavigationMVVM.ViewModels;

namespace XF_NavigationMVVM.Views
{
    public partial class FirstPage : ContentPage
    {
        public FirstPage()
        {
            InitializeComponent();
            BindingContext = new FirstPageViewModel();
        }

        void ToSecondPage(object sender, EventArgs e)
        {
            Navigation.PushAsync(new SecondPage());
        }
    }
}

  
ビューモデル

namespace XF_NavigationMVVM.ViewModels
{
    public class FirstPageViewModel 
    {
        private string name;
        public string Name 
        {
            get { return name; }
            set
            {
                name = value;
                Models.Person.Name = name;
            }
        }
    }
}

  

2ページ目の画面、コードビハインド、ビューモデルは以下のようになります。ビューモデルのコンストラクタでPersonクラスのNameプロパティを受け取り画面に表示します。   
  
画面

<?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="XF_NavigationMVVM.Views.SecondPage">
    <ContentPage.Content>
        <StackLayout VerticalOptions="Center">
            <Label Text="{Binding Name}"/>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

  
コードビハインド

using Xamarin.Forms;
using XF_NavigationMVVM.ViewModels;

namespace XF_NavigationMVVM.Views
{
    public partial class SecondPage : ContentPage
    {
        public SecondPage()
        {
            InitializeComponent();
            BindingContext = new SecondPageViewModel();
        }
    }
}

  
ビューモデル

namespace XF_NavigationMVVM.ViewModels
{
    public class SecondPageViewModel
    {
        public SecondPageViewModel()
        {
            this.Name = Models.Person.Name;
        }

        public string Name { get; set; }
    }
}

動きを確認すると以下の方な感じです。

f:id:takataka430:20190316211321g:plain

一応画面上ではできているようですが、こんな感じでいいのかな・・?

気になる事

この場合だと逆のパターン、つまり2ページ目に値を入力した後、ナビゲーションバーで1ページ目に戻りそこで2ページ目に入力した値を表示する場合はどうすればいいですかね?今回の方法だとできなさそうなので気になりました。ちょっと調べてみようと思います。

【Xamarin.Forms】マテリアルデザインを使ってみた

数日前にXamarin.FormsでButtonやEntryなどのUIをマテリアルデザインにできる機能が追加されたようなので早速試してみました。

環境

Visual Studio Community 2017 for Mac
Xamarin.Forms (3.6.0.220655)

手順

まずは空のXamarin.Formsアプリを作り、Nugetパッケージ【Xamarin.Forms.Visual.Material】を各プロジェクト(共通、AndroidiOS)にインストールします。また、全てのNugetパッケージは最新にしておきます。

f:id:takataka430:20190314214130p:plain

  
次にAndroidプロジェクトのTarget FrameworkをAndroid9.0に変更します。(ここで8.1などの低いバージョンになっているとこの後のコードでエラーになります)

f:id:takataka430:20190314220840p:plain

最後にAndroidiOSの各プロジェクトのファイルにコードを追加します。

Android(MainActivity.cs)

protected override void OnCreate(Bundle savedInstanceState)
{
    TabLayoutResource = Resource.Layout.Tabbar;
    ToolbarResource = Resource.Layout.Toolbar;

    base.OnCreate(savedInstanceState);
    global::Xamarin.Forms.Forms.Init(this, savedInstanceState);

    //ここを追加
    global::Xamarin.Forms.FormsMaterial.Init(this, savedInstanceState);

    LoadApplication(new App());
}

iOS(AppDelegate.cs)

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

    //ここを追加
    global::Xamarin.Forms.FormsMaterial.Init(); 

    LoadApplication(new App());
    return base.FinishedLaunching(app, options);
}

これで準備は完了です。あとはXAMLに記述するだけです。UIのVisualプロパティを「Material」とすればOKです。例えばButtonだと以下のようになります。

<Button Visual="Material"
        Text="マテリアルデザインのボタン" />

Buttonを例にしてデフォルトとマテリアルデザインの比較を実際の画面で確認しましょう。

f:id:takataka430:20190314221117p:plain:w250f:id:takataka430:20190314221008p:plain:w250
左:iOS   右:Android

iOSは結構変わりますね!

使ってみて一つ気になる点としては、マテリアルデザインにするとXAMLのプレビューでエラーが発生するようで、表示されなくなってしまうことです。うーん、これは困る。何か解決方法はないものか・・・。

といったところで今回の記事はこれまで!

参考ページ

https://blog.xamarin.com/beautiful-material-design-android-ios/

Xamarin.Forms マテリアル Visual - Xamarin | Microsoft Docs

【Xamarin.Forms】ToolbarItemを有効化・無効化する方法

以前Xamarin.Formsで画面の操作をロックする方法(ナビゲーションバー含む)という記事を書きました。その記事では画面全体をContentPageで覆ってもナビゲーションバーが隠れないのでナビゲーションバーを含めた画面全体を覆う方法を考えました。しかし、よく考えたらナビゲーションバーのボタン、つまりToolbarItemを無効化する方法もあるのでは?と思い、その方法を調べました。

結論から言いますと、ToolbarItemのCommandプロパティをバインドすることで実現することができました。コマンドを実行できる場合はボタンが有効、実行できない場合はボタンが無効になるようです。
(ちなみに、ToolbarItemはIsEnableプロパティも持っているようなので最初はこちらを使おうと思いましたが、うまくいきませんでした。)

環境

Visual Studio Community 2017 for Mac
Xamarin.Forms (3.6.0.220655)

コード

画面は次の通りです。コードビハインドはInitializeComponent()のみの記載です。ToolbarItemのCommandプロパティをバインドします。

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:XF_Overlay" 
             x:Class="XF_Overlay.MainPage">
    <ContentPage.BindingContext>
        <local:MainPageViewModel/>
    </ContentPage.BindingContext>
    <ContentPage.ToolbarItems>
        <ToolbarItem Text="Test"
                     Command="{Binding TBCommand}"/>  <!-- ここでバインド -->
    </ContentPage.ToolbarItems>
    <AbsoluteLayout>
        <StackLayout AbsoluteLayout.LayoutFlags="All"
                     AbsoluteLayout.LayoutBounds="0,0,1,1"
                     VerticalOptions="Center">
            <Button Command="{Binding OverlayClicked}"
                    Text="OverlayVer2"/>
        </StackLayout>
        <ContentView x:Name="bglayer"
                     BackgroundColor="Black"
                     Opacity="0.4"
                     IsVisible="{Binding IsBusy}"
                     AbsoluteLayout.LayoutFlags="All"
                     AbsoluteLayout.LayoutBounds="0,0,1,1"/>
            <Frame  x:Name="frame"
                    IsVisible="{Binding IsBusy}"
                    AbsoluteLayout.LayoutFlags="PositionProportional"
                    AbsoluteLayout.LayoutBounds="0.5,0.5,AutoSize,AutoSize">
                <StackLayout>
                    <ActivityIndicator  Color="Black"
                                        IsRunning="true"/>
                    <Label Text="処理中です" />
                </StackLayout>
            </Frame>
    </AbsoluteLayout>
</ContentPage>

  
ビューモデルは次のようになります。有効・無効の値を入れるためにIsEnableプロパティを作成します。Commandの第2引数であるChangeCanExecuteにIsEnableプロパティを指定し、このプロパティの値を変更するたびにコマンドの実行可否を再評価します。前述の通りコマンドが実行可能な場合はボタンが有効に、実行不可能な場合はボタンが無効になるようなので、それを利用しています。

MainPageViewModel.cs

using System.ComponentModel;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace XF_Overlay
{
    public class MainPageViewModel : INotifyPropertyChanged
    {
        public MainPageViewModel()
        {
            IsEnable = true;
            IsBusy = false;

            OverlayClicked = new Command(async() =>
            {
                IsEnable = false;
                IsBusy = true;
                TBCommand.ChangeCanExecute();  //コマンドの実行可否を再評価
                await Task.Delay(2000);
                IsEnable = true;
                IsBusy = false;
                TBCommand.ChangeCanExecute();  //コマンドの実行可否を再評価
            });

            //コマンドの実行可否をIsEnableプロパティによって決定
            TBCommand = new Command( () =>
            {

            }, ()=> IsEnable);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public Command OverlayClicked { get; }
        public Command TBCommand { get; set; }

        public bool IsEnable { get; set; }

        private bool isBusy;
        public bool IsBusy
        {
            get { return isBusy; }
            set
            {
                if(isBusy != value)
                {
                    isBusy = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsBusy)));
                }
            }
        }
    }
}

動きは下のような感じです。

f:id:takataka430:20190307214501g:plain:w200

処理中は右上のTestボタンが押せなくなっていますね!

参考にしたサイト

ToolbarItem を Disable 状態にする方法 | Xamarin.Forms - ITブログ時々なんでもブログ

Xamarin.Forms の Command でも CanExecute でボタンを制御 - Xamarin 日本語情報

【Xamarin.Forms】データバインディングとMVVMを学ぶ

今回の記事ではデータバインディングによってLabelやEntryの表示・非表示を行い、さらにMVVMっぽくするところまでをやってみたいと思います。題材として以下のような動きをするアプリを考えてみることにします。

f:id:takataka430:20190302231209g:plain:w200

初めはユーザーIDとパスワードを入力する入力欄2つとログインボタン1つを表示しておき、ボタンを押すとログアウトするボタンのみが表示されます。これをクリックするごとに切り替えることとします。

データバインディング無しでコードビハインドに書く

まずは特に何も考えずにコードビハインドに記述してみましょう。

  
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:XF_MVVM"
             x:Class="XF_MVVM.MainPage">
    <StackLayout VerticalOptions="Center">
        <Entry x:Name="entry1" 
               Placeholder="ユーザーIDを入力してください"/>
        <Entry x:Name="entry2"
               Placeholder="パスワードを入力してください"/>
        <Button x:Name="button1"
                Text="ログインする"
                Clicked="Handle_Clicked"/>
        <Button x:Name="button2"
                Text="ログアウトする"
                Clicked="Handle_Clicked"
                IsVisible="false"/>
    </StackLayout>
</ContentPage>

  

MainPage.xaml.cs

using System;
using Xamarin.Forms;

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

        void Handle_Clicked(object sender, EventArgs e)
        {
            entry1.IsVisible = !entry1.IsVisible;
            entry2.IsVisible = !entry2.IsVisible;
            button1.IsVisible = !button1.IsVisible;
            button2.IsVisible = !button2.IsVisible;
        }
    }
}

ボタンを押すごとにLabel、EntryのIsVisibleプロパティを否定します(Trueの場合はFalseに、Falseの場合はTrueにします)。一応これでも目的の動きを達成していますが、Handle_Clicked内の処理を見ると各要素に同じような処理をしていることがわかります。entry1、entry2、button1は常に同じ値になるので共通の値を参照するような形にしたほうが良さそうです。そこでデータバインディングを活用します。

データバインディングを設定する

データバインディングとは2つのオブジェクト間において、一方のプロパティが変更されたらもう一方のプロパティも変更する仕組みのようです。詳しくは以下をご覧ください。

docs.microsoft.com

データバインディングではソースターゲットの二つの役割があります。基本的にはソースのプロパティの値が変更されたらターゲットのプロパティの値も自動的に変更されるようです。

先ほどのコードをデータバインディングするように変更してみます。

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:XF_MVVM"
             x:Class="XF_MVVM.MainPage">
    <StackLayout VerticalOptions="Center">
        <Entry Placeholder="ユーザーIDを入力してください"
               IsVisible="{Binding IsLogin}"/>  <!-- 追加 -->
        <Entry Placeholder="パスワードを入力してください"
               IsVisible="{Binding IsLogin}"/>  <!-- 追加 -->
        <Button Text="ログインする"
                Clicked="Handle_Clicked"
                IsVisible="{Binding IsLogin}"/>  <!-- 追加 -->
        <Button Text="ログアウトする"
                Clicked="Handle_Clicked"
                IsVisible="{Binding IsLogout}"/>  <!-- 追加 -->
    </StackLayout>
</ContentPage>

「追加」となっている場所ではターゲットを指定しています。それぞれのIsVisibleプロパティが、ソースIsLoginまたhIsLogoutプロパティと同期するようにしています。
次にコードビハインドを見てみましょう。   

MainPage.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace XF_MVVM
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
            
            //ソースの指定
            BindingContext = this;
        }

        public bool IsLogin { get; set; } = true;

        public bool IsLogout { get; set; } = false;

        //ボタンを押すと各プロパティの値が否定される
        void Handle_Clicked(object sender, EventArgs e)
        {
            IsLogin = !IsLogin;
            IsLogout = !IsLogout;
        }
    }
}

BindingContextによってソースとなるオブジェクトの指定を行っています。
以上をまとめると、2つのEntry要素と「ログインする」LabelはIsLoginプロパティを基に、「ログアウトする」LabelはIsLogoutプロパティを基に値が決まるという設定をしました。
動きを確認してみましょう。

f:id:takataka430:20190304002514g:plain:w200

ボタンを押しても何も変化しません。しかしVisualStudioで確認するとIsLoginIsLogoutそれぞれのプロパティはボタンを押すと値が変化します。
なぜこうなるかといいますと、今回の場合、ソースの値が変化してもターゲットに通知されないためです。そのため変更を通知するためにソースにINotifyPropertyChangedを実装する必要があります。

INotifyPropertyChangedの実装

実装すると以下のようになります。(MainPage.xamlは変更なしのため省略)

MainPage.xaml.cs

using System;
using System.ComponentModel;
using Xamarin.Forms;

namespace XF_MVVM
{
    public partial class MainPage : ContentPage
    {
        Control control = new Control();

        public MainPage()
        {
            InitializeComponent();
            BindingContext = control;
        }

        void Handle_Clicked(object sender, EventArgs e)
        {
            control.IsLogin = !control.IsLogin;
            control.IsLogout = !control.IsLogout;
        }
    }

    public class Control : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private bool isLogin = true;
        public bool IsLogin
        {
            get { return isLogin; }
            set
            {
                isLogin = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLogin)));
            }
        }

        private bool isLogout;
        public bool IsLogout
        {
            get { return this.isLogout; }
            set
            {
                isLogout = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLogout)));
            }
        }
    }
}

INotifyPropertyChangedを継承したControlクラスを新しく作ります。
  
※INotifyPropertyChangedについて詳しくは以下をご覧ください。

docs.microsoft.com

MainPage.xaml.csクラスでControlクラスのインスタンスを作り、このインスタンスをソースに指定します。こうすれば変更が通知されるはずです。

f:id:takataka430:20190304004022g:plain:w200

うまく動きました!

MVVMに書き換えてみる

以上のようなコードでも良さそうですが、ビューのファイルに画面と画面を操作するコードが混在して書かれているので、これを分離してみたいと思います。   

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:XF_MVVM"
             x:Class="XF_MVVM.MainPage">
    <StackLayout VerticalOptions="Center">
        <Entry Placeholder="ユーザーIDを入力してください"
               IsVisible="{Binding IsLogin}"/>
        <Entry Placeholder="パスワードを入力してください"
               IsVisible="{Binding IsLogin}"/>
        <Button Text="ログインする"
                IsVisible="{Binding IsLogin}"
                Command="{Binding LoginLogout}"/>  <!-- 追加 -->
        <Button Text="ログアウトする"
                IsVisible="{Binding IsLogout}"
                Command="{Binding LoginLogout}"/>  <!-- 追加 -->
    </StackLayout>
</ContentPage>

クリック時の処理をコードビハインド以外に記述する場合はClickedが使えないので代わりにCommandを使います。
  
*Commandについて詳しくは以下をご覧ください docs.microsoft.com   

  

MainPage.xaml.cs

using Xamarin.Forms;

namespace XF_MVVM
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
            BindingContext = new MainPageViewModel();
        }
    }
}

  

MainPageViewModel.cs

using System.ComponentModel;
using Xamarin.Forms;

namespace XF_MVVM
{
    public class MainPageViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private bool isLogin = true;
        public bool IsLogin
        {
            get { return isLogin; }
            set
            {
                isLogin = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLogin)));
            }
        }

        private bool isLogout;
        public bool IsLogout
        {
            get { return this.isLogout; }
            set
            {
                isLogout = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLogout)));
            }
        }

        public Command LoginLogout { get; }

        public MainPageViewModel()
        {
            LoginLogout = new Command(() =>
            {
                IsLogin = !IsLogin;
                IsLogout = !IsLogout;
            });
        }
    }
}

コードビハインドに書いていたControlクラスに変わってMainPageViewModelクラスを新しく作ります。
そしてMainPage.xaml.csMainPageViewModelクラスをBindingContextに指定すれば完了です。これによってコードビハインド(MainPage.xaml.cs)がスッキリしましたね。

まとめ

以上のように役割によってクラスを分けると理解しやすいコードになるのではないかと思いました。今まではコードビハインドにほとんどのコードを書いていたのですが、ファイルを分けた方が自分の頭が混乱しなくていいですね。

参考サイト

今さら入門するMVVMに必要な技術要素(Xamarin.Forms & UWP) - かずきのBlog@hatena

Xamarin.Forms のデータ バインディング - Xamarin | Microsoft Docs