Sök…
Anmärkningar
Modeller och visningsmodeller
Definitionen av en modell diskuteras ofta varmt, och linjen mellan en modell och en visningsmodell kan vara suddig. Vissa föredrar att inte "förorenar" sina modeller med INotifyPropertyChanged
gränssnittet och istället duplicera modellegenskaper i vyn-modellen, som gör genomföra detta gränssnitt. Liksom många saker inom mjukvaruutveckling finns det inget rätt eller fel svar. Var pragmatisk och gör vad som känns rätt.
Visa separation
MVVM: s avsikt är att skilja dessa tre distinkta områden - Model, view-model och View. Även om det är acceptabelt för vyn att få åtkomst till visningsmodellen (VM) och (indirekt) modellen, är den viktigaste regeln med MVVM att VM inte ska ha tillgång till vyn eller dess kontroller. VM ska exponera allt som vyn behöver via offentliga fastigheter. VM ska inte direkt exponera eller manipulera UI-kontroller som TextBox
, Button
etc.
I vissa fall kan den här stränga separationen vara svår att arbeta med, särskilt om du behöver få lite komplex UI-funktionalitet igång. Här är det helt acceptabelt att använda händelser och händelseshanterare i vyn "kod bakom" -filen. Om det är rent UI-funktionalitet använder du på alla sätt händelser i vyn. Det är också acceptabelt för dessa evenemangshanterare att ringa offentliga metoder på VM-instansen - gå bara inte vidare med referenser till UI-kontroller eller något liknande.
RelayCommand
Tyvärr är RelayCommand
klassen som används i detta exempel inte en del av WPF-ramverket (det borde ha varit!), Men du hittar det i nästan alla WPF-utvecklarens verktygslåda. En snabb sökning online kommer att avslöja många kodavsnitt som du kan lyfta för att skapa dina egna.
Ett användbart alternativ till RelayCommand
är ActionCommand
som tillhandahålls som en del av Microsoft.Expression.Interactivity.Core
som ger jämförbar funktionalitet.
Grundläggande MVVM-exempel med WPF och C #
Detta är ett grundläggande exempel för att använda MVVM-modellen i ett Windows-skrivbordsapplikation med WPF och C #. Exempelkoden implementerar en enkel "användarinfo" -dialog.
Vyn
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>
och koden bakom
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;
}
}
Vynmodellen
// 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));
}
}
}
Modellen
sealed class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
Visa-modellen
Visningsmodellen är "VM" i MV VM . Detta är en klass som fungerar som ett mellanrum, exponerar modellen / modellerna för användargränssnittet (vyn) och hanterar förfrågningar från vyn, till exempel kommandon som höjs med knappklick. Här är en grundläggande visningsmodell:
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
}
}
Konstruktören skapar ett Customer
och tilldelar det egenskapen CustomerToEdit
så att det är synligt för vyn.
Konstruktören skapar också ett RelayCommand
objekt och tilldelar det till egenskapen ApplyChangesCommand
, vilket återigen gör det synligt för vyn. WPF-kommandon används för att hantera förfrågningar från vyn, t.ex. klick på knapp eller menyalternativ.
RelayCommand
tar två parametrar - den första är delegaten som kommer att ringas när kommandot körs (t.ex. som svar på ett knappklick). Den andra parametern är en delegat som returnerar ett booleskt värde som indikerar om kommandot kan köra; i det här exemplet är det kopplat till IsValid
egenskap. När detta returnerar falskt inaktiverar det knappen eller menyposten som är bunden till detta kommando (andra kontroller kan uppträda annorlunda). Detta är en enkel men effektiv funktion som undviker behovet av att skriva kod för att aktivera eller inaktivera kontroller baserat på olika förhållanden.
Om du kör det här exemplet kan du försöka tömma en av TextBox
(för att placera Customer
i ogiltigt tillstånd). När du tabbar bort från TextBox
bör du upptäcka att knappen "Apply" blir inaktiverad.
Anmärkning på skapande av kunder
INotifyPropertyChanged
implementerar inte INotifyPropertyChanged
(INPC). Detta innebär att om ett annat Customer
skulle tilldelas egenskapen CustomerToEdit
så skulle vyns kontroller inte ändras för att återspegla det nya objektet - TextBox
es skulle fortfarande innehålla förnamn och efternamn till den tidigare kunden.
Exempelkoden fungerar eftersom Customer
skapas i visningsmodellens konstruktör, innan den tilldelas visningens DataContext
(vid vilken punkt bindningarna är kopplade). I en verklig applikation hämtar du kanske kunder från en databas på andra metoder än konstruktören. För att stödja detta bör VM implementera INPC, och egenskapen CustomerToEdit
bör ändras för att använda det "utvidgade" getter- och settermönstret som du ser i exemplet Modelkod, höja PropertyChanged
händelsen i setern.
ApplyChangesCommand
's ApplyChangesCommand
behöver inte implementera INPC eftersom kommandot är mycket osannolikt att ändras. Du skulle behöva implementera detta mönster om du skapade kommandot någon annanstans än konstruktören, till exempel någon form av Initialize()
-metod.
Den allmänna regeln är: implementera INPC om egenskapen är bunden till någon vykontroll och egenskapens värde kan ändras någon annanstans än i konstruktören. Du behöver inte implementera INPC om fastighetsvärdet endast tilldelas i konstruktören (och du sparar dig själv att skriva in processen).
Modellen
Modellen är den första "M" i M VVM. Modellen är vanligtvis en klass som innehåller de data som du vill exponera via någon form av användargränssnitt.
Här är en mycket enkel modellklass som exponerar ett par egenskaper: -
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));
}
}
Denna klass implementerar gränssnittet INotifyPropertyChanged
som visar en PropertyChanged
händelse. Denna händelse bör höjas när någon av fastighetsvärdena ändras - du kan se detta i handling i koden ovan. Händelsen PropertyChanged
är en nyckel i WPF-databindningsmekanismerna, eftersom utan den skulle användargränssnittet inte kunna spegla de ändringar som gjorts i en fastighets värde.
Modellen innehåller också en mycket enkel valideringsrutin som kommer att ringas från fastighetsinstituten. Den anger en offentlig egendom som anger om modellen är i giltigt skick eller inte. Jag har inkluderat den här funktionen för att visa en "speciell" funktion i WPF- kommandon , som du snart kommer att se. WPF-ramverket tillhandahåller ett antal mer sofistikerade metoder för validering, men dessa ligger utanför denna artikel .
Vyn
Vyn är "V" i M V VM. Detta är ditt användargränssnitt. Du kan använda Visual Studio-drag-and-drop-designern, men de flesta utvecklare slutligen kodar den råa XAML - en upplevelse som liknar att skriva HTML.
Här är XAML för en enkel vy för att tillåta redigering av en Customer
. I stället för att skapa en ny vy kan det bara klistras in i ett WPF-projekt MainWindow.xaml
fil, mellan <Window ...>
och </Window>
taggarna: -
<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>
Denna kod skapar en enkel datainmatningsformulär som består av två TextBox
es - en för kundens förnamn och en för efternamnet. Det finns en Label
ovanför varje TextBox
och en "Apply" Button
längst ner i formuläret.
Leta TextBox
den första TextBox
och titta på dess Text
egenskap:
Text="{Binding CustomerToEdit.Forename}"
I stället för att ställa TextBox
texten till ett fast värde, binder denna speciella lockiga stag-syntax istället texten till "sökvägen" CustomerToEdit.Forename
. Vad är den här vägen relativt? Det är vyns "datakontext" - i det här fallet vår visningsmodell. Den bindande sökvägen, som du kanske kan räkna ut, är visningsmodellens egendom CustomerToEdit
, som är av typen Customer
som i sin tur exponerar en egenskap som heter Forename
- därmed den "prickade" söknotationen.
På samma sätt, om du tittar på Button
XAML, har den en Command
som är bunden till egenskapen ApplyChangesCommand
i visningsmodellen. Det är allt som behövs för att få en knapp till VM: s kommando.
DataContext
Så hur ställer du in visningsmodellen som vyns datakontext? Ett sätt är att ställa in det i visningens "kod bakom". Tryck på F7 för att se den här kodfilen och lägga till en rad i den befintliga konstruktören för att skapa en instans av visningsmodellen och tilldela den till fönstret DataContext
egenskap. Det skulle se ut så här:
public MainWindow()
{
InitializeComponent();
// Our new line:-
DataContext = new CustomerEditViewModel();
}
I verkliga världssystem används ofta andra tillvägagångssätt för att skapa visningsmodellen, såsom beroendeinjektion eller MVVM-ramverk.
Kommande i MVVM
Kommandon används för att hantera Events
i WPF med respekt för MVVM-mönstret.
En normal EventHandler
skulle se ut så här (ligger i Code-Behind
):
public MainWindow()
{
_dataGrid.CollectionChanged += DataGrid_CollectionChanged;
}
private void DataGrid_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//Do what ever
}
Nej för att göra samma sak i MVVM vi använder Commands
:
<Button Command="{Binding Path=CmdStartExecution}" Content="Start" />
Jag rekommenderar att du använder ett slags prefix (
Cmd
) för dina kommandoegenskaper, eftersom du främst kommer att behöva dem i xaml - på det sättet är de lättare att känna igen.
Eftersom det är MVVM vill du hantera det kommandot (för Button
"eq" Button_Click
) i din ViewModel
.
För det behöver vi i princip två saker:
- System.Windows.Input.ICommand
- RelayCommand (t.ex. härifrån .
Ett enkelt exempel kan se ut så här:
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.
}
Så vad gör detta i detalj:
ICommand
är vad Control
i xaml är bindande för. RelayCommand
ditt kommando till en Action
(dvs. ring en Method
). Nullkontrollen ser bara till att varje Command
bara initialiseras en gång (på grund av prestandafrågor). Om du har läst länken till RelayCommand
ovan kan du ha märkt att RelayCommand
har två överbelastningar för sin konstruktör. (Action<object> execute)
och (Action<object> execute, Predicate<object> canExecute)
.
Det innebär att du (additionellt) kan lägga till en andra Method
returnerar en bool
att berätta att Control
wheather "Event" kan skjuta eller inte.
En bra sak för det är att Button
till exempel är Enabled="false"
om Method
kommer att returnera false
CommandParameters
<DataGrid x:Name="TicketsDataGrid">
<DataGrid.InputBindings>
<MouseBinding Gesture="LeftDoubleClick"
Command="{Binding CmdTicketClick}"
CommandParameter="{Binding ElementName=TicketsDataGrid,
Path=SelectedItem}" />
</DataGrid.InputBindings>
<DataGrid />
I det här exemplet vill jag överföra DataGrid.SelectedItem
till Click_Command i min ViewModel.
Din metod ska se ut så här medan ICommand-implementeringen förblir som ovan.
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
}
}