Velící
V aplikaci .NET Multi-Platform App UI (.NET MAUI), která používá model-View-ViewModel (MVVM), jsou datové vazby definovány mezi vlastnostmi v modelu viewmodel, což je obvykle třída odvozená od INotifyPropertyChanged
a vlastnosti v zobrazení, což je obvykle soubor XAML. Někdy aplikace potřebuje, aby přesahovala tyto vazby vlastností tím, že vyžaduje, aby uživatel inicioval příkazy, které mají vliv na něco v modelu viewmodel. Tyto příkazy jsou obecně signalizovány kliknutím na tlačítko nebo prstem klepněte a tradičně se zpracovávají v souboru kódu v obslužné rutině pro Clicked
událost události Button nebo Tapped
události TapGestureRecognizerudálosti .
Příkazové rozhraní poskytuje alternativní přístup k implementaci příkazů, které jsou pro architekturu MVVM mnohem vhodnější. Model viewmodel může obsahovat příkazy, které jsou metody spouštěné v reakci na konkrétní aktivitu v zobrazení, například Button kliknutí. Datové vazby jsou definovány mezi těmito příkazy a Button.
Chcete-li povolit datovou vazbu mezi modelem Button a modelem viewmodel, Button definuje dvě vlastnosti:
Command
typuSystem.Windows.Input.ICommand
CommandParameter
typuObject
Chcete-li použít příkazové rozhraní, definujete datovou vazbu, která cílí na Command
vlastnost Button , kde zdroj je vlastnost v modelu viewmodel typu ICommand. Model viewmodel obsahuje kód přidružený k této ICommand vlastnosti, která se spustí při kliknutí na tlačítko. Vlastnost můžete nastavit CommandParameter
na libovolná data, abyste mohli rozlišovat mezi více tlačítky, pokud jsou všechny vázány na stejnou ICommand vlastnost v modelu viewmodel.
Mnoho dalších zobrazení také definuje Command
a CommandParameter
vlastnosti. Všechny tyto příkazy je možné zpracovat v modelu viewmodel pomocí přístupu, který nezávisí na objektu uživatelského rozhraní v zobrazení.
ICommands
Rozhraní ICommand je definováno v oboru názvů System.Windows.Input a skládá se ze dvou metod a jedné události:
public interface ICommand
{
public void Execute (Object parameter);
public bool CanExecute (Object parameter);
public event EventHandler CanExecuteChanged;
}
Chcete-li použít příkazové rozhraní, model viewmodel by měl obsahovat vlastnosti typu ICommand:
public ICommand MyCommand { private set; get; }
Model viewmodel musí také odkazovat na třídu, která implementuje ICommand rozhraní. V zobrazení Command
je vlastnost Button vlastnosti vázána na tuto vlastnost:
<Button Text="Execute command"
Command="{Binding MyCommand}" />
Když uživatel stiskne Button, Button volá metodu Execute
v objektu ICommand vázaném na jeho Command
vlastnost.
Když je vazba poprvé definována na Command
vlastnosti , a když se datová vazba nějakým způsobem změní, Button volá metodu CanExecute
v objektu ICommand Button. Pokud CanExecute
se vrátí false
, pak Button se zakáže sám. To znamená, že konkrétní příkaz je momentálně nedostupný nebo neplatný.
Také Button připojí obslužnou rutinu k CanExecuteChanged
události ICommand. Událost je vyvolána z modelu viewmodel. Při vyvolání této události se Button volání CanExecute
znovu zobrazí. Umožňuje Button sám sebe, pokud CanExecute
vrátí true
a zakáže sám sebe, pokud CanExecute
vrátí false
.
Upozorňující
IsEnabled
Nepoužívejte vlastnostButton, pokud používáte příkazové rozhraní.
Když váš model viewmodel definuje vlastnost typu ICommand, viewmodel musí také obsahovat nebo odkazovat na třídu, která implementuje ICommand rozhraní. Tato třída musí obsahovat nebo odkazovat na Execute
metody a CanExecute
aktivovat CanExecuteChanged
událost vždy, když CanExecute
metoda může vrátit jinou hodnotu. K implementaci ICommand rozhraní můžete použít Command
rozhraní nebo Command<T>
třídu, která je součástí rozhraní .NET MAUI. Tyto třídy umožňují určit těla Execute
a CanExecute
metody v konstruktorech tříd.
Tip
Používáte-li Command<T>
CommandParameter
vlastnost k rozlišení mezi více zobrazeními vázanými na stejnou ICommand vlastnost, a Command
třídu, pokud to není požadavek.
Základní příkazování
Následující příklady ukazují základní příkazy implementované v modelu viewmodel.
Třída PersonViewModel
definuje tři vlastnosti s názvem Name
, Age
a Skills
které definují osobu:
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));
}
}
Níže PersonCollectionViewModel
uvedená třída vytvoří nové objekty typu PersonViewModel
a umožní uživateli vyplnit data. Pro tento účel třída definuje IsEditing
, typ bool
a PersonEdit
, typ PersonViewModel
, vlastnosti. Třída navíc definuje tři vlastnosti typu ICommand a vlastnost s názvem Persons
typu 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));
}
}
V tomto příkladu se změní na tři ICommand vlastnosti a Persons
vlastnost nemá za následek PropertyChanged
vyvolání událostí. Tyto vlastnosti jsou nastaveny při prvním vytvoření třídy a nemění se.
Následující příklad ukazuje XAML, který využívá PersonCollectionViewModel
:
<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">
<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 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>
V tomto příkladu je vlastnost stránky BindingContext
nastavena na PersonCollectionViewModel
hodnotu . Obsahuje Grid s textem Button New s jeho Command
vlastností vázanou na NewCommand
vlastnost v modelu viewmodel, vstupní formulář s vlastnostmi vázanými na IsEditing
vlastnost, stejně jako vlastnosti PersonViewModel
, a dva další tlačítka svázané s SubmitCommand
a CancelCommand
vlastnosti modelu viewmodel. Zobrazí ListView kolekci osob, které již byly zadány:
Následující snímek obrazovky ukazuje tlačítko Odeslat povolené po nastavení věku:
Když uživatel poprvé stiskne tlačítko Nový , povolí se formulář pro zadání, ale zakáže se tlačítko Nový . Uživatel pak zadá jméno, věk a dovednosti. Kdykoli během úprav může uživatel stisknout tlačítko Storno a začít znovu. Je-li zadáno jméno a platný věk, je tlačítko Odeslat povoleno. Stisknutím tohoto tlačítka Odeslat přenesete osobu do kolekce zobrazené pomocí ListViewtlačítka . Po stisknutí tlačítka Zrušit nebo Odeslat se formulář pro zadání vymaže a tlačítko Nový se znovu povolí.
Veškerá logika tlačítek Nový, Odeslat a Zrušit se zpracovává PersonCollectionViewModel
prostřednictvím definic , NewCommand
SubmitCommand
a CancelCommand
vlastností. Konstruktor těchto PersonCollectionViewModel
tří vlastností nastaví na objekty typu Command
.
Konstruktor Command
třídy umožňuje předat argumenty typu Action
a Func<bool>
odpovídající metodámExecute
.CanExecute
Tuto akci a funkci lze v konstruktoru Command
definovat jako funkce lambda:
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();
}
···
}
Když uživatel klikne na tlačítko Nový , execute
funkce předaná konstruktoru Command
se spustí. Tím se vytvoří nový PersonViewModel
objekt, nastaví obslužnou rutinu pro událost tohoto objektu PropertyChanged
, nastaví IsEditing
na true
a zavolá metodu RefreshCanExecutes
definovanou za konstruktorem.
Kromě implementace ICommand rozhraní třída Command
také definuje metodu s názvem ChangeCanExecute
. Model viewmodel by měl volat ChangeCanExecute
vlastnost ICommand vždy, když se stane cokoli, co by mohlo změnit návratovou CanExecute
hodnotu metody. Volání ChangeCanExecute
způsobí, že Command
třída aktivuje metodu CanExecuteChanged
. Připojil Button obslužnou rutinu pro tuto událost a reaguje opětovným voláním CanExecute
a pak se povolí na základě návratové hodnoty této metody.
execute
Když metoda NewCommand
volání RefreshCanExecutes
, NewCommand
vlastnost získá volání ChangeCanExecute
, a Button volání canExecute
metody, která nyní vrátífalse
, protože IsEditing
vlastnost je nyní true
.
Obslužná rutina PropertyChanged
nového objektu volá metodu ChangeCanExecute
SubmitCommand
:PersonViewModel
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;
});
···
}
···
}
Funkce canExecute
se SubmitCommand
volá při každé změně vlastnosti objektu, který PersonViewModel
se upravuje. Vrátí true
pouze v případě, že Name
je vlastnost alespoň jeden znak dlouhý a Age
je větší než 0. V té době se aktivuje tlačítko Odeslat .
Funkce execute
Submit odebere obslužnou rutinu změněnou vlastností z objektu PersonViewModel
, přidá objekt do Persons
kolekce a vrátí vše do původního stavu.
Funkce execute
tlačítka Zrušit dělá vše, co tlačítko Odeslat dělá, s výjimkou přidání objektu do kolekce:
public class PersonCollectionViewModel : INotifyPropertyChanged
{
···
public PersonCollectionViewModel()
{
···
CancelCommand = new Command(
execute: () =>
{
PersonEdit.PropertyChanged -= OnPersonEditPropertyChanged;
PersonEdit = null;
IsEditing = false;
RefreshCanExecutes();
},
canExecute: () =>
{
return IsEditing;
});
}
···
}
Metoda canExecute
se vrátí true
kdykoliv PersonViewModel
, kdy se upravuje.
Poznámka:
Není nutné definovat execute
a canExecute
metody jako funkce lambda. V modelu viewmodel je můžete napsat jako soukromé metody a odkazovat na ně v Command
konstruktorech. Tento přístup ale může vést k mnoha metodám, na které se odkazuje pouze jednou v modelu viewmodel.
Použití parametrů příkazu
Někdy je vhodné použít jedno nebo více tlačítek nebo jiných objektů uživatelského rozhraní ke sdílení stejné ICommand vlastnosti v modelu viewmodel. V tomto případě můžete pomocí CommandParameter
vlastnosti rozlišovat mezi tlačítky.
Pro tyto sdílené ICommand vlastnosti můžete i nadále používat Command
třídu. Třída definuje alternativní konstruktor, který přijímá execute
a canExecute
metody s parametry typu Object
. CommandParameter
Takto se tyto metody předávají. Při zadávání CommandParameter
, je nejjednodušší použít generické Command<T>
třídy určit typ objektu nastaven na CommandParameter
. canExecute
Zadané execute
metody mají parametry tohoto typu.
Následující příklad ukazuje klávesnici pro zadávání desetinných čísel:
<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">
<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>
V tomto příkladu je stránka BindingContext
.DecimalKeypadViewModel
Vlastnost Entry tohoto modelu zobrazení je vázána na Text
vlastnost objektu Label. Button Všechny objekty jsou vázány na příkazy v modelu viewmodel: ClearCommand
, BackspaceCommand
a DigitCommand
. Tlačítka 11 pro 10 číslic a desetinná čárka sdílejí vazbu s DigitCommand
. Rozlišení CommandParameter
mezi těmito tlačítky. Hodnota nastavená na CommandParameter
hodnotu je obecně stejná jako text zobrazený tlačítkem s výjimkou desetinné čárky, která je pro účely srozumitelnosti zobrazena střední tečkou:
Definuje DecimalKeypadViewModel
vlastnost typu string
a tři vlastnosti typuICommand:Entry
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; }
}
Tlačítko odpovídající ho ClearCommand
je vždy povoleno a nastaví položku zpět na "0":
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
···
public DecimalKeypadViewModel()
{
ClearCommand = new Command(
execute: () =>
{
Entry = "0";
RefreshCanExecutes();
});
···
}
void RefreshCanExecutes()
{
((Command)BackspaceCommand).ChangeCanExecute();
((Command)DigitCommand).ChangeCanExecute();
}
···
}
Protože tlačítko je vždy povoleno, není nutné zadat canExecute
argument v konstruktoru Command
.
Tlačítko Backspace je povoleno pouze v případě, že délka položky je větší než 1 nebo pokud Entry není rovna řetězci "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";
});
···
}
···
}
Logika pro funkci tlačítka execute
Backspace zajišťuje, že Entry je alespoň řetězec "0".
Vlastnost DigitCommand
je vázána na 11 tlačítek, z nichž každý identifikuje sám se CommandParameter
vlastností. Je DigitCommand
nastavena na instanci Command<T>
třídy. Při použití příkazového rozhraní s XAML CommandParameter
jsou vlastnosti obvykle řetězce, což je typ obecného argumentu. canExecute
Funkce execute
pak mají argumenty typustring
:
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("."));
});
}
···
}
Metoda execute
připojí řetězcový argument k Entry vlastnosti. Pokud ale výsledek začíná nulou (ale ne nulou a desetinnou čárkou), musí být tato počáteční nula odebrána pomocí Substring
funkce. Metoda canExecute
vrátí false
pouze v případě, že argument je desetinná čárka (označuje, že desetinná čárka je stisknuta) a Entry již obsahuje desetinnou čárku. Všechny metody volají execute
RefreshCanExecutes
, které pak volá ChangeCanExecute
obojí DigitCommand
a ClearCommand
. Tím zajistíte, že jsou tlačítka desetinné čárky a backspace povolená nebo zakázaná na základě aktuální sekvence zadaných číslic.