Zoeken…


Opmerkingen

Modellen en weergavemodellen

Over de definitie van een model wordt vaak fel gedebatteerd en de lijn tussen een model en een view-model kan wazig zijn. Sommigen geven er de voorkeur aan hun modellen niet te "vervuilen" met de INotifyPropertyChanged interface, en dupliceren in plaats daarvan de INotifyPropertyChanged in het view-model, dat deze interface wel implementeert. Zoals bij veel dingen in softwareontwikkeling is er geen goed of fout antwoord. Wees pragmatisch en doe wat goed voelt.

Scheiding bekijken

De bedoeling van MVVM is om die drie verschillende gebieden te scheiden - Model, view-model en View. Hoewel het acceptabel is dat de view toegang heeft tot het view-model (VM) en (indirect) het model, is de belangrijkste regel met MVVM dat de VM geen toegang moet hebben tot de view of de besturingselementen. De VM moet alles wat de weergave nodig heeft, openbaar maken via openbare eigenschappen. De VM mag UI-besturingselementen zoals TextBox , Button , etc. niet rechtstreeks blootleggen of manipuleren.

In sommige gevallen kan deze strikte scheiding moeilijk zijn om mee te werken, vooral als u een aantal complexe UI-functionaliteit in gebruik moet nemen. Hier is het volkomen acceptabel om gebruik te maken van evenementen en event-handlers in het "code-behind" -bestand van de view. Als het puur UI-functionaliteit is, gebruik dan in elk geval gebeurtenissen in de weergave. Het is ook acceptabel voor deze event-handlers om openbare methoden op het VM-exemplaar aan te roepen - ga gewoon niet door met verwijzingen naar UI-besturingselementen of iets dergelijks.

RelayCommand

Helaas maakt de RelayCommand klasse die in dit voorbeeld wordt gebruikt geen deel uit van het WPF-framework (het had moeten zijn!), Maar je vindt het in bijna elke WPF-ontwikkelaarskist. Een snelle online zoekactie zal veel codefragmenten onthullen die u kunt opheffen om uw eigen te maken.

Een nuttig alternatief voor RelayCommand is ActionCommand die wordt geleverd als onderdeel van Microsoft.Expression.Interactivity.Core die vergelijkbare functionaliteit biedt.

Standaard MVVM-voorbeeld met WPF en C #

Dit is een eenvoudig voorbeeld voor het gebruik van het MVVM-model in een Windows-bureaubladtoepassing, met behulp van WPF en C #. De voorbeeldcode implementeert een eenvoudig dialoogvenster "gebruikersinfo".

voer hier de afbeeldingsbeschrijving in

Het uitzicht

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

en de code erachter

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

Het View-model

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

Het model

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

    public string LastName { get; set; }

    public DateTime BirthDate { get; set; }
} 

Het view-model

Het view-model is de "VM" in MV VM . Dit is een klasse die fungeert als een tussenpersoon, die het model (de modellen) blootstelt aan de gebruikersinterface (weergave) en die verzoeken uit de weergave behandelt, zoals opdrachten die worden gegenereerd door klikken op knoppen. Hier is een standaard view-model:

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

De aannemer zorgt voor een Customer model object en wijst deze toe aan de CustomerToEdit pand, zodat het zichtbaar is voor het uitzicht.

De constructor maakt een RelayCommand object en wijst deze toe aan de ApplyChangesCommand eigenschap weer waardoor het zichtbaar is voor de weergave. WPF-opdrachten worden gebruikt om verzoeken uit de weergave te verwerken, zoals klikken op knoppen of menu-items.

De RelayCommand neemt twee parameters - de eerste is de gedelegeerde die wordt aangeroepen wanneer de opdracht wordt uitgevoerd (bijvoorbeeld als reactie op een klik op een knop). De tweede parameter is een gedelegeerde die een booleaanse waarde retourneert die aangeeft of de opdracht kan worden uitgevoerd; in dit voorbeeld is het aangesloten op de eigenschap IsValid het IsValid . Wanneer dit false retourneert, wordt de knop of het menu-item uitgeschakeld dat aan deze opdracht is gebonden (andere besturingselementen kunnen zich anders gedragen). Dit is een eenvoudige maar effectieve functie, waardoor het niet nodig is om code te schrijven om besturingselementen op basis van verschillende omstandigheden in of uit te schakelen.

Als je toch dit voorbeeld up and running, probeer het legen uit een van de TextBox es (om het te plaatsen Customer model in een ongeldige status). Wanneer u weggaat van de TextBox zult u merken dat de knop "Toepassen" wordt uitgeschakeld.

Opmerking over het maken van klanten

Het view-model implementeert INotifyPropertyChanged (INPC) niet. Dit betekent dat als een ander Customer zou worden toegewezen aan de eigenschap CustomerToEdit , de besturingselementen van de weergave niet zouden veranderen om het nieuwe object weer te geven - de TextBox es zouden nog steeds de voor- en achternaam van de vorige klant bevatten.

De voorbeeldcode werkt omdat de Customer is gemaakt in de constructor van het view-model, voordat deze wordt toegewezen aan de DataContext van de view (op welk punt de bindingen worden aangesloten). In een echte toepassing haalt u mogelijk klanten uit een database op andere manieren dan de constructor. Om dit te ondersteunen, moet de VM INPC implementeren en moet de eigenschap CustomerToEdit worden gewijzigd om het "uitgebreide" getter- en setterpatroon te gebruiken dat u in de voorbeeldmodelcode ziet, waardoor de gebeurtenis PropertyChanged in de setter wordt verhoogd.

De view-model ApplyChangesCommand hoeft niet te implementeren INPC als de opdracht is zeer waarschijnlijk niet veranderen. U zou dit patroon moeten implementeren als u de opdracht ergens anders dan de constructor zou maken, bijvoorbeeld een methode Initialize() .

De algemene regel is: implementeer INPC als de eigenschap is gebonden aan weergavebesturingselementen en de waarde van de eigenschap ergens anders dan in de constructor kan worden gewijzigd. U hoeft INPC niet te implementeren als de eigenschapswaarde alleen ooit in de constructor wordt toegewezen (en u bespaart uzelf wat typen in het proces).

Het model

Het model is de eerste "M" in M VVM. Het model is meestal een klasse die de gegevens bevat die u via een soort gebruikersinterface wilt blootleggen.

Hier is een zeer eenvoudige modelklasse die enkele eigenschappen blootstelt: -

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

Deze klasse implementeert de INotifyPropertyChanged interface die een PropertyChanged gebeurtenis blootstelt. Deze gebeurtenis moet worden gegenereerd wanneer een van de eigenschapswaarden verandert - u kunt dit in actie in de bovenstaande code zien. De gebeurtenis PropertyChanged is een belangrijk onderdeel van de WPF-gegevensbindende mechanismen, omdat zonder deze interface de gebruikersinterface niet de wijzigingen in de waarde van een eigenschap zou kunnen weerspiegelen.

Het model bevat ook een zeer eenvoudige validatieroutine die wordt aangeroepen door de bezetters. Het stelt een openbare eigenschap in die aangeeft of het model een geldige status heeft. Ik heb deze functionaliteit opgenomen om een "speciale" functie van WPF- opdrachten te demonstreren, die u binnenkort zult zien. Het WPF-framework biedt een aantal meer geavanceerde benaderingen van validatie, maar deze vallen buiten het bestek van dit artikel .

Het uitzicht

De weergave is de "V" in M V VM. Dit is uw gebruikersinterface. U kunt de Visual Studio-ontwerper gebruiken voor slepen en neerzetten, maar de meeste ontwikkelaars coderen uiteindelijk voor de onbewerkte XAML - een ervaring die vergelijkbaar is met het schrijven van HTML.

Hier is de XAML van een eenvoudige weergave, zodat het bewerken van een Customer model. In plaats van een nieuwe weergave te maken, kan deze gewoon in het MainWindow.xaml bestand van een WPF-project worden geplakt, tussen de tags <Window ...> en </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>

Deze code maakt een eenvoudig gegevensinvoerformulier dat bestaat uit twee TextBox es - een voor de voornaam van de klant en een voor de achternaam. Boven elke TextBox staat een Label en onderaan het formulier een Button 'Toepassen'.

Zoek de eerste TextBox en bekijk de Text eigenschap:

Text="{Binding CustomerToEdit.Forename}"

In plaats van de tekst van de TextBox op een vaste waarde in te stellen, bindt deze speciale accolade met accolades in plaats daarvan de tekst aan het "pad" CustomerToEdit.Forename . Waar heeft dit pad betrekking op? Het is de "data context" van de view - in dit geval ons view-model. Het bindingspad, zoals u wellicht kunt achterhalen, is de eigenschap CustomerToEdit het view-model, die van het type Customer op zijn beurt een eigenschap met de naam Forename blootstelt - vandaar de "gestippelde" padnotatie.

Als u naar de XAML van de Button kijkt, heeft deze ook een Command die gebonden is aan de eigenschap ApplyChangesCommand van het view-model. Dat is alles wat nodig is om een knop aan te sluiten op de opdracht van de VM.

De gegevenscontext

Dus hoe stel je het view-model in op de data-context van de view? Een manier is om het in te stellen in de "code-behind" van de view. Druk op F7 om dit codebestand te bekijken en voeg een regel toe aan de bestaande constructor om een exemplaar van het view-model te maken en toe te wijzen aan de eigenschap DataContext van het venster. Het zou er zo uit moeten zien:

    public MainWindow()
    {
        InitializeComponent();

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

In echte systemen worden vaak andere benaderingen gebruikt om het weergavemodel te maken, zoals afhankelijkheidsinjectie of MVVM-frameworks.

Commandant in MVVM

Opdrachten worden gebruikt voor het afhandelen van Events in WPF met inachtneming van het MVVM-patroon.

Een normale EventHandler ziet er zo uit (bevindt zich in Code-Behind ):

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

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

Nee, om hetzelfde te doen in MVVM gebruiken we Commands :

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

Ik raad aan om een soort voorvoegsel ( Cmd ) te gebruiken voor je opdrachteigenschappen, omdat je ze vooral in xaml nodig hebt - op die manier zijn ze gemakkelijker te herkennen.

Omdat het MVVM is, wilt u dat commando (voor Button "eq" Button_Click ) in uw ViewModel Button_Click .

Daarvoor hebben we eigenlijk twee dingen nodig:

  1. System.Windows.Input.ICommand
  2. RelayCommand (bijvoorbeeld hier vandaan genomen) .

Een eenvoudig voorbeeld zou er zo uit kunnen zien:

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

Dus wat doet dit in detail:

Het ICommand is waar het Control in xaml aan bindt. De RelayCommand uw opdracht naar een Action (dwz een Method aanroepen). De Null-Check zorgt er alleen voor dat elke Command slechts één keer wordt geïnitialiseerd (vanwege prestatieproblemen). Als je de link voor de RelayCommand hierboven hebt gelezen, heb je misschien gemerkt dat RelayCommand twee overbelastingen heeft voor de constructor. (Action<object> execute) en (Action<object> execute, Predicate<object> canExecute) .

Dat betekent dat je (aanvullend) een tweede Method die een bool retourneert om te vertellen dat Control de "gebeurtenis" kan worden geactiveerd of niet.

Een goede zaak daarvoor is dat Button s bijvoorbeeld Enabled="false" als de Method false retourneert

CommandParameters

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

In dit voorbeeld wil ik de DataGrid.SelectedItem doorgeven aan de Click_Command in mijn ViewModel.

Uw methode zou er zo uit moeten zien terwijl de ICommand-implementatie zelf zoals hierboven blijft.

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
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow