Buscar..


Observaciones

Modelos y modelos de vista

La definición de un modelo a menudo se debate ardientemente, y la línea entre un modelo y un modelo de vista se puede difuminar. Algunos prefieren no "contaminar" sus modelos con el INotifyPropertyChanged interfaz, y en lugar de duplicar las del modelo en la vista-modelo, que hace implementar esta interfaz. Como muchas cosas en el desarrollo de software, no hay una respuesta correcta o incorrecta. Sea pragmático y haga lo que se sienta bien.

Ver separación

La intención de MVVM es separar esas tres áreas distintas: Modelo, modelo de vista y Vista. Si bien es aceptable para la vista acceder al modelo de vista (VM) y (indirectamente) al modelo, la regla más importante con MVVM es que la VM no debe tener acceso a la vista ni a sus controles. La máquina virtual debe exponer todo lo que la vista necesita, a través de propiedades públicas. La máquina virtual no debe exponer ni manipular directamente los controles de la IU, como TextBox , Button , etc.

En algunos casos, puede ser difícil trabajar con esta separación estricta, especialmente si necesita poner en funcionamiento alguna funcionalidad de interfaz de usuario compleja. Aquí, es perfectamente aceptable recurrir al uso de eventos y controladores de eventos en el archivo de "código subyacente" de la vista. Si se trata de una funcionalidad de UI pura, entonces utilice los eventos en la vista. También es aceptable que estos manejadores de eventos llamen a métodos públicos en la instancia de VM, simplemente no le pasen referencias a los controles de UI ni nada de eso.

RelayCommand

Desafortunadamente, la clase RelayCommand utilizada en este ejemplo no forma parte del marco de trabajo de WPF (¡debería haberlo sido!), Pero lo encontrará en casi todas las cajas de herramientas para desarrolladores de WPF. Una búsqueda rápida en línea revelará muchos fragmentos de código que puede levantar, para crear el suyo propio.

Una alternativa útil a RelayCommand es ActionCommand que se proporciona como parte de Microsoft.Expression.Interactivity.Core que proporciona una funcionalidad comparable.

Ejemplo básico de MVVM usando WPF y C #

Este es un ejemplo básico para usar el modelo MVVM en una aplicación de escritorio de Windows, usando WPF y C #. El código de ejemplo implementa un simple diálogo de "información del usuario".

introduzca la descripción de la imagen aquí

La vista

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

y el código detrás

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

El modelo de 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));
        }
    }
}

El modelo

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

    public string LastName { get; set; }

    public DateTime BirthDate { get; set; }
} 

El modelo de vista

El modelo de vista es la "VM" en MV VM . Esta es una clase que actúa como intermediario, expone los modelos a la interfaz de usuario (vista) y maneja las solicitudes desde la vista, como los comandos generados por los clics de los botones. Aquí hay un modelo de vista básico:

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

El constructor crea un objeto de modelo de Customer y lo asigna a la propiedad CustomerToEdit , para que sea visible para la vista.

El constructor también crea un objeto RelayCommand y lo asigna a la propiedad ApplyChangesCommand , haciéndolo nuevamente visible para la vista. Los comandos de WPF se utilizan para manejar solicitudes desde la vista, como los clics en los botones o en los elementos del menú.

RelayCommand toma dos parámetros: el primero es el delegado al que se llama cuando se ejecuta el comando (por ejemplo, en respuesta a un clic del botón). El segundo parámetro es un delegado que devuelve un valor booleano que indica si el comando puede ejecutarse; en este ejemplo, está conectado a la propiedad IsValid del objeto del IsValid . Cuando esto devuelve falso, desactiva el botón o elemento de menú que está vinculado a este comando (otros controles pueden comportarse de manera diferente). Esta es una característica simple pero efectiva, evitando la necesidad de escribir código para habilitar o deshabilitar controles basados ​​en diferentes condiciones.

Si tiene este ejemplo en funcionamiento, intente vaciar uno de los TextBox de TextBox (para colocar el modelo del Customer en un estado no válido). Cuando se aleje del TextBox , debería encontrar que el botón "Aplicar" se deshabilita.

Comentario sobre la creación del cliente

El modelo de vista no implementa INotifyPropertyChanged (INPC). Esto significa que si se asignara un objeto Customer diferente a la propiedad CustomerToEdit , entonces los controles de la vista no cambiarían para reflejar el nuevo objeto; el TextBox aún contendría el nombre y el apellido del cliente anterior.

El código de ejemplo funciona porque el Customer se crea en el constructor del modelo de vista, antes de que se asigne al DataContext la vista (en cuyo punto se conectan los enlaces). En una aplicación del mundo real, podría estar recuperando clientes de una base de datos en métodos distintos al constructor. Para admitir esto, la VM debe implementar INPC, y la propiedad CustomerToEdit debe cambiarse para usar el patrón "establecido" de obtención y establecimiento que se ve en el código de modelo de ejemplo, lo que provoca el evento PropertyChanged en el configurador.

El ApplyChangesCommand del modelo de ApplyChangesCommand no necesita implementar INPC ya que es muy poco probable que el comando cambie. Lo que se necesita para implementar este patrón si estuviera creando un lugar del comando que no sea el constructor, por ejemplo, algún tipo de Initialize() método.

La regla general es: implementar INPC si la propiedad está vinculada a cualquier control de vista y el valor de la propiedad puede cambiar en cualquier lugar que no sea el constructor. No necesita implementar INPC si el valor de la propiedad solo se asigna en el constructor (y se ahorrará algo de escritura en el proceso).

El modelo

El modelo es la primera "M" en M VVM. El modelo suele ser una clase que contiene los datos que desea exponer a través de algún tipo de interfaz de usuario.

Aquí hay una clase de modelo muy simple que expone un par de propiedades:

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

Esta clase implementa la interfaz INotifyPropertyChanged que expone un evento PropertyChanged . Este evento debe producirse siempre que cambie uno de los valores de propiedad; puede verlo en acción en el código anterior. El evento PropertyChanged es una pieza clave en los mecanismos de enlace de datos de WPF, ya que sin él, la interfaz de usuario no podría reflejar los cambios realizados en el valor de una propiedad.

El modelo también contiene una rutina de validación muy simple a la que se llama desde los establecedores de propiedades. Establece una propiedad pública que indica si el modelo está o no en un estado válido. He incluido esta funcionalidad para demostrar una característica "especial" de los comandos de WPF, que veremos en breve. El marco WPF proporciona una serie de enfoques más sofisticados para la validación, pero están fuera del alcance de este artículo .

La vista

La vista es la "V" en M V VM. Esta es su interfaz de usuario. Puede usar el diseñador de arrastrar y soltar de Visual Studio, pero la mayoría de los desarrolladores finalmente terminan codificando el XAML en bruto, una experiencia similar a la de escribir HTML.

Aquí está el XAML de una vista simple para permitir la edición de un modelo de Customer . En lugar de crear una nueva vista, esto solo se puede pegar en el archivo MainWindow.xaml un proyecto WPF, entre las etiquetas <Window ...> y </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>

Este código crea un formulario de entrada de datos simple que consta de dos TextBox es: uno para el nombre del cliente y otro para el apellido. Hay una Label encima de cada TextBox y un Button "Aplicar" en la parte inferior del formulario.

Localiza el primer TextBox y mira su propiedad de Text :

Text="{Binding CustomerToEdit.Forename}"

En lugar de establecer el TextBox del TextBox en un valor fijo, esta sintaxis especial de corchete es el enlace del texto a la "ruta" CustomerToEdit.Forename . ¿A qué se refiere este camino? Es el "contexto de datos" de la vista, en este caso, nuestro modelo de vista. La ruta de enlace, como podrá descubrir, es la propiedad CustomerToEdit del modelo de vista, que es de tipo Customer que a su vez expone una propiedad llamada Forename - por lo tanto, la notación de ruta "punteada".

De manera similar, si observa la XAML del Button , tiene un Command que está vinculado a la propiedad ApplyChangesCommand del modelo de vista. Eso es todo lo que se necesita para conectar un botón al comando de la máquina virtual.

El DataContext

Entonces, ¿cómo configura el modelo de vista para que sea el contexto de datos de la vista? Una forma es configurarlo en el "código subyacente" de la vista. Presione F7 para ver este archivo de código y agregue una línea al constructor existente para crear una instancia del modelo de vista y asignarla a la propiedad DataContext la ventana. Debería acabar luciendo así:

    public MainWindow()
    {
        InitializeComponent();

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

En los sistemas del mundo real, a menudo se utilizan otros enfoques para crear el modelo de vista, como la inyección de dependencias o los marcos MVVM.

Comandando en MVVM

Los comandos se utilizan para manejar Events en WPF respetando el patrón MVVM.

Un EventHandler normal se vería así (ubicado en Code-Behind ):

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

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

No para hacer lo mismo en MVVM usamos Commands :

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

Recomiendo usar algún tipo de prefijo ( Cmd ) para las propiedades de tus comandos, porque principalmente los necesitarás en xaml, de esa forma son más fáciles de reconocer.

Ya que es MVVM, usted quiere manejar ese comando (para el Button "eq" Button_Click ) en su ViewModel .

Para eso básicamente necesitamos dos cosas:

  1. System.Windows.Input.ICommand
  2. RelayCommand (por ejemplo, tomado de aquí .

Un ejemplo simple podría verse así:

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

Entonces, ¿qué está haciendo esto en detalle:

El ICommand es a lo que se vincula el Control en xaml. RelayCommand dirigirá su comando a una Action (es decir, llama a un Method ). La comprobación de nulos solo garantiza que cada Command solo se inicializará una vez (debido a problemas de rendimiento). Si ha leído el enlace de RelayCommand anterior, puede haber notado que RelayCommand tiene dos sobrecargas para su constructor. (Action<object> execute) y (Action<object> execute, Predicate<object> canExecute) .

Eso significa que puede (adicionalmente) agregar un segundo Method devolviendo un bool para decirle a Control el "Evento" puede activarse o no.

Una buena cosa para eso es que Button s, por ejemplo, se Enabled="false" si el Method devolverá false

CommandParameters

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

En este ejemplo, quiero pasar el DataGrid.SelectedItem al Click_Command en mi ViewModel.

Su Método debería tener este aspecto mientras que la implementación de ICommand se mantiene como se indica arriba.

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
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow