Szukaj…


Uwagi

Modele i modele widoków

Definicja modelu jest często przedmiotem gorących dyskusji, a linia między modelem a modelem widoku może być rozmyta. Niektórzy wolą nie „zanieczyścić” swoich modeli z INotifyPropertyChanged interfejsu, a zamiast powielać właściwości modelu w widoku model, który implementuje ten interfejs. Podobnie jak wiele rzeczy w rozwoju oprogramowania, nie ma dobrej lub złej odpowiedzi. Bądź pragmatyczny i rób wszystko, co uważasz za słuszne.

Zobacz separację

Intencją MVVM jest rozdzielenie tych trzech odrębnych obszarów - Model, model widoku i widok. Chociaż widok może uzyskiwać dostęp do modelu widoku (VM) i (pośrednio) modelu, najważniejszą zasadą w MVVM jest to, że maszyna wirtualna nie powinna mieć dostępu do widoku ani jego kontrolek. Maszyna wirtualna powinna udostępniać wszystko, czego potrzebuje widok, za pośrednictwem właściwości publicznych. Maszyna wirtualna nie powinna bezpośrednio ujawniać ani manipulować kontrolkami interfejsu użytkownika, takimi jak TextBox , Button itp.

W niektórych przypadkach taka ścisła separacja może być trudna w obsłudze, szczególnie jeśli potrzebujesz uruchomić złożoną funkcjonalność interfejsu użytkownika. W tym przypadku całkowicie dopuszczalne jest używanie zdarzeń i procedur obsługi zdarzeń w pliku „zakodowanym” w widoku. Jeśli jest to czysto funkcjonalność interfejsu użytkownika, to z całą pewnością wykorzystuj zdarzenia w widoku. Dopuszczalne jest również, aby te procedury obsługi zdarzeń wywoływały metody publiczne w wystąpieniu maszyny wirtualnej - po prostu nie przechodź do odwołań do elementów sterujących interfejsu użytkownika itp.

RelayCommand

Niestety klasa RelayCommand zastosowana w tym przykładzie nie jest częścią frameworka WPF (tak powinno być!), Ale znajdziesz go w prawie każdym zestawie narzędzi programisty WPF. Szybkie wyszukiwanie online ujawni wiele fragmentów kodu, które możesz podnieść, aby stworzyć swój własny.

Przydatną alternatywą dla RelayCommand jest ActionCommand który jest dostarczany jako część Microsoft.Expression.Interactivity.Core który zapewnia porównywalną funkcjonalność.

Podstawowy przykład MVVM z wykorzystaniem WPF i C #

To podstawowy przykład użycia modelu MVVM w aplikacji komputerowej dla systemu Windows przy użyciu WPF i C #. Przykładowy kod implementuje proste okno dialogowe „informacje o użytkowniku”.

wprowadź opis zdjęcia tutaj

Widok

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>

i kod z tyłu

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

Model widoku

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

Model

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

    public string LastName { get; set; }

    public DateTime BirthDate { get; set; }
} 

Model widoku

Model widoku to „VM” w MV VM . Jest to klasa, która działa jako pośrednik, udostępnia model (modele) interfejsowi użytkownika (widok) i obsługuje żądania z widoku, takie jak polecenia wywoływane przez kliknięcie przycisku. Oto podstawowy model widoku:

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

Konstruktor tworzy obiekt modelu Customer i przypisuje go do właściwości CustomerToEdit , aby był widoczny dla widoku.

Konstruktor tworzy również obiekt RelayCommand i przypisuje go do właściwości ApplyChangesCommand , ponownie czyniąc go widocznym dla widoku. Polecenia WPF są używane do obsługi żądań z widoku, takich jak kliknięcia przycisku lub elementu menu.

RelayCommand przyjmuje dwa parametry - pierwszy to delegat, który jest wywoływany po wykonaniu polecenia (np. W odpowiedzi na kliknięcie przycisku). Drugi parametr to delegat, który zwraca wartość logiczną wskazującą, czy polecenie może zostać wykonane; w tym przykładzie jest podłączony do właściwości IsValid obiektu klienta. Gdy zwraca wartość false, wyłącza przycisk lub element menu związany z tym poleceniem (inne elementy sterujące mogą zachowywać się inaczej). Jest to prosta, ale skuteczna funkcja, pozwalająca uniknąć konieczności pisania kodu w celu włączenia lub wyłączenia sterowania w zależności od różnych warunków.

Jeśli uruchomisz ten przykład, spróbuj opróżnić jeden z TextBox (aby ustawić model Customer w nieprawidłowym stanie). Po tabulacji z dala od TextBox powinieneś zauważyć, że przycisk „Zastosuj” staje się nieaktywny.

Uwaga na temat tworzenia klientów

Model widoku nie implementuje INotifyPropertyChanged (INPC). Oznacza to, że jeśli inny obiekt Customer miałby zostać przypisany do właściwości CustomerToEdit wówczas formanty widoku nie zmieniłyby się w celu odzwierciedlenia nowego obiektu - TextBox nadal zawierałoby imię i nazwisko poprzedniego klienta.

Przykładowy kod działa, ponieważ Customer jest tworzony w konstruktorze modelu widoku, zanim zostanie przypisany do DataContext widoku (w którym momencie powiązania są połączone). W rzeczywistej aplikacji możesz pobierać klientów z bazy danych metodami innymi niż konstruktor. Aby to obsłużyć, maszyna wirtualna powinna zaimplementować INPC, a właściwość CustomerToEdit powinna zostać zmieniona w celu użycia „rozszerzonego” wzorca pobierającego i ustawiającego, który widzisz w przykładowym kodzie modelu, podnosząc zdarzenie PropertyChanged w ustawiającym.

ApplyChangesCommand w modelu widoku nie musi implementować INPC, ponieważ zmiana polecenia jest mało prawdopodobna. Trzeba by zaimplementować ten wzorzec, jeśli tworzysz polecenie w innym miejscu niż konstruktor, na przykład jakiś rodzaj metody Initialize() .

Ogólna zasada brzmi: zaimplementuj INPC, jeśli właściwość jest powiązana z dowolnymi kontrolkami widoku, a wartość właściwości może się zmieniać gdziekolwiek poza konstruktorem. Nie musisz implementować INPC, jeśli wartość właściwości jest zawsze przypisywana tylko w konstruktorze (a zaoszczędzisz sobie trochę pisania w tym procesie).

Model

Model jest pierwszym „M” w M VVM. Model jest zwykle klasą zawierającą dane, które chcesz udostępnić za pomocą interfejsu użytkownika.

Oto bardzo prosta klasa modelu pokazująca kilka właściwości: -

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

Ta klasa implementuje interfejs INotifyPropertyChanged , który udostępnia zdarzenie PropertyChanged . To zdarzenie powinno być wywoływane za każdym razem, gdy zmienia się jedna z wartości właściwości - możesz to zobaczyć w działaniu w powyższym kodzie. Zdarzenie PropertyChanged jest kluczowym elementem mechanizmów powiązania danych WPF, ponieważ bez niego interfejs użytkownika nie byłby w stanie odzwierciedlić zmian dokonanych w wartości właściwości.

Model zawiera również bardzo prostą procedurę sprawdzania poprawności, która jest wywoływana z ustawiaczy właściwości. Ustawia właściwość publiczną wskazującą, czy model jest w poprawnym stanie. Dołączyłem tę funkcję, aby zademonstrować „specjalną” funkcję poleceń WPF, którą wkrótce zobaczycie. Struktura WPF zapewnia szereg bardziej wyrafinowanych podejść do sprawdzania poprawności, ale są one poza zakresem tego artykułu .

Widok

Widok jest „V” w M V VM. To jest twój interfejs użytkownika. Możesz użyć projektanta przeciągania i upuszczania programu Visual Studio, ale większość programistów ostatecznie koduje surowy XAML - doświadczenie podobne do pisania HTML.

Oto XAML prostego widoku umożliwiającego edycję modelu Customer . Zamiast tworzyć nowy widok, można go po prostu wkleić do pliku MainWindow.xaml projektu WPF, pomiędzy <Window ...> i </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>

Ten kod tworzy prosty formularz wprowadzania danych składający się z dwóch es TextBox - jednej dla imienia klienta i jednej dla nazwiska. Jest Label nad każdym TextBox , i „Zastosuj” Button w dolnej części formularza.

Znajdź pierwszy TextBox i spójrz na jego właściwość Text :

Text="{Binding CustomerToEdit.Forename}"

Zamiast ustawiać tekst w TextBox na stałą wartość, ta specjalna składnia nawiasów klamrowych wiąże tekst z „ścieżką” CustomerToEdit.Forename . W stosunku do tej ścieżki? To „kontekst danych” widoku - w tym przypadku nasz model widoku. Ścieżka wiązania, jak można się domyślić, jest właściwością CustomerToEdit modelu widoku, która jest typu Customer która z kolei ujawnia właściwość o nazwie Forename - stąd zapis „kropkowanej” ścieżki.

Podobnie, jeśli spojrzeć na Button „s XAML, że ma Command , które jest związane z ApplyChangesCommand własność view-modelu. To wszystko, czego potrzeba, aby podłączyć przycisk do polecenia maszyny wirtualnej.

DataContext

Jak więc ustawić model widoku jako kontekst danych widoku? Jednym ze sposobów jest ustawienie go w „zakodowanym” widoku. Naciśnij klawisz F7, aby wyświetlić ten plik kodu, i dodaj wiersz do istniejącego konstruktora, aby utworzyć instancję modelu widoku i przypisać go do właściwości DataContext okna. Powinno to wyglądać następująco:

    public MainWindow()
    {
        InitializeComponent();

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

W systemach rzeczywistych często stosuje się inne podejścia do stworzenia modelu widoku, takie jak wstrzykiwanie zależności lub struktury MVVM.

Dowodzenie w MVVM

Polecenia są używane do obsługi Events w WPF przy jednoczesnym przestrzeganiu wzorca MVVM.

Normalny EventHandler wyglądałby tak (znajduje się w Code-Behind ):

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

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

Nie, aby zrobić to samo w MVVM, używamy Commands :

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

Polecam użycie pewnego rodzaju prefiksu ( Cmd ) dla twoich właściwości poleceń, ponieważ będziesz potrzebował ich głównie w Xaml - w ten sposób łatwiej je rozpoznać.

Ponieważ jest to MVVM, chcesz obsłużyć to polecenie (dla Button „eq” Button_Click ) w ViewModel .

W tym celu potrzebujemy dwóch rzeczy:

  1. System.Windows.Input.ICommand
  2. RelayCommand (na przykład wzięty stąd .

Prosty przykład może wyglądać następująco:

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

Co to dokładnie robi:

ICommand jest tym, z czym wiąże się Control w xaml. RelayCommand przekieruje twoje polecenie do Action (tj. RelayCommand Method ). Kontrola zerowa zapewnia tylko, że każde Command zostanie zainicjowane tylko raz (z powodu problemów z wydajnością). Jeśli przeczytałeś link do RelayCommand powyżej, być może zauważyłeś, że RelayCommand ma dwa przeciążenia dla swojego konstruktora. (Action<object> execute) i (Action<object> execute, Predicate<object> canExecute) (Action<object> execute) (Action<object> execute, Predicate<object> canExecute) .

Oznacza to, że możesz (dodatkowo) dodać drugą Method zwracającą wartość bool aby powiedzieć, że Control kiedy „Zdarzenie” może odpalić, czy nie.

Dobrą rzeczą jest to, że na przykład Button s zostanie Enabled="false" jeśli Method zwróci false

CommandParameters

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

W tym przykładzie chcę przekazać DataGrid.SelectedItem do Click_Command w moim ViewModel.

Twoja metoda powinna wyglądać tak, podczas gdy sama implementacja ICommand pozostaje jak wyżej.

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
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow