次の方法で共有


データ バインディングと MVVM

サンプルを参照します。 サンプルを参照する

Model-View-ViewModel (MVVM) パターンでは、XAML ユーザー インターフェイス (ビュー)、基になるデータ (モデル)、ビューとモデル間の仲介 (ビューモデル) という 3 つのソフトウェア レイヤー間の分離が強制されます。 ビューとビューモデルは、多くの場合、XAML で定義されているデータ バインディングを介して接続されます。 ビューの BindingContext は通常、ビューモデルのインスタンスです。

重要

.NET マルチプラットフォーム アプリ UI (.NET MAUI) は、UI スレッドへのバインドの更新をマーシャリングします。 MVVM を使用すると、.NET MAUI のバインド エンジンによって UI スレッドに更新が適用され、データ バインド ビューモデル プロパティを任意のスレッドから更新できます。

MVVM パターンを実装するには複数の方法があり、この記事では単純なアプローチに焦点を当てています。 ビューとビューモデルを使用しますが、モデルは使用せず、2 つのレイヤー間のデータ バインディングに焦点を当てます。 .NET MAUI で MVVM パターンを使用する方法の詳細については、「.NET MAUI を使用したエンタープライズ アプリケーション パターン」の「Model-View-ViewModel (MVVM)」を参照してください。 MVVM パターンの実装に役立つチュートリアルについては、「MVVM の概念を使用してアプリをアップグレードする」を参照してください。

単純な MVVM

XAML マークアップ拡張機能では、XAML ファイルが他のアセンブリ内のクラスを参照できるように、新しい XML 名前空間宣言を定義する方法について説明しました。 次の例では、x:Static マークアップ拡張機能を使用して、System 名前空間の静的 DateTime.Now プロパティから現在の日付と時刻を取得します。

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:sys="clr-namespace:System;assembly=netstandard"
             x:Class="XamlSamples.OneShotDateTimePage"
             Title="One-Shot DateTime Page">

    <VerticalStackLayout BindingContext="{x:Static sys:DateTime.Now}"
                         Spacing="25" Padding="30,0"
                         VerticalOptions="Center" HorizontalOptions="Center">

        <Label Text="{Binding Year, StringFormat='The year is {0}'}" />
        <Label Text="{Binding StringFormat='The month is {0:MMMM}'}" />
        <Label Text="{Binding Day, StringFormat='The day is {0}'}" />
        <Label Text="{Binding StringFormat='The time is {0:T}'}" />

    </VerticalStackLayout>

</ContentPage>

この例では、取得した DateTime 値は StackLayoutBindingContext として設定されます。 要素に BindingContext を設定すると、その要素のすべての子によって継承されます。 これは、BindingContext のすべての子に同じ StackLayout があり、そのオブジェクトのプロパティへのバインドを含むことができることを意味します。

日付と時刻を表示するページのスクリーンショット。

ただし、問題は、ページが構築および初期化されるときに日付と時刻が 1 回設定され、変更されないということです。

警告

BindableObject から派生するクラスでは、BindableProperty 型のプロパティのみがバインド可能です。 たとえば、VisualElement.IsLoadedElement.Parent はバインドできません。

XAML ページでは、常に現在の時刻を表示するクロックを表示できますが、追加のコードが必要です。 MVVM パターンは、ビジュアル オブジェクトと基になるデータの間のプロパティからのデータ バインディングの場合、.NET MAUI アプリにとって自然な選択肢です。 MVVM の観点から考えると、モデルとビューモデルは完全にコードで記述されたクラスです。 ビューは、多くの場合、データ バインディングを介してビューモデルで定義されたプロパティを参照する XAML ファイルです。 MVVM では、モデルはビューモデルを認識せず、ビューモデルはビューを認識しません。 ただし、多くの場合、ビューモデルによって公開される型を、UI に関連付けられている型に合わせて調整します。

ここに示すような MVVM の単純な例では、多くの場合、モデルがまったくなく、パターンにはデータ バインディングとリンクされたビューとビューモデルだけが含まれます。

次の例は、1 秒ごとに更新される DateTime という名前の 1 つのプロパティがある、クロックのビューモデルを示しています。

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace XamlSamples;

class ClockViewModel: INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private DateTime _dateTime;
    private Timer _timer;

    public DateTime DateTime
    {
        get => _dateTime;
        set
        {
            if (_dateTime != value)
            {
                _dateTime = value;
                OnPropertyChanged(); // reports this property
            }
        }
    }

    public ClockViewModel()
    {
        this.DateTime = DateTime.Now;

        // Update the DateTime property every second.
        _timer = new Timer(new TimerCallback((s) => this.DateTime = DateTime.Now),
                           null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
    }

    ~ClockViewModel() =>
        _timer.Dispose();

    public void OnPropertyChanged([CallerMemberName] string name = "") =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

通常、ビューモデルは INotifyPropertyChanged インターフェイスを実装します。このインターフェイスを使用すると、クラスのプロパティのいずれかが変更されるたびに PropertyChanged イベントを発生させる機能が提供されます。 .NET MAUI のデータ バインディング メカニズムは、ハンドラーをこの PropertyChanged イベントにアタッチして、プロパティが変更されたときに通知を受け取り、ターゲットを新しい値で更新し続けることができます。 前のコード例では、OnPropertyChanged メソッドはイベントの発生を処理しながら、プロパティソース名 DateTime を自動的に決定します。

次の例は、ClockViewModel を使用する XAML を示しています。

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             x:Class="XamlSamples.ClockPage"
             Title="Clock Page">
    <ContentPage.BindingContext>
        <local:ClockViewModel />
    </ContentPage.BindingContext>

    <Label Text="{Binding DateTime, StringFormat='{0:T}'}"
           FontSize="18"
           HorizontalOptions="Center"
           VerticalOptions="Center" />
</ContentPage>

この例では、ClockViewModel はプロパティ要素タグを使って ContentPageBindingContext に設定されます。 または、分離コード ファイルでビューモデルをインスタンス化することもできます。

LabelText プロパティの Binding マークアップ拡張機能によって、DateTime のプロパティを書式設定できます。 次のスクリーンショットは、結果を示しています。

ビューモデルを使用して日付と時刻を表示するページのスクリーンショット。

さらに、プロパティをピリオドで区切ることで、ビューモデルの DateTime プロパティの個々のプロパティにアクセスできます。

<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >

対話的な MVVM

MVVM は、多くの場合、基になるデータ モデルに基づく対話型ビューの双方向データ バインディングで使用されます。

次の例は、Color 値を HueSaturationLuminosity の値に変換し、再度変換する HslViewModel を示しています。

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace XamlSamples;

class HslViewModel: INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private float _hue, _saturation, _luminosity;
    private Color _color;

    public float Hue
    {
        get => _hue;
        set
        {
            if (_hue != value)
                Color = Color.FromHsla(value, _saturation, _luminosity);
        }
    }

    public float Saturation
    {
        get => _saturation;
        set
        {
            if (_saturation != value)
                Color = Color.FromHsla(_hue, value, _luminosity);
        }
    }

    public float Luminosity
    {
        get => _luminosity;
        set
        {
            if (_luminosity != value)
                Color = Color.FromHsla(_hue, _saturation, value);
        }
    }

    public Color Color
    {
        get => _color;
        set
        {
            if (_color != value)
            {
                _color = value;
                _hue = _color.GetHue();
                _saturation = _color.GetSaturation();
                _luminosity = _color.GetLuminosity();

                OnPropertyChanged("Hue");
                OnPropertyChanged("Saturation");
                OnPropertyChanged("Luminosity");
                OnPropertyChanged(); // reports this property
            }
        }
    }

    public void OnPropertyChanged([CallerMemberName] string name = "") =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

この例では、HueSaturationLuminosity プロパティを変更すると Color プロパティが変更され、Color プロパティを変更すると他の 3 つのプロパティが変更されます。 プロパティが変更されない限り、ビューモデルが PropertyChanged イベントを呼び出さない点を除いて、これは無限ループのように見える場合があります。

次の XAML の例では、ビューモデルの Color プロパティに Color プロパティがバインドされた BoxView と、HueSaturationLuminosity プロパティにバインドされた 3 つの Slider と 3 つの Label ビューが含まれています。

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             x:Class="XamlSamples.HslColorScrollPage"
             Title="HSL Color Scroll Page">
    <ContentPage.BindingContext>
        <local:HslViewModel Color="Aqua" />
    </ContentPage.BindingContext>

    <VerticalStackLayout Padding="10, 0, 10, 30">
        <BoxView Color="{Binding Color}"
                 HeightRequest="100"
                 WidthRequest="100"
                 HorizontalOptions="Center" />
        <Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}"
               HorizontalOptions="Center" />
        <Slider Value="{Binding Hue}"
                Margin="20,0,20,0" />
        <Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}"
               HorizontalOptions="Center" />
        <Slider Value="{Binding Saturation}"
                Margin="20,0,20,0" />
        <Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"
               HorizontalOptions="Center" />
        <Slider Value="{Binding Luminosity}"
                Margin="20,0,20,0" />
    </VerticalStackLayout>
</ContentPage>

それぞれの Label のバインドは、既定の OneWay です。 値を表示するだけで済みます。 ただし、それぞれの Slider の既定のバインドは TwoWay です。 これにより、ビューモデルから Slider を初期化できます。 ビューモデルがインスタンス化されると、その Color プロパティは Aqua に設定されます。 Slider の変更により、ビューモデル内のプロパティの新しい値が設定され、新しい色が計算されます。

双方向データ バインディングを使用する MVVM

コマンド実行

アプリには、プロパティのバインディングを超えて、ユーザーにビューモデルに何らかの影響を与えるコマンドを開始させることが必要な場合があります。 通常、このようなコマンドはボタンのクリックや指のタップによって通知され、従来は、ButtonClicked のイベントまたは TapGestureRecognizerTapped イベントに対するハンドラーの分離コード ファイル内で処理されます。

コマンド実行インターフェイスでは、MVVM アーキテクチャにいっそうよく適した代わりのコマンド実装方法が提供されます。 ビューモデル自体にコマンドを含めることができます。その場合のコマンドは、Button クリックのようなビュー内の特定のアクティビティに対応して実行されるメソッドです。 データ バインディングは、これらのコマンドと Button の間で定義されます。

Button とビューモデル間のデータ バインディングを可能にするには、Button で 2 つのプロパティを定義します。

他の多くのコントロールも CommandCommandParameter プロパティを定義します。

ICommand インターフェイスは、System.Windows.Input 名前空間内で定義されており、2 つのメソッドと 1 つのイベントで構成されます。

  • void Execute(object arg)
  • bool CanExecute(object arg)
  • event EventHandler CanExecuteChanged

ビューモデルでは、型 ICommand のプロパティを定義できます。 その後、これらのプロパティを各 ButtonCommand プロパティ、または他の要素のプロパティ、またはこのインターフェイスを実装するカスタム ビューにバインドできます。 必要に応じて、このビューモデル プロパティにバインドされている個々の Button オブジェクト (または他の要素) を識別するように CommandParameter プロパティを設定できます。 内部的には、Button はユーザーが Button をタップするたびに Execute メソッドを呼び出し、Execute メソッドに CommandParameter を渡します。

CanExecute メソッドと CanExecuteChanged イベントは、Button タップが現在無効かもしれない場合に使用されます、その場合、Button はそれ自体を無効にする必要があります。 Buttonは、Command プロパティが最初に設定されたときと、CanExecuteChanged イベントが発生したときに、CanExecute を呼び出します。 CanExecutefalse を返した場合、Button はそれ自体を無効にし、Execute 呼び出しを発生させません。

.NET MAUI に含まれている Command クラスまたは Command<T> クラスを使用して、ICommand インターフェイスを実装できます。 これらの 2 つのクラスは、いくつかのコンストラクタと、Command オブジェクトに CanExecuteChanged イベントを発生させるためにビューモデルが呼び出す ChangeCanExecute メソッドを定義します。

次の例は、電話番号の入力を目的とした単純なキーパッドのビューモデルを示しています。

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace XamlSamples;

class KeypadViewModel: INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string _inputString = "";
    private string _displayText = "";
    private char[] _specialChars = { '*', '#' };

    public ICommand AddCharCommand { get; private set; }
    public ICommand DeleteCharCommand { get; private set; }

    public string InputString
    {
        get => _inputString;
        private set
        {
            if (_inputString != value)
            {
                _inputString = value;
                OnPropertyChanged();
                DisplayText = FormatText(_inputString);

                // Perhaps the delete button must be enabled/disabled.
                ((Command)DeleteCharCommand).ChangeCanExecute();
            }
        }
    }

    public string DisplayText
    {
        get => _displayText;
        private set
        {
            if (_displayText != value)
            {
                _displayText = value;
                OnPropertyChanged();
            }
        }
    }

    public KeypadViewModel()
    {
        // Command to add the key to the input string
        AddCharCommand = new Command<string>((key) => InputString += key);

        // Command to delete a character from the input string when allowed
        DeleteCharCommand =
            new Command(
                // Command will strip a character from the input string
                () => InputString = InputString.Substring(0, InputString.Length - 1),

                // CanExecute is processed here to return true when there's something to delete
                () => InputString.Length > 0
            );
    }

    string FormatText(string str)
    {
        bool hasNonNumbers = str.IndexOfAny(_specialChars) != -1;
        string formatted = str;

        // Format the string based on the type of data and the length
        if (hasNonNumbers || str.Length < 4 || str.Length > 10)
        {
            // Special characters exist, or the string is too small or large for special formatting
            // Do nothing
        }

        else if (str.Length < 8)
            formatted = string.Format("{0}-{1}", str.Substring(0, 3), str.Substring(3));

        else
            formatted = string.Format("({0}) {1}-{2}", str.Substring(0, 3), str.Substring(3, 3), str.Substring(6));

        return formatted;
    }


    public void OnPropertyChanged([CallerMemberName] string name = "") =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

この例では、コマンドの Execute メソッドと CanExecute メソッドがコンストラクターでラムダ関数として定義されています。 ビューモデルは、AddCharCommand プロパティが複数のボタン (またはコマンド インターフェイスを持つ他のコントロール) の Command プロパティにバインドされており、各ボタンが CommandParameter で識別されていると想定しています。 これらのボタンによって InputString プロパティに文字が追加され、DisplayText プロパティの電話番号として書式設定されます。 DeleteCharCommand という名前の型 ICommand の 2 番目のプロパティもあります。 これはバックスペース ボタンにバインドされますが、削除する文字がない場合は、ボタンを無効にする必要があります。

次の例は、KeypadViewModel を使用する XAML を示します。

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             x:Class="XamlSamples.KeypadPage"
             Title="Keypad Page">
    <ContentPage.BindingContext>
        <local:KeypadViewModel />
    </ContentPage.BindingContext>

    <Grid HorizontalOptions="Center" VerticalOptions="Center">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="80" />
            <ColumnDefinition Width="80" />
            <ColumnDefinition Width="80" />
        </Grid.ColumnDefinitions>

        <Label Text="{Binding DisplayText}"
               Margin="0,0,10,0" FontSize="20" LineBreakMode="HeadTruncation"
               VerticalTextAlignment="Center" HorizontalTextAlignment="End"
               Grid.ColumnSpan="2" />

        <Button Text="&#x21E6;" Command="{Binding DeleteCharCommand}" Grid.Column="2"/>

        <Button Text="1" Command="{Binding AddCharCommand}" CommandParameter="1" Grid.Row="1" />
        <Button Text="2" Command="{Binding AddCharCommand}" CommandParameter="2" Grid.Row="1" Grid.Column="1" />
        <Button Text="3" Command="{Binding AddCharCommand}" CommandParameter="3" Grid.Row="1" Grid.Column="2" />

        <Button Text="4" Command="{Binding AddCharCommand}" CommandParameter="4" Grid.Row="2" />
        <Button Text="5" Command="{Binding AddCharCommand}" CommandParameter="5" Grid.Row="2" Grid.Column="1" />
        <Button Text="6" Command="{Binding AddCharCommand}" CommandParameter="6" Grid.Row="2" Grid.Column="2" />

        <Button Text="7" Command="{Binding AddCharCommand}" CommandParameter="7" Grid.Row="3" />
        <Button Text="8" Command="{Binding AddCharCommand}" CommandParameter="8" Grid.Row="3" Grid.Column="1" />
        <Button Text="9" Command="{Binding AddCharCommand}" CommandParameter="9" Grid.Row="3" Grid.Column="2" />

        <Button Text="*" Command="{Binding AddCharCommand}" CommandParameter="*" Grid.Row="4" />
        <Button Text="0" Command="{Binding AddCharCommand}" CommandParameter="0" Grid.Row="4" Grid.Column="1" />
        <Button Text="#" Command="{Binding AddCharCommand}" CommandParameter="#" Grid.Row="4" Grid.Column="2" />
    </Grid>
</ContentPage>

この例では、DeleteCharCommand にバインドされている最初の ButtonCommand プロパティです。 その他のボタンは、Button に表示される文字と同じ CommandParameterAddCharCommand にバインドされています。

MVVM とコマンドを使用した電卓のスクリーンショット。