Ricerca…


Osservazioni

Modelli e modelli di vista

La definizione di un modello è spesso molto discussa e la linea tra un modello e un modello di vista può essere sfocata. Alcuni preferiscono non "inquinare" i loro modelli con INotifyPropertyChanged di interfaccia, e invece duplicare le proprietà modello in-modello di vista, che fa implementare questa interfaccia. Come molte cose nello sviluppo del software, non c'è una risposta giusta o sbagliata. Sii pragmatico e fa tutto ciò che è giusto.

Visualizza Separazione

L'intenzione di MVVM è di separare queste tre aree distinte: modello, modello di vista e vista. Mentre è accettabile per la vista accedere al modello di visualizzazione (VM) e (indirettamente) al modello, la regola più importante con MVVM è che la macchina virtuale non dovrebbe avere accesso alla vista o ai suoi controlli. La VM dovrebbe esporre tutto ciò che la vista ha bisogno, tramite proprietà pubbliche. La VM non dovrebbe esporre o manipolare direttamente controlli UI come TextBox , Button , ecc.

In alcuni casi, questa separazione rigorosa può essere difficile da gestire, soprattutto se è necessario disporre di alcune funzionalità dell'interfaccia utente complessive attive e in esecuzione. Qui, è perfettamente accettabile ricorrere all'utilizzo di eventi e gestori di eventi nel file "code-behind" della vista. Se si tratta di funzionalità puramente dell'interfaccia utente, utilizzare in ogni caso gli eventi nella vista. È anche accettabile che questi gestori di eventi chiamino metodi pubblici sull'istanza VM: basta non passarli riferimenti a controlli dell'interfaccia utente o qualcosa del genere.

RelayCommand

Sfortunatamente la classe RelayCommand usata in questo esempio non fa parte del framework WPF (avrebbe dovuto essere!), Ma lo troverai in quasi tutti gli strumenti dello sviluppatore WPF. Una rapida ricerca online rivelerà un sacco di frammenti di codice che puoi sollevare, per crearne uno tuo.

Un'utile alternativa a RelayCommand è ActionCommand fornita come parte di Microsoft.Expression.Interactivity.Core che offre funzionalità comparabili.

Esempio MVVM di base che utilizza WPF e C #

Questo è un esempio di base per l'utilizzo del modello MVVM in un'applicazione Windows desktop, utilizzando WPF e C #. Il codice di esempio implementa una semplice finestra di dialogo "informazioni utente".

inserisci la descrizione dell'immagine qui

La vista

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>

e il codice dietro

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;
    }
}

Il modello di vista

// 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));
        }
    }
}

Il modello

sealed class User
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public DateTime BirthDate { get; set; }
} 

Il modello di vista

Il modello di visualizzazione è la "VM" in MV VM . Questa è una classe che funge da intermediario, espone il modello (i) all'interfaccia utente (vista) e gestisce le richieste dalla vista, come i comandi generati dai clic sui pulsanti. Ecco un modello di visualizzazione di base:

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
    }
}

Il costruttore crea un oggetto Modello Customer e lo assegna alla proprietà CustomerToEdit , in modo che sia visibile alla vista.

Il costruttore crea anche un oggetto RelayCommand e lo assegna alla proprietà ApplyChangesCommand , rendendolo nuovamente visibile alla vista. I comandi WPF vengono utilizzati per gestire le richieste dalla vista, come i clic sui pulsanti o sulle voci di menu.

RelayCommand accetta due parametri: il primo è il delegato che viene chiamato quando viene eseguito il comando (ad es. In risposta a un clic del pulsante). Il secondo parametro è un delegato che restituisce un valore booleano che indica se il comando può essere eseguito; in questo esempio è collegato alla proprietà IsValid dell'oggetto del IsValid . Quando restituisce false, disattiva il pulsante o la voce di menu associata a questo comando (altri controlli potrebbero comportarsi in modo diverso). Questa è una funzionalità semplice ma efficace, evitando la necessità di scrivere codice per abilitare o disabilitare i controlli in base a condizioni diverse.

Se si ottiene questo esempio attivo e funzionante, provare a svuotare uno dei TextBox (per posizionare il modello Customer in uno stato non valido). Quando ti allontani dal TextBox dovresti scoprire che il pulsante "Applica" diventa disabilitato.

Osservazione sulla creazione del cliente

Il modello di visualizzazione non implementa INotifyPropertyChanged (INPC). Ciò significa che se un oggetto Customer diverso dovesse essere assegnato alla proprietà CustomerToEdit , i controlli della vista non cambierebbero per riflettere il nuovo oggetto: i TextBox rimarrebbero comunque il nome e il cognome del cliente precedente.

Il codice di esempio funziona perché il Customer viene creato nel costruttore del modello di vista, prima che venga assegnato al DataContext della vista (a quel punto i collegamenti sono cablati). In un'applicazione reale, è possibile recuperare i clienti da un database in metodi diversi dal costruttore. Per supportare questo, la VM deve implementare INPC e la proprietà CustomerToEdit deve essere modificata per utilizzare il modello getter e setter "esteso" che si vede nel codice Modello di esempio, sollevando l'evento PropertyChanged nel setter.

ApplyChangesCommand del modello di ApplyChangesCommand non ha bisogno di implementare INPC in quanto è improbabile che il comando cambi. Si avrebbe bisogno di implementare questo modello se si vuole creare il comando da qualche parte diverso da quello del costruttore, ad esempio una sorta di Initialize() metodo.

La regola generale è: implementare INPC se la proprietà è associata a qualsiasi controllo di visualizzazione e il valore della proprietà è in grado di cambiare in qualsiasi altro punto rispetto al costruttore. Non è necessario implementare INPC se il valore della proprietà è sempre assegnato nel costruttore (e si risparmia qualche digitazione nel processo).

Il modello

Il modello è la prima "M" in M VVM. Il modello è in genere una classe contenente i dati che si desidera esporre tramite una sorta di interfaccia utente.

Ecco una classe di modelli molto semplice che espone un paio di proprietà: -

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));
    }
}

Questa classe implementa l'interfaccia INotifyPropertyChanged che espone un evento PropertyChanged . Questo evento dovrebbe essere generato ogni volta che uno dei valori della proprietà cambia: puoi vederlo in azione nel codice precedente. L'evento PropertyChanged è un elemento chiave nei meccanismi di associazione dei dati WPF, in quanto senza di esso l'interfaccia utente non sarebbe in grado di riflettere le modifiche apportate al valore di una proprietà.

Il modello contiene anche una routine di validazione molto semplice che viene chiamata dai setter della proprietà. Imposta una proprietà pubblica che indica se il modello è o meno in uno stato valido. Ho incluso questa funzionalità per dimostrare una funzione "speciale" dei comandi WPF, che vedrai tra breve. Il framework WPF fornisce una serie di approcci più sofisticati alla convalida, ma questi sono al di fuori dello scopo di questo articolo .

La vista

La vista è la "V" nella VM M V. Questa è la tua interfaccia utente. È possibile utilizzare il designer di trascinamento della selezione di Visual Studio, ma alla fine molti sviluppatori finiscono per codificare lo XAML non elaborato, un'esperienza simile alla scrittura di HTML.

Ecco l'XAML di una vista semplice per consentire la modifica di un modello Customer . Piuttosto che creare una nuova vista, questa può essere incollata nel file MainWindow.xaml di un progetto WPF, tra i <Window ...> e </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>

Questo codice crea un semplice modulo di inserimento dati composto da due TextBox , una per il nome cliente e una per il cognome. C'è Label sopra ogni TextBox e un Button "Applica" nella parte inferiore del modulo.

Individua il primo TextBox e osserva la sua proprietà Text :

Text="{Binding CustomerToEdit.Forename}"

Piuttosto che impostare il testo del TextBox su un valore fisso, questa speciale sintassi di parentesi graffa invece vincola il testo al "percorso" CustomerToEdit.Forename . A cosa serve questo percorso? È il "contesto dati" della vista, in questo caso il nostro modello di visualizzazione. Il percorso di collegamento, come potresti essere in grado di capire, è la proprietà CustomerToEdit del modello di visualizzazione, che è di tipo Customer che a sua volta espone una proprietà chiamata Forename - da qui la notazione di percorso "tratteggiata".

Allo stesso modo, se si guarda XAML del Button , ha un Command associato alla proprietà ApplyChangesCommand del modello di visualizzazione. Questo è tutto ciò che è necessario per collegare un pulsante al comando della VM.

DataContext

Quindi, come si imposta il modello di visualizzazione come contesto dati della vista? Un modo è impostarlo nel "code-behind" della vista. Premi F7 per vedere questo file di codice e aggiungi una linea al costruttore esistente per creare un'istanza del modello di vista e assegnarlo alla proprietà DataContext della finestra. Dovrebbe finire così:

    public MainWindow()
    {
        InitializeComponent();

        // Our new line:-
        DataContext = new CustomerEditViewModel();
    }

Nei sistemi del mondo reale, vengono spesso utilizzati altri approcci per creare il modello di visualizzazione, come l'integrazione delle dipendenze o i framework MVVM.

Comandante in MVVM

I comandi vengono utilizzati per gestire gli Events in WPF rispettando il pattern MVVM.

Un normale EventHandler sarebbe simile a questo (situato in Code-Behind ):

public MainWindow()
{
    _dataGrid.CollectionChanged += DataGrid_CollectionChanged;
}

private void DataGrid_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
        //Do what ever
}

No per fare lo stesso in MVVM usiamo i Commands :

 <Button Command="{Binding Path=CmdStartExecution}" Content="Start" />

Raccomando di usare una sorta di prefisso ( Cmd ) per le proprietà del comando, perché ne avrete principalmente bisogno in xaml - in questo modo sono più facili da riconoscere.

Poiché si tratta di MVVM, si desidera gestire tale comando (per il Button "eq" Button_Click ) nel ViewModel .

Per questo fondamentalmente abbiamo bisogno di due cose:

  1. System.Windows.Input.ICommand
  2. RelayCommand (ad esempio preso da qui .

Un semplice esempio potrebbe assomigliare a questo:

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.
}

Quindi, cosa sta facendo in dettaglio:

L' ICommand è ciò a cui il Control in xaml è vincolante. RelayCommand indirizzerà il comando a Action (ad esempio chiama un Method ). Il controllo Null assicura solo che ogni Command venga inizializzato una sola volta (a causa di problemi di prestazioni). Se hai letto il link per RelayCommand sopra puoi aver notato che RelayCommand ha due overload per il suo costruttore. (Action<object> execute) e (Action<object> execute, Predicate<object> canExecute) .

Ciò significa che è possibile aggiungere (ad) un secondo Method restituendo un bool per dire che il Control che l'evento può sparare o meno.

Una buona cosa è che Button s per esempio sarà Enabled="false" se il Method restituirà false

CommandParameters

<DataGrid x:Name="TicketsDataGrid">
    <DataGrid.InputBindings>
        <MouseBinding Gesture="LeftDoubleClick" 
                      Command="{Binding CmdTicketClick}" 
                      CommandParameter="{Binding ElementName=TicketsDataGrid, 
                                                 Path=SelectedItem}" />
    </DataGrid.InputBindings>
<DataGrid />

In questo esempio voglio passare DataGrid.SelectedItem a Click_Command nel mio ViewModel.

Il tuo metodo dovrebbe apparire così mentre l'implementazione ICommand rimane come sopra.

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
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow