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.

Geben Sie hier die Bildbeschreibung ein

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:

  1. System.Windows.Input.ICommand
  2. 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
    }
}


Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow