Model-View-ViewModel (MVVM) パターンを使用する .NET マルチプラットフォーム アプリ UI (.NET MAUI) アプリでは、データ バインディングはビューモデル内のプロパティ (通常は INotifyPropertyChangedから派生するクラス) とビュー内のプロパティ (通常は XAML ファイル) の間で定義されます。 場合によっては、ビューモデル内の何かに影響を与えるコマンドをユーザーに開始するようユーザーに要求することで、これらのプロパティ バインドを超える必要がある場合があります。 これらのコマンドは通常、ボタンのクリックまたは指のタップによってトリガーされ、従来は、Clicked の Button イベントまたは Tapped の TapGestureRecognizer イベントのハンドラーがコードビハインドファイルで処理されます。
コマンド 実行インターフェイスは、MVVM アーキテクチャにはるかに適したコマンドを実装するための代替アプローチを提供します。 ビューモデルにはコマンドを含めることができます。これは、ビュー内の特定のアクティビティ ( Button クリックなど) に反応して実行されるメソッドです。 データ バインディングは、これらのコマンドと Buttonの間で定義されます。
Buttonとビューモデルの間のデータ バインディングを許可するために、Buttonは次の 2 つのプロパティを定義します。
-
Command型System.Windows.Input.ICommand -
CommandParameter型Object
コマンド インターフェイスを使用するには、ソースがCommand型のビューモデルのプロパティであるButtonのICommand プロパティを対象とするデータ バインディングを定義します。 ビューモデルには、ボタンがクリックされたときに実行される ICommand プロパティに関連付けられているコードが含まれています。
CommandParameter プロパティを任意のデータに設定して、複数のボタンがすべてビューモデル内の同じICommand プロパティにバインドされている場合は、複数のボタンを区別できます。
他の多くのビューでは、 Command プロパティと CommandParameter プロパティも定義されています。 これらのコマンドはすべて、ビュー内のユーザー インターフェイス オブジェクトに依存しないアプローチを使用して、ビューモデル内で処理できます。
ICommands
ICommand インターフェイスは System.Windows.Input 名前空間で定義され、2 つのメソッドと 1 つのイベントで構成されます。
public interface ICommand
{
public void Execute (Object parameter);
public bool CanExecute (Object parameter);
public event EventHandler CanExecuteChanged;
}
コマンド インターフェイスを使用するには、viewmodel に ICommand型のプロパティが含まれている必要があります。
public ICommand MyCommand { private set; get; }
ビューモデルは、 ICommand インターフェイスを実装するクラスも参照する必要があります。 ビューでは、CommandのButton プロパティはそのプロパティにバインドされます。
<Button Text="Execute command"
Command="{Binding MyCommand}" />
ユーザーがButtonを押すと、Buttonは、Execute プロパティにバインドされたICommand オブジェクトのCommand メソッドを呼び出します。
バインディングが最初にCommandのButton プロパティで定義され、データ バインディングが何らかの方法で変更されると、ButtonはCanExecute オブジェクトのICommand メソッドを呼び出します。
CanExecuteがfalseを返す場合、Buttonはそれ自体を無効にします。 これは、特定のコマンドが現在使用できないか無効であることを示します。
Buttonは、CanExecuteChangedのICommand イベントにもハンドラーをアタッチします。 イベントは、 CanExecute 結果に影響する条件が変化するたびに、ビューモデル内から手動で発生する必要があります。 そのイベントが発生すると、 Button は再び CanExecute 呼び出します。
Buttonは、CanExecuteがtrueを返した場合に自分自身を有効にし、CanExecuteがfalseを返した場合に自分自身を無効にします。
Important
一部の UI フレームワーク (WPF など) とは異なり、.NET MAUI は、 CanExecute の戻り値がいつ変更されるかを自動的に検出しません。
CanExecuteChangedの結果に影響する条件が変更されるたびに、ChangeCanExecute() イベントを手動で発生させる (または Command クラスでCanExecuteを呼び出す) 必要があります。 これは通常、 CanExecute 依存するプロパティが変更されたときに行われます。
注
また、IsEnabled メソッドの代わりにButtonのCanExecute プロパティを使用することも、それと組み合わせて使用することもできます。 .NET MAUI 7 以前では、IsEnabled メソッドの戻り値が常に Button プロパティを上書きするため、コマンド インターフェイスの使用中にCanExecuteのIsEnabled プロパティを使用できませんでした。 これは .NET MAUI 8 以降で修正されています。 IsEnabled プロパティは、コマンド ベースの Buttonで使用できるようになりました。 ただし、IsEnabledを有効にするには、CanExecute プロパティと メソッドのButtonが true を返す必要があることに注意してください (また、親コントロールも有効にする必要があります)。
ビューモデルで ICommand型のプロパティを定義する場合、ビューモデルには、 ICommand インターフェイスを実装するクラスも含まれているか参照する必要があります。 このクラスには、ExecuteメソッドとCanExecute メソッドを含めるか参照する必要があります。また、CanExecuteChanged メソッドが別の値を返す場合は常に、CanExecute イベントを手動で発生する必要があります。 .NET MAUI に含まれる Command クラスまたは Command<T> クラスを使用して、 ICommand インターフェイスを実装できます。 これらのクラスを使用すると、クラス コンストラクターで Execute メソッドと CanExecute メソッドの本体を指定できます。
ヒント
Command<T>は、CommandParameter プロパティを使用して、同じICommand プロパティにバインドされた複数のビューと、それが要件でない場合は Command クラスを区別する場合に使用します。
基本的なコマンド
次の例では、ビューモデルに実装される基本的なコマンドを示します。
PersonViewModel クラスは、ユーザーを定義する Name、Age、Skillsという名前の 3 つのプロパティを定義します。
public class PersonViewModel : INotifyPropertyChanged
{
string name;
double age;
string skills;
public event PropertyChangedEventHandler PropertyChanged;
public string Name
{
set { SetProperty(ref name, value); }
get { return name; }
}
public double Age
{
set { SetProperty(ref age, value); }
get { return age; }
}
public string Skills
{
set { SetProperty(ref skills, value); }
get { return skills; }
}
public override string ToString()
{
return Name + ", age " + Age;
}
bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Object.Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
次に示す PersonCollectionViewModel クラスは、 PersonViewModel 型の新しいオブジェクトを作成し、ユーザーがデータを入力できるようにします。 そのため、クラスでは、 IsEditing型の boolと PersonEdit、 PersonViewModel型のプロパティを定義します。 さらに、このクラスでは、ICommand型の 3 つのプロパティと、Persons型のIList<PersonViewModel>という名前のプロパティを定義します。
public class PersonCollectionViewModel : INotifyPropertyChanged
{
PersonViewModel personEdit;
bool isEditing;
public event PropertyChangedEventHandler PropertyChanged;
···
public bool IsEditing
{
private set { SetProperty(ref isEditing, value); }
get { return isEditing; }
}
public PersonViewModel PersonEdit
{
set { SetProperty(ref personEdit, value); }
get { return personEdit; }
}
public ICommand NewCommand { private set; get; }
public ICommand SubmitCommand { private set; get; }
public ICommand CancelCommand { private set; get; }
public IList<PersonViewModel> Persons { get; } = new ObservableCollection<PersonViewModel>();
bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Object.Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
この例では、3 つの ICommand プロパティと Persons プロパティを変更しても、 PropertyChanged イベントは発生しません。 これらのプロパティはすべて、クラスが最初に作成されるときに設定され、変更されません。
次の例は、 PersonCollectionViewModelを使用する XAML を示しています。
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:DataBindingDemos"
x:Class="DataBindingDemos.PersonEntryPage"
Title="Person Entry"
x:DataType="local:PersonCollectionViewModel">
<ContentPage.BindingContext>
<local:PersonCollectionViewModel />
</ContentPage.BindingContext>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- New Button -->
<Button Text="New"
Grid.Row="0"
Command="{Binding NewCommand}"
HorizontalOptions="Start" />
<!-- Entry Form -->
<Grid Grid.Row="1"
IsEnabled="{Binding IsEditing}">
<Grid x:DataType="local:PersonViewModel"
BindingContext="{Binding PersonEdit}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Text="Name: " Grid.Row="0" Grid.Column="0" />
<Entry Text="{Binding Name}"
Grid.Row="0" Grid.Column="1" />
<Label Text="Age: " Grid.Row="1" Grid.Column="0" />
<StackLayout Orientation="Horizontal"
Grid.Row="1" Grid.Column="1">
<Stepper Value="{Binding Age}"
Maximum="100" />
<Label Text="{Binding Age, StringFormat='{0} years old'}"
VerticalOptions="Center" />
</StackLayout>
<Label Text="Skills: " Grid.Row="2" Grid.Column="0" />
<Entry Text="{Binding Skills}"
Grid.Row="2" Grid.Column="1" />
</Grid>
</Grid>
<!-- Submit and Cancel Buttons -->
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Text="Submit"
Grid.Column="0"
Command="{Binding SubmitCommand}"
VerticalOptions="Center" />
<Button Text="Cancel"
Grid.Column="1"
Command="{Binding CancelCommand}"
VerticalOptions="Center" />
</Grid>
<!-- List of Persons -->
<ListView Grid.Row="3"
ItemsSource="{Binding Persons}" />
</Grid>
</ContentPage>
この例では、ページの BindingContext プロパティが PersonCollectionViewModelに設定されています。
Gridには、「New」というテキストを持つButtonが含まれており、そのプロパティはビュー・モデルのCommandプロパティにバインドされています。また、NewCommandプロパティにバインドされたエントリ フォームやIsEditingのプロパティ、およびビュー・モデルのPersonViewModelプロパティとSubmitCommandプロパティにバインドされた2つのボタンも含まれています。
ListViewには、既に入力されているユーザーのコレクションが表示されます。
次のスクリーンショットは、年齢が設定された後に有効になっている [送信] ボタンを示しています。
ユーザーが最初に [新規 ] ボタンを押すと、入力フォームは有効になりますが、[ 新規 ] ボタンは無効になります。 その後、ユーザーは名前、年齢、スキルを入力します。 編集中はいつでも、ユーザーは [キャンセル ] ボタンを押してやり直すことができます。 名前と有効な年齢が入力されている場合にのみ、[ 送信] ボタンが有効になります。 この [送信] ボタンを押すと、ユーザーは ListViewによって表示されるコレクションに転送されます。 [キャンセル] または [送信] ボタンを押すと、入力フォームがクリアされ、[新規] ボタンが再度有効になります。
[新規]、[送信]、[キャンセル] ボタンのすべてのロジックは、PersonCollectionViewModel、NewCommand、およびSubmitCommandプロパティの定義を使用して、CancelCommandで処理されます。
PersonCollectionViewModelのコンストラクターは、これら 3 つのプロパティをCommand型のオブジェクトに設定します。
Command クラスのコンストラクターを使用すると、ActionメソッドとFunc<bool>メソッドに対応するExecute型とCanExecuteの引数を渡すことができます。 このアクションと関数は、 Command コンストラクターでラムダ関数として定義できます。
public class PersonCollectionViewModel : INotifyPropertyChanged
{
···
public PersonCollectionViewModel()
{
NewCommand = new Command(
execute: () =>
{
PersonEdit = new PersonViewModel();
PersonEdit.PropertyChanged += OnPersonEditPropertyChanged;
IsEditing = true;
RefreshCanExecutes();
},
canExecute: () =>
{
return !IsEditing;
});
···
}
void OnPersonEditPropertyChanged(object sender, PropertyChangedEventArgs args)
{
(SubmitCommand as Command).ChangeCanExecute();
}
void RefreshCanExecutes()
{
(NewCommand as Command).ChangeCanExecute();
(SubmitCommand as Command).ChangeCanExecute();
(CancelCommand as Command).ChangeCanExecute();
}
···
}
ユーザーが [新規] ボタンをクリックすると、execute コンストラクターに渡されたCommand関数が実行されます。 これにより、新しい PersonViewModel オブジェクトが作成され、そのオブジェクトの PropertyChanged イベントにハンドラーが設定され、 IsEditing が trueに設定され、コンストラクターの後に定義された RefreshCanExecutes メソッドが呼び出されます。
ICommand インターフェイスの実装に加えて、Command クラスでは ChangeCanExecute という名前のメソッドも定義されます。 ビューモデルは、ChangeCanExecute メソッドの戻り値を変更する可能性のある何らかのことが発生するたびに、ICommand プロパティのCanExecuteを呼び出す必要があります。
ChangeCanExecuteを呼び出すと、Command クラスで CanExecuteChanged イベントが発生します。
Buttonはそのイベントのハンドラーをアタッチし、CanExecuteを再度呼び出して応答し、そのメソッドの戻り値に基づいて自身を有効にします。
executeのNewCommandメソッドがRefreshCanExecutesを呼び出すと、NewCommand プロパティはChangeCanExecuteの呼び出しを取得し、Buttonは canExecute メソッドを呼び出します。false プロパティがIsEditingされたため、trueが返されるようになりました。
新しいPropertyChanged オブジェクトのPersonViewModel ハンドラーは、ChangeCanExecuteのSubmitCommand メソッドを呼び出します。
public class PersonCollectionViewModel : INotifyPropertyChanged
{
···
public PersonCollectionViewModel()
{
···
SubmitCommand = new Command(
execute: () =>
{
Persons.Add(PersonEdit);
PersonEdit.PropertyChanged -= OnPersonEditPropertyChanged;
PersonEdit = null;
IsEditing = false;
RefreshCanExecutes();
},
canExecute: () =>
{
return PersonEdit != null &&
PersonEdit.Name != null &&
PersonEdit.Name.Length > 1 &&
PersonEdit.Age > 0;
});
···
}
···
}
canExecuteのSubmitCommand関数は、編集中のPersonViewModel オブジェクトでプロパティが変更されるたびに呼び出されます。
true プロパティが 1 文字以上で、Nameが 0 より大きい場合にのみ、Ageが返されます。 その時点で、[ 送信 ] ボタンが有効になります。
execute の関数は、プロパティ変更ハンドラーをPersonViewModelから削除し、オブジェクトを Persons コレクションに追加して、すべてを初期状態に戻します。
execute] ボタンの関数は、[送信] ボタンが行うすべての処理を行います。ただし、コレクションにオブジェクトを追加する必要はありません。
public class PersonCollectionViewModel : INotifyPropertyChanged
{
···
public PersonCollectionViewModel()
{
···
CancelCommand = new Command(
execute: () =>
{
PersonEdit.PropertyChanged -= OnPersonEditPropertyChanged;
PersonEdit = null;
IsEditing = false;
RefreshCanExecutes();
},
canExecute: () =>
{
return IsEditing;
});
}
···
}
canExecute メソッドは、trueが編集されるたびにPersonViewModelを返します。
注
executeメソッドとcanExecuteメソッドをラムダ関数として定義する必要はありません。 ビューモデルでプライベート メソッドとして記述し、 Command コンストラクターで参照できます。 ただし、この方法では、ビューモデルで 1 回だけ参照される多くのメソッドが発生する可能性があります。
コマンド パラメーターの使用
1 つ以上のボタンや他のユーザー インターフェイス オブジェクトが、ビューモデルで同じ ICommand プロパティを共有すると便利な場合があります。 この場合は、 CommandParameter プロパティを使用してボタンを区別できます。
これらの共有Commandプロパティには、引き続き ICommand クラスを使用できます。 このクラスは、execute型のパラメーターを持つcanExecuteメソッドとObject メソッドを受け入れる代替コンストラクターを定義します。 これにより、 CommandParameter がこれらのメソッドに渡されます。 ただし、 CommandParameterを指定する場合は、ジェネリック Command<T> クラスを使用して、 CommandParameterするオブジェクト セットの型を指定するのが最も簡単です。 指定する execute メソッドと canExecute メソッドには、その型のパラメーターがあります。
次の例では、10 進数を入力するためのキーボードを示します。
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:DataBindingDemos"
x:Class="DataBindingDemos.DecimalKeypadPage"
Title="Decimal Keyboard"
x:DataType="local:DecimalKeypadViewModel">
<ContentPage.BindingContext>
<local:DecimalKeypadViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<Style TargetType="Button">
<Setter Property="FontSize" Value="32" />
<Setter Property="BorderWidth" Value="1" />
<Setter Property="BorderColor" Value="Black" />
</Style>
</ContentPage.Resources>
<Grid WidthRequest="240"
HeightRequest="480"
ColumnDefinitions="80, 80, 80"
RowDefinitions="Auto, Auto, Auto, Auto, Auto, Auto"
ColumnSpacing="2"
RowSpacing="2"
HorizontalOptions="Center"
VerticalOptions="Center">
<Label Text="{Binding Entry}"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"
Margin="0,0,10,0"
FontSize="32"
LineBreakMode="HeadTruncation"
VerticalTextAlignment="Center"
HorizontalTextAlignment="End" />
<Button Text="CLEAR"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
Command="{Binding ClearCommand}" />
<Button Text="⇦"
Grid.Row="1" Grid.Column="2"
Command="{Binding BackspaceCommand}" />
<Button Text="7"
Grid.Row="2" Grid.Column="0"
Command="{Binding DigitCommand}"
CommandParameter="7" />
<Button Text="8"
Grid.Row="2" Grid.Column="1"
Command="{Binding DigitCommand}"
CommandParameter="8" />
<Button Text="9"
Grid.Row="2" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="9" />
<Button Text="4"
Grid.Row="3" Grid.Column="0"
Command="{Binding DigitCommand}"
CommandParameter="4" />
<Button Text="5"
Grid.Row="3" Grid.Column="1"
Command="{Binding DigitCommand}"
CommandParameter="5" />
<Button Text="6"
Grid.Row="3" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="6" />
<Button Text="1"
Grid.Row="4" Grid.Column="0"
Command="{Binding DigitCommand}"
CommandParameter="1" />
<Button Text="2"
Grid.Row="4" Grid.Column="1"
Command="{Binding DigitCommand}"
CommandParameter="2" />
<Button Text="3"
Grid.Row="4" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="3" />
<Button Text="0"
Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="2"
Command="{Binding DigitCommand}"
CommandParameter="0" />
<Button Text="·"
Grid.Row="5" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="." />
</Grid>
</ContentPage>
この例では、ページの BindingContext は DecimalKeypadViewModelです。 このビューモデルのEntry プロパティは、TextのLabel プロパティにバインドされます。 すべての Button オブジェクトは、 ClearCommand、 BackspaceCommand、および DigitCommandのビューモデル内のコマンドにバインドされます。 10 桁の数字と小数点の 11 個のボタンは、 DigitCommandへのバインドを共有します。
CommandParameterでは、これらのボタンが区別されます。
CommandParameterに設定された値は、通常、小数点を除いてボタンによって表示されるテキストと同じです。小数点は、わかりやすくするために中央のドット文字で表示されます。
DecimalKeypadViewModelでは、Entry型のstring プロパティと、ICommand型の 3 つのプロパティを定義します。
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
string entry = "0";
public event PropertyChangedEventHandler PropertyChanged;
···
public string Entry
{
private set
{
if (entry != value)
{
entry = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Entry"));
}
}
get
{
return entry;
}
}
public ICommand ClearCommand { private set; get; }
public ICommand BackspaceCommand { private set; get; }
public ICommand DigitCommand { private set; get; }
}
ClearCommandに対応するボタンは常に有効になり、エントリが "0" に戻ります。
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
···
public DecimalKeypadViewModel()
{
ClearCommand = new Command(
execute: () =>
{
Entry = "0";
RefreshCanExecutes();
});
···
}
void RefreshCanExecutes()
{
((Command)BackspaceCommand).ChangeCanExecute();
((Command)DigitCommand).ChangeCanExecute();
}
···
}
ボタンは常に有効になっているため、canExecute コンストラクターでCommand引数を指定する必要はありません。
Backspace ボタンは、エントリの長さが 1 より大きい場合、またはEntryが文字列 "0" と等しくない場合にのみ有効になります。
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
···
public DecimalKeypadViewModel()
{
···
BackspaceCommand = new Command(
execute: () =>
{
Entry = Entry.Substring(0, Entry.Length - 1);
if (Entry == "")
{
Entry = "0";
}
RefreshCanExecutes();
},
canExecute: () =>
{
return Entry.Length > 1 || Entry != "0";
});
···
}
···
}
execute ボタンの関数のロジックにより、Entryが少なくとも "0" の文字列になります。
DigitCommand プロパティは 11 個のボタンにバインドされ、それぞれが CommandParameter プロパティを使用して自身を識別します。
DigitCommandは、Command<T> クラスのインスタンスに設定されます。 XAML でコマンド インターフェイスを使用する場合、 CommandParameter プロパティは通常、ジェネリック引数の型である文字列です。
execute関数とcanExecute関数には、string型の引数があります。
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
···
public DecimalKeypadViewModel()
{
···
DigitCommand = new Command<string>(
execute: (string arg) =>
{
Entry += arg;
if (Entry.StartsWith("0") && !Entry.StartsWith("0."))
{
Entry = Entry.Substring(1);
}
RefreshCanExecutes();
},
canExecute: (string arg) =>
{
return !(arg == "." && Entry.Contains("."));
});
}
···
}
execute メソッドは、文字列引数を Entry プロパティに追加します。 ただし、結果がゼロ (ゼロと小数点ではない) で始まる場合は、 Substring 関数を使用して最初のゼロを削除する必要があります。
canExecuteメソッドは、引数が小数点 (小数点が押されていることを示す) であり、falseに小数点が既に含まれている場合にのみ、Entryを返します。 すべてのexecuteメソッドはRefreshCanExecutesを呼び出し、ChangeCanExecuteとDigitCommandの両方に対してClearCommandを呼び出します。 これにより、入力された数字の現在のシーケンスに基づいて、小数点ボタンとバックスペース ボタンが有効または無効になります。
.NET MAUI
サンプルを参照する