Come implementare la convalida prima di impostare un valore di proprietà in MVVM (WPF)

1

Scenario.

  • ViewModel ha una proprietà State .
  • Visualizza ha un ComboBox che consente di modificare il valore di State .
  • ViewModel deve eseguire alcune convalide (chiama il metodo bool ValidateState(State value) ) prima di impostare il valore della proprietà State .

Domanda.

Come lo implementeresti?

Le mie preoccupazioni.

  • Voglio che la mia soluzione sia generica, quindi potrei usarla in diversi scenari simili. Ad esempio in uno scenario in cui ValidateState() è un metodo async .
  • Non voglio modificare il valore di State in ViewModel a meno che non sia sicuro al 100% che sia valido.
  • Se il nuovo valore di State non è accettato (non è valido) Voglio che ComboBox continui ad avere il vecchio valore come selezionato.

Disclaimer. So che esistono diversi modi per implementarlo. Inoltre non sono un novizio MVVM. Vorrei discutere la soluzione che ritieni più adatta a questo scenario e possibilmente discuterne i pro ei contro.

    
posta Pavels Ahmadulins 21.11.2017 - 13:59
fonte

2 risposte

2

Potresti implementare l'interfaccia INotifyPropertyChanged e utilizzare i setter per eseguire le convalide. Una volta completata la convalida, si imposta il campo privato e si notifica la proprietà modificata per l'UI da aggiornare in base allo stato di ViewModel.

Ecco un esempio di codice.

class YourViewModel : INotifyPropertyChanged {
    private State _state;

    public event PropertyChangedEventHandler PropertyChanged;

    public State State {
        public get { return _state; }
        public set {
            ValidateState(value);
            //If async operation is required, you could set
            //this ViewModel state to "Validating" or something like that
        }
    }

    //ValidateState
    void ValidateState(State newState) {
        //Perform validations
        if (validEnough) {
            //Change the actual state, and also the UI state
            _state = newState;

            //Tells ui to update its view
            PropertyChanged(this, new PropertyChangedEventArgs("State"));
        }
    }

}

//Example of unit test
[TestMethod]
public void SetState_HasNoPermissions_DoesNotChangeState() {
    //... setup resources for the test
    yourViewModel.State = State.FirstStep;

    Assert.NotEquals(yourViewModel.State, State.FirstStep);
}

Aggiornamento per indirizzare alcuni commenti

... I personally think ViewModel should only contain logic, but should not know anything about View

È corretto. Le cose importanti del tuo schermo sono lo stato, non il combobox (che non è noto a viewModel). Non riesco a vedere come il mio esempio fallisce.

Examples: ... it's not OK to generate readable text for View like error meessages ...

Chi ha detto che non è giusto farlo? Vedi qualche problema nell'avere un viewModel che carica messaggi localizzati in fase di runtime, per esempio? Se è OK o no dipenderà dalle esigenze della tua applicazione.

So the thing I'm trying to say is I do not agree with this: here your viewModel IS the user interface.

Quello che ho cercato di dire è che la grafica e il materiale visivo sono posizionati SOLO nella vista, e lo stato e il comportamento sono posizionati nel ViewModel. Pertanto, le GUI sono rappresentate in parte dalla vista e in parte dal ViewModel. Ecco perché ho detto che nell'esempio, viewModel è la tua interfaccia utente, dal momento che non sto considerando dettagli come Combobox o GridView o altro; Mi interessa solo il comportamento dello schermo e questo comportamento dovrebbe essere interamente implementato nel tuo viewModel, se hai scelto di seguire il pattern MVVM.

Ma non è necessario credermi, per favore basta leggere l'articolo Modello di presentazione (un altro nome per lo stesso modello) e l'altra fonte menzionata di seguito.

Also it's not view that requires validation - bussinss logic ruquires it. And therefore it should implemented in the ViewModel layer rather then in View layer.

Quello che intendevo è che potrebbe esserci un requisito come: " Quando l'utente modifica il valore dello stato, deve esserci una convalida su di esso e la vista deve aggiornare questo valore solo se la convalida ha esito positivo. "

Per questo motivo, si desidera implementare un comportamento sulla GUI dell'utente: l'utente cambia il valore di una casella combinata e la convalida avviene; se la validazione fallisce, il valore di combobox rimane lo stesso; altrimenti un nuovo valore è impostato nella casella combinata.

L'affermazione precedente è implementata dal mio esempio, seguendo il modello di progettazione ModelView-ViewModel. In realtà, nell'esempio di codice inserisco ValidateState nel ViewModel, ma l'idea è che viewModel attiva la logica di validazione che dovrebbe essere implementata da qualche altra parte; il codice serve come esempio per aiutarti nel tuo scenario di sviluppo. Ma se tutto sommato ti sembra una cattiva pratica dopotutto, questo è solo un suggerimento comunque:)

Programming best practices are same for everything. If you use MVVM architecture pattern it doesn't mean you should be allowed introducing code smells.

Sono d'accordo e personalmente non vedo l'odore del codice qui.

One last thing. If your viewModel IS the user interface then how is it different from code behind file? ... What is the actual benefit of separating ViewModel from View if ViewModel "serves" View?

Lasciatemi rispondere citando Martin Fowler nel suo articolo sul modello di presentazione:

Presentation Model: Represent the state and behavior of the presentation independently of the GUI controls used in the interface

... E citando l'articolo MVVM da Msdn:

It provides separation of concerns. ... A clean separation between application logic and the UI will make an application easier to test, maintain, and evolve. It improves code re-use opportunities and enables the developer-designer workflow.

Informazioni sul file code-behind: questa è solo la parte C # del tuo XAML. Tutto il codice che hai inserito dovrebbe riguardare solo i problemi di presentazione; idealmente, il comportamento e lo stato della GUI dovrebbero essere implementati nelle classi ViewModel. Almeno questo è ciò che è descritto come il modello, dalle fonti di seguito. Ti suggerisco di leggere e comprendere completamente gli articoli seguenti.

Fonti:

link

link

    
risposta data 21.11.2017 - 16:56
fonte
1

Ecco la mia versione di implementazione:

Visualizza:

<CheckBox 
    Style="{StaticResource CheckBoxStyle}"
    Content="Accept new value" 
    IsChecked="{Binding Path=IsValid, Mode=TwoWay}"
/>

<ComboBox
    Style="{StaticResource ComboBoxStyle}"
    ItemsSource="{Binding Path=States, Mode=OneTime}"
    SelectedItem="{Binding Path=State, Mode=OneWay}"
    SelectionChanged="ComboBox_SelectionChanged"
/>

Visualizza code-behind:

private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var comboBox = (ComboBox)sender;
    var isSuccess = _viewModel.StateSetTry((string)comboBox.SelectedItem);

    if (!isSuccess)
    {
        comboBox.GetBindingExpression(ComboBox.SelectedItemProperty).UpdateTarget();
    }
}

ViewModel:

public class ViewModel : BindableBase
{
    private string _state = "State one";
    private bool _isValid = true;

    public ImmutableArray<string> States { get; } = 
        new [] { "State one", "State two", "State three" }.ToImmutableArray();

    public string State
    {
        get => _state;
        private set => SetProperty(ref _state, value);
    }

    public bool IsValid
    {
        get => _isValid;
        set => SetProperty(ref _isValid, value);
    }

    public bool StateSetTry(string value)
    {
        if (IsValid)
        {
            State = value;
        }

        return IsValid;
    }
}

Pro:

  • Semplice interfaccia pubblica ViewModel (non ci sono ulteriori proprietà "helper")
  • I getter / setter delle proprietà rimangono semplici (segue le best practice di programmazione)
  • Facile conversione in metodo asincrono / attesa (basta creare StateSetTry retrun Task<bool> )

Contro:

  • Approccio MVVM non convenzionale
  • Richiede il codice nel codice sottostante (è possibile spostarlo in una classe Behaviur separata ma probabilmente richiederà l'utilizzo di Reflection, simile a CallMethodAction )

Penso che il fatto che View non possa chiamare (o associare in qualche modo) il metodo public bool StateSetTry(string value) di ViewModel dal codice XAML è un problema del livello View e quindi dovrebbe essere risolto / gestito dal livello View.

Inoltre, ritengo che il livello ViewModel non debba generare testo leggibile dall'uomo in caso di errore a causa del modo in cui è possibile riutilizzare ViewModel. Con le tecnologie Silverlight e Windows Phone che diventano obsolete, l'unico caso in cui ho dovuto riutilizzare ViewModel in un'applicazione commerciale era l'etichettatura bianca. Due diverse applicazioni avevano la stessa logica ma diversa interfaccia utente. In questo caso, queste due diverse applicazioni potrebbero mirare a un pubblico diverso: uno si rivolge ad utenti tecnicamente più ricchi e l'altro utente non ha familiarità con le tecnologie. Pertanto in alcuni casi lo stesso errore generale o di convalida può essere gestito in modo diverso: è possibile visualizzare un messaggio con una descrizione dettagliata e l'altro può provare a risolvere il problema automaticamente e nascondere i dettagli dagli utenti e pertanto non visualizzerà alcun messaggio di errore. Inoltre, View può anche essere un'applicazione console che restituisce codici di errore intero se esiste un requisito di business per tale.

    
risposta data 26.11.2017 - 16:14
fonte

Leggi altre domande sui tag