Suche…
Bemerkungen
Modelle und Ansichtsmodelle
Die Definition eines Modells wird häufig heiß diskutiert, und die Grenze zwischen einem Modell und einem Ansichtsmodell kann verschwimmen. Manche bevorzugen nicht zu „verunreinigen“ ihre Modelle mit der INotifyPropertyChanged
- Schnittstelle, und stattdessen die Modell - Eigenschaften in der Ansicht-Modell duplizieren, die diese Schnittstelle nicht implementiert. Wie viele Dinge in der Softwareentwicklung gibt es keine richtige oder falsche Antwort. Seien Sie pragmatisch und tun Sie, was sich richtig anfühlt.
Separation anzeigen
Die Absicht von MVVM besteht darin, diese drei unterschiedlichen Bereiche - Modell, Ansichtsmodell und Ansicht - voneinander zu trennen. Während es für die Ansicht zulässig ist, auf das Ansichtsmodell (VM) und (indirekt) auf das Modell zuzugreifen, besteht die wichtigste Regel in MVVM darin, dass die VM keinen Zugriff auf die Ansicht oder deren Steuerelemente haben sollte. Die VM sollte über öffentliche Eigenschaften alles bereitstellen, was die Ansicht benötigt. Die VM sollte UI-Steuerelemente wie TextBox
, Button
usw. nicht direkt TextBox
oder TextBox
.
In manchen Fällen kann es schwierig sein, mit dieser strikten Trennung zu arbeiten, insbesondere wenn Sie einige komplexe UI-Funktionen in Betrieb nehmen müssen. Hier ist es durchaus akzeptabel, auf Ereignisse und Ereignishandler in der "Code-Behind" -Datei der Ansicht zurückzugreifen. Wenn es sich um eine reine UI-Funktionalität handelt, werden auf jeden Fall Ereignisse in der Ansicht verwendet. Es ist auch akzeptabel, dass diese Ereignishandler öffentliche Methoden für die VM-Instanz aufrufen. Vergeben Sie die Verweise nicht an die Steuerelemente der Benutzeroberfläche oder ähnliches.
RelayCommand
Leider ist die in diesem Beispiel verwendete RelayCommand
Klasse nicht Teil des WPF-Frameworks (sollte es auch sein!), Aber Sie finden sie in fast jedem Werkzeugkasten des WPF-Entwicklers. Bei einer schnellen Online-Suche werden zahlreiche Code-Snippets angezeigt, die Sie anheben können, um eigene zu erstellen.
Eine nützliche Alternative zu RelayCommand
ist ActionCommand
das als Teil von Microsoft.Expression.Interactivity.Core
bereitgestellt wird und vergleichbare Funktionen bietet.
Grundlegendes MVVM-Beispiel mit WPF und C #
Dies ist ein einfaches Beispiel für die Verwendung des MVVM-Modells in einer Windows-Desktopanwendung mit WPF und C #. Der Beispielcode implementiert einen einfachen "Benutzerinfo" -Dialog.
Die Aussicht
Die XAML
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Margin="4" Text="{Binding FullName}" HorizontalAlignment="Center" FontWeight="Bold"/>
<Label Grid.Column="0" Grid.Row="1" Margin="4" Content="First Name:" HorizontalAlignment="Right"/>
<!-- UpdateSourceTrigger=PropertyChanged makes sure that changes in the TextBoxes are immediately applied to the model. -->
<TextBox Grid.Column="1" Grid.Row="1" Margin="4" Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="2" Margin="4" Content="Last Name:" HorizontalAlignment="Right"/>
<TextBox Grid.Column="1" Grid.Row="2" Margin="4" Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="3" Margin="4" Content="Age:" HorizontalAlignment="Right"/>
<TextBlock Grid.Column="1" Grid.Row="3" Margin="4" Text="{Binding Age}" HorizontalAlignment="Left"/>
</Grid>
und der Code dahinter
public partial class MainWindow : Window
{
private readonly MyViewModel _viewModel;
public MainWindow() {
InitializeComponent();
_viewModel = new MyViewModel();
// The DataContext serves as the starting point of Binding Paths
DataContext = _viewModel;
}
}
Das Ansichtsmodell
// INotifyPropertyChanged notifies the View of property changes, so that Bindings are updated.
sealed class MyViewModel : INotifyPropertyChanged
{
private User user;
public string FirstName {
get {return user.FirstName;}
set {
if(user.FirstName != value) {
user.FirstName = value;
OnPropertyChange("FirstName");
// If the first name has changed, the FullName property needs to be udpated as well.
OnPropertyChange("FullName");
}
}
}
public string LastName {
get { return user.LastName; }
set {
if (user.LastName != value) {
user.LastName = value;
OnPropertyChange("LastName");
// If the first name has changed, the FullName property needs to be udpated as well.
OnPropertyChange("FullName");
}
}
}
// This property is an example of how model properties can be presented differently to the View.
// In this case, we transform the birth date to the user's age, which is read only.
public int Age {
get {
DateTime today = DateTime.Today;
int age = today.Year - user.BirthDate.Year;
if (user.BirthDate > today.AddYears(-age)) age--;
return age;
}
}
// This property is just for display purposes and is a composition of existing data.
public string FullName {
get { return FirstName + " " + LastName; }
}
public MyViewModel() {
user = new User {
FirstName = "John",
LastName = "Doe",
BirthDate = DateTime.Now.AddYears(-30)
};
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChange(string propertyName) {
if(PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Das Model
sealed class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
Das Ansichtsmodell
Das Ansichtsmodell ist die "VM" in MV VM . Hierbei handelt es sich um eine Klasse, die als Vermittler fungiert, die Modelle der Benutzeroberfläche (Ansicht) zur Verfügung stellt und Anforderungen aus der Ansicht verarbeitet, z. B. Befehle, die durch Klicken mit der Schaltfläche ausgelöst werden. Hier ist ein grundlegendes Ansichtsmodell:
public class CustomerEditViewModel
{
/// <summary>
/// The customer to edit.
/// </summary>
public Customer CustomerToEdit { get; set; }
/// <summary>
/// The "apply changes" command
/// </summary>
public ICommand ApplyChangesCommand { get; private set; }
/// <summary>
/// Constructor
/// </summary>
public CustomerEditViewModel()
{
CustomerToEdit = new Customer
{
Forename = "John",
Surname = "Smith"
};
ApplyChangesCommand = new RelayCommand(
o => ExecuteApplyChangesCommand(),
o => CustomerToEdit.IsValid);
}
/// <summary>
/// Executes the "apply changes" command.
/// </summary>
private void ExecuteApplyChangesCommand()
{
// E.g. save your customer to database
}
}
Der Konstruktor erstellt ein Customer
Modellobjekt und weist es der CustomerToEdit
Eigenschaft zu, sodass es für die Ansicht sichtbar ist.
Der Konstruktor erstellt auch ein RelayCommand
Objekt und ordnet es der ApplyChangesCommand
Eigenschaft zu. ApplyChangesCommand
wird es erneut für die Ansicht sichtbar. WPF-Befehle werden verwendet, um Anforderungen aus der Ansicht zu bearbeiten, z. B. das Klicken von Schaltflächen oder Menüs.
Der RelayCommand
benötigt zwei Parameter - der erste ist der Delegat, der aufgerufen wird, wenn der Befehl ausgeführt wird (z. B. als Reaktion auf einen Tastenklick). Der zweite Parameter ist ein Delegat, der einen booleschen Wert zurückgibt, der angibt, ob der Befehl ausgeführt werden kann. In diesem Beispiel ist es mit der IsValid
Eigenschaft des IsValid
. Wenn dies false zurückgibt, wird die Schaltfläche oder das Menüelement deaktiviert, das an diesen Befehl gebunden ist (andere Steuerelemente verhalten sich möglicherweise anders). Dies ist eine einfache, aber effektive Funktion, die das Schreiben von Code zum Aktivieren oder Deaktivieren von Steuerelementen aufgrund verschiedener Bedingungen vermeidet.
Wenn Sie dieses Beispiel in Betrieb nehmen, versuchen Sie, eine der TextBox
zu TextBox
(um das Customer
in einen ungültigen Zustand zu TextBox
). Wenn Sie das TextBox
, sollten Sie feststellen, dass die Schaltfläche "Übernehmen" deaktiviert ist.
Bemerkung zur Kundenerstellung
Das Ansichtsmodell implementiert INotifyPropertyChanged
(INPC) nicht. Das bedeutet, wenn der CustomerToEdit
Eigenschaft ein anderes Customer
Objekt zugewiesen werden sollte, würden sich die Steuerelemente der Ansicht nicht ändern, um das neue Objekt TextBox
Die TextBox
würde immer noch den TextBox
und Nachnamen des vorherigen Kunden enthalten.
Der Beispielcode funktioniert, da der Customer
im Konstruktor des Ansichtsmodells erstellt wird, bevor er dem DataContext
der Ansicht zugewiesen wird (an diesem Punkt werden die Bindungen verdrahtet). In einer realen Anwendung können Sie Kunden aus einer Datenbank mit anderen Methoden als dem Konstruktor abrufen. Um dies zu unterstützen, sollte die VM INPC implementieren, und die CustomerToEdit
Eigenschaft sollte geändert werden, um das "erweiterte" Getter- und Setter-Muster zu verwenden, das Sie im Beispielcode für das Modell sehen. Dadurch wird das PropertyChanged
Ereignis im Setter ausgelöst.
Der ApplyChangesCommand
des ApplyChangesCommand
muss INPC nicht implementieren, da es unwahrscheinlich ist, dass sich der Befehl ändert. Sie müssten dieses Muster implementieren , wenn Sie den Befehl an einer anderen Stelle zu schaffen wurden als Konstruktor zum Beispiel eine Art von Initialize()
Methode.
Die allgemeine Regel lautet: Implementieren Sie INPC, wenn die Eigenschaft an beliebige Ansichtssteuerelemente gebunden ist und der Wert der Eigenschaft an einer anderen Stelle als im Konstruktor geändert werden kann. Sie müssen INPC nicht implementieren, wenn der Eigenschaftswert immer nur im Konstruktor zugewiesen wird (und Sie sich etwas Tipparbeit ersparen).
Das Model
Das Modell ist das erste "M" in M VVM. Das Modell ist normalerweise eine Klasse, die die Daten enthält, die Sie über eine Art Benutzeroberfläche anzeigen möchten.
Hier ist eine sehr einfache Modellklasse, die einige Eigenschaften zeigt: -
public class Customer : INotifyPropertyChanged
{
private string _forename;
private string _surname;
private bool _isValid;
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Customer forename.
/// </summary>
public string Forename
{
get
{
return _forename;
}
set
{
if (_forename != value)
{
_forename = value;
OnPropertyChanged();
SetIsValid();
}
}
}
/// <summary>
/// Customer surname.
/// </summary>
public string Surname
{
get
{
return _surname;
}
set
{
if (_surname != value)
{
_surname = value;
OnPropertyChanged();
SetIsValid();
}
}
}
/// <summary>
/// Indicates whether the model is in a valid state or not.
/// </summary>
public bool IsValid
{
get
{
return _isValid;
}
set
{
if (_isValid != value)
{
_isValid = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Sets the value of the IsValid property.
/// </summary>
private void SetIsValid()
{
IsValid = !string.IsNullOrEmpty(Forename) && !string.IsNullOrEmpty(Surname);
}
/// <summary>
/// Raises the PropertyChanged event.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
private void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Diese Klasse implementiert die INotifyPropertyChanged
Schnittstelle, die ein PropertyChanged
Ereignis INotifyPropertyChanged
macht. Dieses Ereignis sollte ausgelöst werden, wenn sich einer der Eigenschaftswerte ändert. Sie können dies in Aktion im obigen Code sehen. Das PropertyChanged
Ereignis ist ein Schlüsselelement in den WPF-Datenbindungsmechanismen, da die Benutzeroberfläche ohne sie die Änderungen des Werts einer Eigenschaft nicht widerspiegeln kann.
Das Modell enthält auch eine sehr einfache Validierungsroutine, die von den Eigenschaftssetzern aufgerufen wird. Es legt eine öffentliche Eigenschaft fest, die angibt, ob das Modell in einem gültigen Status ist oder nicht. Ich habe diese Funktionalität hinzugefügt, um eine "spezielle" Funktion von WPF- Befehlen zu demonstrieren, die Sie in Kürze sehen werden. Das WPF-Framework bietet eine Reihe komplexerer Ansätze für die Validierung, die jedoch nicht in den Rahmen dieses Artikels fallen .
Die Aussicht
Die Ansicht ist das "V" in M V VM. Dies ist Ihre Benutzeroberfläche. Sie können den Visual Studio-Drag-and-Drop-Designer verwenden, aber die meisten Entwickler müssen schließlich die reine XAML-Datei kodieren - eine Erfahrung, die dem Schreiben von HTML ähnelt.
Hier ist die XAML einer einfachen Ansicht, um das Bearbeiten eines Customer
zu ermöglichen. Anstatt eine neue Ansicht zu erstellen, kann diese einfach in die MainWindow.xaml
Datei eines WPF-Projekts MainWindow.xaml
werden, zwischen den Tags <Window ...>
und </Window>
: -
<StackPanel Orientation="Vertical"
VerticalAlignment="Top"
Margin="20">
<Label Content="Forename"/>
<TextBox Text="{Binding CustomerToEdit.Forename}"/>
<Label Content="Surname"/>
<TextBox Text="{Binding CustomerToEdit.Surname}"/>
<Button Content="Apply Changes"
Command="{Binding ApplyChangesCommand}" />
</StackPanel>
Dieser Code erstellt ein einfaches Dateneingabeformular, das aus zwei TextBox
- einer für den Vornamen des Kunden und eine für den Nachnamen. Es gibt ein Label
über jedem TextBox
und ein „ Übernehmen“ Button
am unteren Rand des Formulars.
TextBox
Sie das erste TextBox
und sehen Sie seine Text
Eigenschaft an:
Text="{Binding CustomerToEdit.Forename}"
Anstatt den Text der TextBox
auf einen festen Wert zu setzen, bindet diese spezielle geschweifte Klammer-Syntax den Text stattdessen an den "Pfad" CustomerToEdit.Forename
. Was ist dieser Weg relativ? Es ist der "Datenkontext" der Ansicht - in diesem Fall unser Ansichtsmodell. Der Bindungspfad ist, wie Sie möglicherweise herausfinden können, die CustomerToEdit
Eigenschaft des Forename
. Forename
Eigenschaft ist vom Typ Customer
, der wiederum eine Eigenschaft namens Forename
- daher die "gepunktete" Forename
.
Und falls Sie auf der Suche Button
‚s XAML, hat es einen Command
, der an die gebunden ist ApplyChangesCommand
Eigenschaft des View-Modell. Das ist alles, was Sie benötigen, um eine Schaltfläche mit dem Befehl der VM zu verbinden.
Der DataContext
Wie stellen Sie also das Ansichtsmodell als Datenkontext der Ansicht ein? Eine Möglichkeit besteht darin, sie in den "Code-Behind" der Ansicht zu setzen. Drücken Sie F7, um diese Codedatei anzuzeigen, und fügen Sie dem vorhandenen Konstruktor eine Zeile hinzu, um eine Instanz des Ansichtsmodells zu erstellen und sie der DataContext
Eigenschaft des Fensters zuzuweisen. Es sollte am Ende so aussehen:
public MainWindow()
{
InitializeComponent();
// Our new line:-
DataContext = new CustomerEditViewModel();
}
In realen Systemen werden häufig andere Ansätze verwendet, um das Ansichtsmodell zu erstellen, z. B. Abhängigkeitsinjektion oder MVVM-Frameworks.
Befehlen in MVVM
Befehle werden zur Behandlung von Events
in WPF unter Berücksichtigung des MVVM-Musters verwendet.
Ein normaler EventHandler
würde so aussehen (in Code-Behind
):
public MainWindow()
{
_dataGrid.CollectionChanged += DataGrid_CollectionChanged;
}
private void DataGrid_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//Do what ever
}
Nein, um das gleiche in MVVM zu tun, verwenden wir Commands
:
<Button Command="{Binding Path=CmdStartExecution}" Content="Start" />
Ich empfehle, eine Art Präfix (
Cmd
) für Ihre Befehlseigenschaften zu verwenden, da Sie sie hauptsächlich in xaml benötigen - auf diese Weise sind sie leichter zu erkennen.
Da es sich um MVVM handelt, möchten Sie diesen Befehl (für Button
"eq" Button_Click
) in Ihrem ViewModel
.
Dafür brauchen wir grundsätzlich zwei Dinge:
- System.Windows.Input.ICommand
- RelayCommand (zum Beispiel hier genommen) .
Ein einfaches Beispiel könnte so aussehen:
private RelayCommand _commandStart;
public ICommand CmdStartExecution
{
get
{
if(_commandStart == null)
{
_commandStart = new RelayCommand(param => Start(), param => CanStart());
}
return _commandStart;
}
}
public void Start()
{
//Do what ever
}
public bool CanStart()
{
return (DateTime.Now.DayOfWeek == DayOfWeek.Monday); //Can only click that button on mondays.
}
Was macht das also im Detail:
Der ICommand
das Control
in xaml. Der RelayCommand
leitet Ihren Befehl an eine Action
(dh, eine Method
aufzurufen). Die Nullprüfung stellt lediglich sicher, dass jeder Command
nur einmal initialisiert wird (aufgrund von Leistungsproblemen). Wenn Sie den Link für RelayCommand
oben gelesen haben, haben Sie möglicherweise bemerkt, dass RelayCommand
zwei Überladungen für den Konstruktor hat. (Action<object> execute)
und (Action<object> execute, Predicate<object> canExecute)
.
Das heißt , Sie können (Aditionally) eine zweite hinzufügen Method
Zurückgeben eines bool
zu sagen , dass Control
wheather das „Ereignis“ abfeuern kann oder nicht.
Das Gute daran ist, dass Button
zum Beispiel Enabled="false"
wenn die Method
false
Befehlsparameter
<DataGrid x:Name="TicketsDataGrid">
<DataGrid.InputBindings>
<MouseBinding Gesture="LeftDoubleClick"
Command="{Binding CmdTicketClick}"
CommandParameter="{Binding ElementName=TicketsDataGrid,
Path=SelectedItem}" />
</DataGrid.InputBindings>
<DataGrid />
In diesem Beispiel möchte ich das DataGrid.SelectedItem
an den Click_Command in meinem ViewModel übergeben.
Ihre Methode sollte so aussehen, während die ICommand-Implementierung selbst so bleibt.
private RelayCommand _commandTicketClick;
public ICommand CmdTicketClick
{
get
{
if(_commandTicketClick == null)
{
_commandTicketClick = new RelayCommand(param => HandleUserClick(param));
}
return _commandTicketClick;
}
}
private void HandleUserClick(object item)
{
MyModelClass selectedItem = item as MyModelClass;
if (selectedItem != null)
{
//Do sth. with that item
}
}