Patrones

WPF aplicaciones con el patrón de diseño Model-View-ViewModel

Josh Smith

En este artículo se describen:

  • Patrones y WPF
  • Modelo de MVP
  • ¿Por qué es mejor para WPF MVVM
  • Creación de una aplicación con MVVM
En este artículo se utilizan las siguientes tecnologías:
WPF, enlace de datos

Descarga de código de la Galería de código de MSDN
Examinar el código en línea

Contenido

Pedido de vs. Caos
La evolución de Model-View-ViewModel
¿Por qué los programadores de WPF Love MVVM?
Las aplicaciones de demostración
La retransmisión de lógica de comandos
Jerarquía de clases de ViewModel
Clase ViewModelBase
Clase CommandViewModel
Clase MainWindowViewModel
Aplicar una vista a un ViewModel
Los datos de modelo y repositorio
Nuevo formulario de entrada de datos de cliente
Ver todos los clientes
Ajustar hacia arriba

El desarrollo de la interfaz de usuario de una aplicación de software profesionales no es fácil. Puede ser una mezcla de datos, interacción con el diseño, diseño visual, conectividad, subprocesamiento múltiple, seguridad, internacionalización, validación, las pruebas unitarias y un toque de voodoo murky. Tener en cuenta que una interfaz de usuario expone el sistema subyacente y debe satisfacer los requisitos de estilo imprevisibles de sus usuarios, puede ser el área más volátil de muchas aplicaciones.

Hay patrones de diseño conocidos que pueden ayudarle a tame este criatura resultar difíciles de administrar, pero correctamente separar y tratar la multitud de preocupaciones pueden ser difíciles. El los patrones son más complicadas, más probable que sea accesos directos utiliza más adelante que comprometer todos los esfuerzos anteriores para realizar acciones en la forma correcta.

No es siempre los patrones de diseño a errores. En ocasiones, utilizamos patrones de diseño complicada, que requieren escribir una gran cantidad de código porque la plataforma de interfaz de usuario en uso no admite propio bien a un modelo más sencillo. Qué es necesario es una plataforma que facilita la generación de interfaces de usuario con los patrones de diseño sencilla, time-tested aprobado el desarrollador. Afortunadamente, Windows Presentation Foundation (WPF) ofrece exactamente.

A medida que el mundo de software continúa para que adopten WPF a un ritmo creciente, la comunidad WPF ha ha desarrollando su propia ecosistema de patrones y prácticas. En este artículo, revisaré algunas de esas prácticas recomendadas para diseñar e implementar aplicaciones de cliente con WPF. Aprovechando algunas características principales de WPF en combinación con el modelo de diseño Model-View-ViewModel (MVVM), guiará a través de un programa de ejemplo que muestra lo sencillo puede ser crear una aplicación WPF "forma correcta".

Al final de este artículo, será cómo borrar las plantillas de datos, comandos, el enlace de datos, el sistema de recursos y la trama MVVM todos los encajan para crear un marco sencillo, comprobable, eficaz en qué WPF cualquier aplicación puede prosperan. El programa de demostración que acompaña a este artículo puede servir como una plantilla para una aplicación WPF real que utiliza MVVM como su arquitectura principal. Las pruebas unitarias en la solución de demostración mostrar lo fácil que es probar la funcionalidad de interfaz de usuario de una aplicación cuando existe esa funcionalidad en un conjunto de clases de ViewModel. Antes de comenzar en los detalles, revisemos ¿por qué se debe utilizar una trama como MVVM en primer lugar.

Orden frente a caos

Es necesario y counterproductive utilizar patrones de diseño de un programa sencillo de "Hello, World!". Cualquier desarrollador competente puede entender unas pocas líneas de código de un vistazo. Sin embargo, a medida que aumenta el número de características en un programa, el número de líneas de código y partes móviles aumenta en consecuencia. Finalmente, la complejidad de un sistema y los problemas recurrentes contiene, anima a los desarrolladores organizar su código de tal forma que es más fácil comprender, explique, extender y solucionar problemas. Se disminuye el caos cognitivas de un sistema complejo al aplicar nombres conocidos a determinadas entidades en el código fuente. Se determinar el nombre para aplicar a un fragmento de código por considerar su función en el sistema.

Los desarrolladores a menudo intencionadamente estructura su código según un modelo de diseño, en contraposición a permitir que los patrones de surgen organically. No hay nada malo en cualquier enfoque, pero en este artículo, examinan las ventajas de utilizar explícitamente MVVM como la arquitectura de una aplicación WPF. Los nombres de ciertas clases incluyen condiciones conocidas desde el modelo MVVM, tales como terminen con "ViewModel" si la clase es una abstracción de una vista. Este enfoque ayuda a evitar el caos cognitivas que se ha mencionado anteriormente. En su lugar, por suerte puede existir en un estado de caos controlado, que es el estado natural de asuntos de proyectos de desarrollo de software más profesionales.

La evolución de Model-View-ViewModel

Empezaron desde personas crear interfaces de usuario de software, ha habido patrones de diseño populares para facilitar. Por ejemplo, el patrón Model-View-Presenter (MVP) ha disfrutado popularidad en varias plataformas de programación de interfaz de usuario. MVP es una variación del diseño Model-View-Controller, que ha sido alrededor durante décadas. En caso de que nunca ha usado la trama de MVP antes, aquí es una explicación simplificada. Lo que ve en la pantalla es la vista, los datos que se muestra están el modelo y el moderador enlaza los dos juntos. La vista se basa en un moderador para rellenarla con los datos de modelo, reaccionar a datos proporcionados por el usuario, proporcionar validación (quizá mediante la delegación en el modelo) y otras tareas tales. Si desea obtener más información sobre la la vista de modelo, le sugiero que leer José de Paul BoodhooColumna de patrones de diseño de agosto de 2006 .

En 2004, Martin Fowler publicado un artículo acerca de un patrón denominado Modelo de presentación (CP). El modelo de administrador de proyecto es similar a MVP en que separa una vista de su comportamiento y el estado. La parte interesante de la trama de administrador de proyecto es que se crea una abstracción de una vista, denominado el modelo de presentación. Una vista, a continuación, se convierte en simplemente una representación de un modelo de presentación. En la explicación del Fowler, muestra que el modelo de presentación actualiza con frecuencia la vista, para que los dos permanecen sincronizados entre sí. Esa lógica de sincronización existe como código de las clases de modelo de presentación.

En 2005, unveiled Gossman de John, actualmente una de las WPF y Architects Silverlight en Microsoft, elPatrón de modelo de vista de ViewModel (MVVM) en su blog. MVVM es idéntico al modelo de presentación del Fowler, que ambos modelos característica una abstracción de una vista, que contiene el estado y el comportamiento de una vista. Fowler introdujo el modelo de presentación como un medio de creación de una abstracción de interfaz de usuario independiente de la plataforma de una vista, mientras que Gossman introdujo MVVM como una forma estándar para aprovechar las características principales de WPF para simplificar la creación de interfaces de usuario. En ese sentido, considere MVVM que una especialización de la trama más general de administrador de proyecto, tailor-made para las plataformas WPF y Silverlight.

En excelente artículo bloque de Glenn" Prism: modelos para crear aplicaciones compuestas con WPF"en el número de septiembre de 2008, explica la guía de aplicaciones Microsoft composición para WPF. El término que ViewModel nunca se utiliza. En su lugar, el término modelo de presentación se utiliza para describir la abstracción de una vista. A lo largo de este artículo, sin embargo, se consulte el modelo como MVVM y la abstracción de una vista como un ViewModel. Resulte esta terminología prevelant mucho más en las comunidades de WPF y Silverlight.

A diferencia del moderador en MVP, un ViewModel no tiene una referencia a una vista. La vista se enlaza a las propiedades de un ViewModel, que, a su vez, expone datos contenidos en objetos del modelo y el otro estado específico a la vista. Los enlaces entre vista y ViewModel son sencillas construir porque un objeto ViewModel está establecido como la DataContext de una vista. Si el cambio ViewModel valores de propiedad, los nuevos valores se propagan automáticamente a la vista a través de enlace de datos. Cuando el usuario hace clic en un botón en la vista, se ejecuta un comando en el ViewModel para realizar la acción solicitada. El ViewModel, nunca la vista, realiza todas las modificaciones realizadas en los datos del modelo.

Las clases de vista no tienen idea que las clases de modelos existen, mientras que la ViewModel y modelo son de la vista. De hecho, el modelo es completamente oblivious al hecho de que existen los ViewModel y vista. Esto es un diseño muy escasamente acoplado, que se paga dividendos de muchas maneras, tal como se pronto se observa.

¿Por qué los programadores de WPF Love MVVM

Una vez que un programador deja de estar cómodo con WPF y MVVM, puede resultar difícil diferenciar los dos. MVVM es la internacional lingua de desarrolladores WPF porque es idóneo en la plataforma WPF y se ha diseñado WPF para que sea fácil de crear aplicaciones utilizando el modelo MVVM (entre otras). De hecho, Microsoft utilizaba MVVM internamente para desarrollar aplicaciones de WPF, como Microsoft Expression Blend, mientras se encontraba la plataforma principal de WPF en construcción. Muchos aspectos de WPF, como el control de menos de aspecto modelo y datos, utilizan la separación segura de pantalla de estado y comportamiento promovidos por MVVM.

El aspecto más importante único de WPF que hace MVVM un modelo excelente para utilizar es la infraestructura de enlace de datos. Las propiedades de enlace de una vista para un ViewModel, obtenga flexible de acoplamiento entre los dos y quita completamente la necesidad de escribir código en un ViewModel que directamente se actualiza una vista. El sistema de enlace de datos también admite la validación de entradas, que proporciona una forma estándar de transmisión de errores de validación a una vista.

Dos otras características de WPF que facilitan este patrón tan útil son plantillas de datos y el sistema de recursos. Las plantillas de datos aplican vistas a objetos ViewModel mostrados en la interfaz de usuario. Puede declarar plantillas en XAML y deje que el sistema de recursos automáticamente localizar y aplicar esas plantillas para usted en tiempo de ejecución. Puede obtener más información sobre el enlace y plantillas de datos en mi artículo de 2008 julio " Datos y WPF: Personalizar la presentación de datos con el enlace de datos y WPF ."

Si no se para la compatibilidad para los comandos en WPF, la trama MVVM sería mucho menos eficaz. En este artículo, le mostraré cómo un ViewModel puede exponer los comandos para una vista, lo que permite la vista para consumir su funcionalidad. Si no está familiarizado con ordenar, se recomienda que lea completo artículo Brian Noyes " WPF avanzada: descripción enrutamiento eventos y comandos en WPF " desde el problema de septiembre de 2008.

Además las características WPF (y 2 de Silverlight) que hacen MVVM forma natural a estructurar una aplicación, el patrón es también popular porque las clases de ViewModel son fáciles de prueba de unidades. Cuando se lógica de interacción de una aplicación reside en un conjunto de clases de ViewModel, es posible escribir fácilmente código que se comprueba. En cierto sentido, vistas y las pruebas unitarias son sólo dos tipos diferentes de ViewModel consumidores. Tener un conjunto de pruebas para ViewModels una aplicación proporciona libre y rápida regresión las pruebas, que ayuda a reducir el costo de mantenimiento de una aplicación a lo largo del tiempo.

Así como promover la creación de pruebas de regresión automatizada, la testability de clases de ViewModel puede ayudar a diseñar correctamente las interfaces de usuario que son fáciles de máscara. Al diseñar una aplicación, con frecuencia se puede decidir si algo debe incluirse en la vista o el ViewModel por imaginándose que desea escribir una prueba unitaria para consumir la ViewModel. Si puede escribir las pruebas unitarias para el ViewModel sin crear los objetos de interfaz de usuario, puede máscara también completamente el ViewModel porque no tiene ninguna dependencia en elementos visuales específicos.

Por último, para los desarrolladores que trabajan con diseñadores visuales, mediante MVVM facilita mucho crear un flujo de trabajo diseñador o desarrollador suave. Dado que una vista es simplemente un consumidor arbitrario de un ViewModel, es fácil de sólo vista de una de copia desde CD fuera y colocar en una nueva vista para representar un ViewModel. Este paso simple permite para prototipos rápida y evaluación de interfaces de usuario realizados por los diseñadores.

El equipo de desarrollo puede centrarse en crear clases ViewModel robustas, y el equipo de diseño puede centrarse en realizar vistas y fáciles de usar. Conectar el resultado de ambos equipos puede implicar poco más de garantizar que los enlaces correctos existen en archivo XAML una vista.

Las aplicaciones de demostración

En este momento, han han revisado del MVVM historial y teoría de operación. También examinan por qué es tan popular entre los desarrolladores WPF. Es momento para resumir sus sleeves y ver el modelo de acción. La aplicación de demostración que acompaña a este artículo utiliza MVVM en una gran variedad de formas. Proporciona un origen fertile de ejemplos para ayudar a colocar los conceptos en un contexto significativo. Creó la aplicación de demostración en Visual Studio 2008 SP1, con Microsoft .NET Framework 3.5 Service Pack 1. Las pruebas unitarias ejecutan en el sistema de prueba de unidades de Visual Studio.

La aplicación puede contener cualquier número de áreas de "trabajo," cada uno de los cuales el usuario puede abrir haciendo clic en un vínculo de comando en el área de exploración de la izquierda. Todas las áreas de trabajo reside en un control TabControl en el área de contenido principal. El usuario puede cierre un área de trabajo haciendo clic en el botón Cerrar de elemento de ficha dicha área de trabajo. La aplicación tiene dos áreas de trabajo disponibles: "todos los clientes" y "cliente nuevo". Después de ejecutar la aplicación y abrir algunas áreas de trabajo, la interfaz de usuario parece Figura 1 .

fig01.gif

La figura 1 áreas de trabajo

Sólo una instancia del área de trabajo "todos los clientes" puede ser abierta a la vez, pero cualquier número de áreas de trabajo de "nuevo cliente" se puede abrir a la vez. Cuando el usuario decide crear un nuevo cliente, debe rellenar el formulario de entrada de datos en la figura 2 .

fig02.gif

La Figura 2 nuevo formulario de entrada de datos de cliente

Después de rellenar el formulario de entrada de datos con los valores válidos y haciendo clic en el botón Guardar, aparece el nuevo nombre del cliente en la ficha artículo y cliente que se agrega a la lista de todos los clientes. La aplicación no tiene compatibilidad para eliminar o modificar un cliente existente, pero esa funcionalidad y muchas otras características similares a ella, son fáciles de implementar mediante la creación en la parte superior de la arquitectura de aplicación existente. Ahora que tiene un conocimiento de alto nivel de lo que hace la aplicación de la demostración, vamos a investigar cómo se ha diseñado y implementado.

La retransmisión de lógica de comandos

Cada vista en la aplicación tiene un archivo codebehind vacío, excepto para el código repetitivo estándar que llama a InitializeComponent en el constructor de la clase. De hecho, puede quitar los archivos de código subyacente de las vistas del proyecto y la aplicación todavía debe compilarse y ejecutarse correctamente. A pesar de la falta de métodos de control de eventos en las vistas, cuando el usuario hace clic en los botones, la aplicación reacciona y atiende las solicitudes del usuario. Esto funciona porque de enlaces que se han establecido en la propiedad de comandos de controles de hipervínculo, el botón y MenuItem mostradas en la interfaz de usuario. Los enlaces de asegurarse de que cuando el usuario hace clic en los controles, ejecutan objetos de ICommand expuestos por la ViewModel. Puede considerar el objeto de comando como un adaptador que facilita consumir la funcionalidad de un ViewModel desde una vista declarada en XAML.

Cuando un ViewModel expone una propiedad de instancia del tipo I­Command, el objeto de comando utiliza normalmente ese objeto ViewModel para obtener su trabajo realizado. Trama de una posible implementación consiste en crear una clase privada anidada dentro de la clase ViewModel, para que el comando tiene acceso a private miembros de su contenedor ViewModel y no contamina el espacio de nombres. Esa clase anidada implementa la interfaz de ICommand, y una referencia al objeto ViewModel que contiene se insertan en su constructor. Sin embargo, crear una clase anidada que implementa ICommand para cada comando expuesto por un ViewModel puede bloat el tamaño de la clase ViewModel. Más código significa un potencial mayor para los errores.

En la aplicación de demostración, la clase RelayCommand resuelve este problema. RelayCommand permite insertar lógica el comando a través de delegados que se pasó a su constructor. Este enfoque permite para implementación de comando concisa y concisa en clases ViewModel. RelayCommand es una variación simplificada de la DelegateCommand se encuentra en el Biblioteca de aplicación compuesta de Microsoft . La clase Relay­Command se muestra en la figura 3 .

Figura 3 la clase RelayCommand

public class RelayCommand : ICommand
{
    #region Fields

    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;        

    #endregion // Fields

    #region Constructors

    public RelayCommand(Action<object> execute)
    : this(execute, null)
    {
    }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;           
    }
    #endregion // Constructors

    #region ICommand Members

    [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    #endregion // ICommand Members
}

El evento CanExecuteChanged, que forma parte de la implementación de interfaz ICommand, tiene algunas características interesantes. Delega la suscripción del evento al evento CommandManager.RequerySuggested. Esto garantiza que el WPF ordenar infraestructura pide todos que RelayCommand objetos si puede ejecutar siempre que se pide a los comandos integrados. El siguiente código de la clase CustomerViewModel, que examinará detallada más adelante, muestra cómo configurar un RelayCommand con expresiones lambda:

RelayCommand _saveCommand;
public ICommand SaveCommand
{
    get
    {
        if (_saveCommand == null)
        {
            _saveCommand = new RelayCommand(param => this.Save(),
                param => this.CanSave );
        }
        return _saveCommand;
    }
}

Jerarquía de clases de ViewModel

La mayoría de las clases ViewModel tienen las mismas características. A menudo necesitan implementar la interfaz INotifyPropertyChanged, normalmente necesitan tener un nombre descriptivo mostrar y, en el caso de las áreas de trabajo, necesitan la capacidad de cierre (es decir, quitarse de la interfaz de usuario). Este problema Naturalmente presta a la creación de una clase de base de ViewModel o dos, para que nuevas clases de ViewModel puedan heredar toda la funcionalidad común de una clase base. Las clases ViewModel forman la jerarquía de herencia en la figura 4 .

fig04.gif

La figura 4 la jerarquía de herencia

Tener una clase base para todos los ViewModels es un requisito. Si prefiere tener las características de sus clases al redactar muchas clases más pequeñas en conjunto, en lugar de utilizar la herencia, no es un problema. Al igual que cualquier otro patrón de diseño, MVVM es un conjunto de instrucciones, no las reglas.

Clase ViewModelBase

ViewModelBase es la clase raíz en la jerarquía, que es la razón por la implementa la interfaz INotifyPropertyChanged utilizada y tiene una propiedad DisplayName. La interfaz INotifyPropertyChanged contiene un evento denominado PropertyChanged. Siempre que una propiedad en un objeto ViewModel tiene un nuevo valor, puede provocar el evento PropertyChanged para notificar el sistema de enlace de WPF del nuevo valor. Al recibir dicha notificación, el sistema de enlace de consulta la propiedad y la propiedad enlazada en algún elemento de la interfaz de usuario recibe el nuevo valor.

Con el fin de WPF saber qué propiedad en el objeto ViewModel ha cambiado, la clase PropertyChangedEventArgs expone una propiedad de nombre de propiedad de tipo String. Debe ser cuidado de pasar el nombre correcto de la propiedad en ese argumento de evento; de lo contrario, WPF se terminan consultar la propiedad incorrecta para un nuevo valor.

Un aspecto interesante del ViewModelBase es que proporciona la posibilidad de comprobar que una propiedad con un nombre determinado existe realmente en el objeto de ViewModel. Esto es muy útil cuando la refactorización, porque cambiar nombre de una propiedad mediante la característica de refactorización en Visual Studio 2008 no actualizará cadenas en el código fuente que se producen contener el nombre de esa propiedad (ni debe). Provoca el evento PropertyChanged con un nombre de propiedad incorrecto en el caso de argumento puede producir errores sutiles que son difíciles de localizar, por lo que esta característica poco puede ser un gran timesaver. El código de ViewModelBase que agrega esta compatibilidad útil se muestra en la figura 5 .

La figura 5 comprobación de una propiedad

// In ViewModelBase.cs
public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged(string propertyName)
{
    this.VerifyPropertyName(propertyName);

    PropertyChangedEventHandler handler = this.PropertyChanged;
    if (handler != null)
    {
        var e = new PropertyChangedEventArgs(propertyName);
        handler(this, e);
    }
}

[Conditional("DEBUG")]
[DebuggerStepThrough]
public void VerifyPropertyName(string propertyName)
{
    // Verify that the property name matches a real,  
    // public, instance property on this object.
    if (TypeDescriptor.GetProperties(this)[propertyName] == null)
    {
        string msg = "Invalid property name: " + propertyName;

        if (this.ThrowOnInvalidPropertyName)
            throw new Exception(msg);
        else
            Debug.Fail(msg);
    }
}

Clase CommandViewModel

La más sencilla subclase ViewModelBase concreta es CommandViewModel. Expone una propiedad denominada comandos de tipo I­Command. MainWindowViewModel expone una colección de estos objetos a través de su propiedad comandos. El área de desplazamiento en la parte izquierda de la ventana principal muestra un vínculo para cada CommandViewModel expuesto por MainWindowView­Model, tales como "ver todos los clientes "y " Crear nuevo cliente". Cuando el usuario hace clic en un vínculo, ejecutar, por tanto, uno de estos comandos, un área de trabajo abre en el control TabControl de la ventana principal. La definición de clase Command­ViewModel se muestra aquí:

public class CommandViewModel : ViewModelBase
{
    public CommandViewModel(string displayName, ICommand command)
    {
        if (command == null)
            throw new ArgumentNullException("command");

        base.DisplayName = displayName;
        this.Command = command;
    }

    public ICommand Command { get; private set; }
}

En el archivo MainWindowResources.xaml existe un Data­Template cuya clave es "CommandsTemplate". MainWindow utiliza dicha plantilla para representar la colección de CommandViewModels se ha mencionado anteriormente. La plantilla simplemente representa cada objeto CommandViewModel como un vínculo en un ItemsControl. Propiedad de comando cada hipervínculo está enlazado a la propiedad comandos de un Command­ViewModel. Que XAML se muestra en la figura 6 .

Figura 6 presentar la lista de comandos

<!-- In MainWindowResources.xaml -->
<!--
This template explains how to render the list of commands on 
the left side in the main window (the 'Control Panel' area).
-->
<DataTemplate x:Key="CommandsTemplate">
  <ItemsControl ItemsSource="{Binding Path=Commands}">
    <ItemsControl.ItemTemplate>
      <DataTemplate>
        <TextBlock Margin="2,6">
          <Hyperlink Command="{Binding Path=Command}">
            <TextBlock Text="{Binding Path=DisplayName}" />
          </Hyperlink>
        </TextBlock>
      </DataTemplate>
    </ItemsControl.ItemTemplate>
  </ItemsControl>
</DataTemplate>

Clase MainWindowViewModel

Como visto anteriormente en el diagrama de clase, la clase WorkspaceViewModel deriva ViewModelBase y agrega la capacidad para cerrar. Con el cierre, quiero decir que algo quita el área de trabajo de la interfaz de usuario en tiempo de ejecución. Tres clases de derivan WorkspaceViewModel: MainWindowViewModel, AllCustomersViewModel y CustomerViewModel. Solicitud de MainWindowViewModel de cierre se controla mediante la clase de aplicación, que crea el MainWindow y su ViewModel, como se ve en la figura 7 .

La figura 7 crear el ViewModel

// In App.xaml.cs
protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    MainWindow window = new MainWindow();

    // Create the ViewModel to which 
    // the main window binds.
    string path = "Data/customers.xml";
    var viewModel = new MainWindowViewModel(path);

    // When the ViewModel asks to be closed, 
    // close the window.
    viewModel.RequestClose += delegate 
    { 
        window.Close(); 
    };

    // Allow all controls in the window to 
    // bind to the ViewModel by setting the 
    // DataContext, which propagates down 
    // the element tree.
    window.DataContext = viewModel;

    window.Show();
}

MainWindow contiene un elemento de menú se enlaza cuya propiedad Command a propiedad de CloseCommand de la MainWindowViewModel. Cuando el usuario hace clic en ese elemento de menú, el responde de clase de aplicación llamando al método Close de la ventana, así:

<!-- In MainWindow.xaml -->
<Menu>
  <MenuItem Header="_File">
    <MenuItem Header="_Exit" Command="{Binding Path=CloseCommand}" />
  </MenuItem>
  <MenuItem Header="_Edit" />
  <MenuItem Header="_Options" />
  <MenuItem Header="_Help" />
</Menu>

MainWindowViewModel contiene una colección observable de objetos de WorkspaceViewModel, denominada áreas de trabajo. La ventana principal contiene un control TabControl cuya propiedad ItemsSource está enlazado a esa colección. Cada elemento de ficha tiene un botón Cerrar cuya propiedad Command está enlazado a la CloseCommand de su instancia WorkspaceViewModel correspondiente. Una versión abreviada de la plantilla que configura cada elemento de ficha se muestra en el código que sigue. El código se encuentra en MainWindowResources.xaml, y la plantilla explica cómo representar un elemento de ficha con un botón Cerrar:

<DataTemplate x:Key="ClosableTabItemTemplate">
  <DockPanel Width="120">
    <Button
      Command="{Binding Path=CloseCommand}"
      Content="X"
      DockPanel.Dock="Right"
      Width="16" Height="16" 
      />
    <ContentPresenter Content="{Binding Path=DisplayName}" />
  </DockPanel>
</DataTemplate>

Cuando el usuario hace clic en el botón Cerrar en un elemento de ficha, que se ejecuta de Workspace­ViewModel CloseCommand, provocando el evento Request­Close. MainWindowViewModel controla el evento RequestClose de sus áreas de trabajo y quita el área de trabajo de la colección Workspaces si se solicita. Puesto que la Main­Window TabControl tiene su propiedad ItemsSource enlazado a la colección de WorkspaceViewModels observable, quitar un elemento de la colección hace que el área de trabajo correspondiente para quitarse el control TabControl. Esa lógica de Main­WindowViewModel se muestra en la figura 8 .

Figura 8 quitar área de trabajo de la interfaz de usuario

// In MainWindowViewModel.cs

ObservableCollection<WorkspaceViewModel> _workspaces;

public ObservableCollection<WorkspaceViewModel> Workspaces
{
    get
    {
        if (_workspaces == null)
        {
            _workspaces = new ObservableCollection<WorkspaceViewModel>();
            _workspaces.CollectionChanged += this.OnWorkspacesChanged;
        }
        return _workspaces;
    }
}

void OnWorkspacesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    if (e.NewItems != null && e.NewItems.Count != 0)
        foreach (WorkspaceViewModel workspace in e.NewItems)
            workspace.RequestClose += this.OnWorkspaceRequestClose;

    if (e.OldItems != null && e.OldItems.Count != 0)
        foreach (WorkspaceViewModel workspace in e.OldItems)
            workspace.RequestClose -= this.OnWorkspaceRequestClose;
}

void OnWorkspaceRequestClose(object sender, EventArgs e)
{
    this.Workspaces.Remove(sender as WorkspaceViewModel);
}

En el proyecto UnitTests, el archivo MainWindowViewModelTests.cs contiene un método de prueba que compruebe que esta funcionalidad está funcionando correctamente. La facilidad con que se pueden crear pruebas unitarias para las clases de ViewModel es un enorme punto de argumentos de venta de la trama MVVM, ya que permite para probar simple funcionalidad de la aplicación sin escribir código que toque la interfaz de usuario. En la figura 9 se muestra dicho método de prueba.

Figura 9 el método de prueba

// In MainWindowViewModelTests.cs
[TestMethod]
public void TestCloseAllCustomersWorkspace()
{
    // Create the MainWindowViewModel, but not the MainWindow.
    MainWindowViewModel target = 
        new MainWindowViewModel(Constants.CUSTOMER_DATA_FILE);

    Assert.AreEqual(0, target.Workspaces.Count, "Workspaces isn't empty.");

    // Find the command that opens the "All Customers" workspace.
    CommandViewModel commandVM = 
        target.Commands.First(cvm => cvm.DisplayName == "View all customers");

    // Open the "All Customers" workspace.
    commandVM.Command.Execute(null);
    Assert.AreEqual(1, target.Workspaces.Count, "Did not create viewmodel.");

    // Ensure the correct type of workspace was created.
    var allCustomersVM = target.Workspaces[0] as AllCustomersViewModel;
    Assert.IsNotNull(allCustomersVM, "Wrong viewmodel type created.");

    // Tell the "All Customers" workspace to close.
    allCustomersVM.CloseCommand.Execute(null);
    Assert.AreEqual(0, target.Workspaces.Count, "Did not close viewmodel.");
}

Aplicar una vista a un ViewModel

MainWindowViewModel indirectamente agrega y quita Workspace­ViewModel objetos a y desde Tab­Control la ventana principal. Por confiar en el enlace de datos, la propiedad el contenido de un TabItem recibe un objeto derivado de ViewModelBase que se va a mostrar. ViewModelBase no es un elemento de interfaz de usuario, para que no tenga ninguna compatibilidad inherente para representación propia. De forma predeterminada, en WPF un objeto no visual se representa al mostrar los resultados de una llamada a su método ToString en un TextBlock. Claramente es no lo que necesita, a menos que los usuarios tienen un deseo de grabación para ver el nombre de tipo de nuestras clases ViewModel!

Se puede saber fácilmente que WPF cómo representar un objeto ViewModel mediante escrito DataTemplates. Un DataTemplate escrito no tiene un valor de x: clave asignada a ella, pero tienen su propiedad de tipo de datos establece en una instancia de la clase de tipo. Si WPF intenta representar uno de los objetos ViewModel, comprobará para ver si el sistema de recursos tiene un DataTemplate escrito en el ámbito cuyo tipo de datos es igual a (o una clase base de) el tipo de su objeto ViewModel. Si encuentra uno, dicha plantilla utiliza para representar el objeto ViewModel al que hace referencia la ficha propiedad del elemento contenido.

El archivo MainWindowResources.xaml tiene un Resource­Dictionary. Ese diccionario se agrega a jerarquía de recursos de la ventana principal, lo que significa que los recursos que contiene son de ámbito de recurso de la ventana. Cuando contenido del elemento de ficha está establecida en un objeto ViewModel, un DataTemplate con tipo de este diccionario proporciona una vista (es decir, un control de usuario) para que, como se muestra en la figura 10 .

Figura 10 proporciona una vista

<!-- 
This resource dictionary is used by the MainWindow. 
-->
<ResourceDictionary
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:vm="clr-namespace:DemoApp.ViewModel"
  xmlns:vw="clr-namespace:DemoApp.View"
  >

  <!-- 
  This template applies an AllCustomersView to an instance 
  of the AllCustomersViewModel class shown in the main window.
  -->
  <DataTemplate DataType="{x:Type vm:AllCustomersViewModel}">
    <vw:AllCustomersView />
  </DataTemplate>

  <!-- 
  This template applies a CustomerView to an instance  
  of the CustomerViewModel class shown in the main window.
  -->
  <DataTemplate DataType="{x:Type vm:CustomerViewModel}">
    <vw:CustomerView />
  </DataTemplate>

 <!-- Other resources omitted for clarity... -->

</ResourceDictionary>

No es necesario escribir ningún código que determina que ver para mostrar de un objeto ViewModel. El sistema de recursos de WPF hace todo el trabajo pesado, liberando copia para centrarse en aspectos más importantes. En escenarios más complejos, es posible seleccionar mediante programación la vista, pero en la mayoría situaciones que no es necesario.

Los datos de modelo y repositorio

Hemos visto cómo objetos ViewModel se carga, muestra y cerrados por el shell de aplicación. Ahora que la estructura general está en su lugar, puede revisar detalles de implementación más específicos en el dominio de la aplicación. Antes de obtener en las áreas de dos trabajo la aplicación, "todos los clientes" y "cliente nuevo", en primer lugar Examinemos el modelo de datos y las clases de acceso a datos. El diseño de esas clases tiene casi nada que ver con el modelo MVVM, porque puede crear una clase ViewModel para adaptar prácticamente cualquier objeto de datos en algo descriptivo para WPF.

La clase de modelo único en el programa de demostración es cliente. Esa clase tiene un conjunto de propiedades que representan información sobre un cliente de una compañía, como su nombre, apellidos y dirección de correo electrónico. Proporciona mensajes de validación implementando la interfaz IDataErrorInfo estándar, que existía durante años antes de la calle de aciertos de WPF. La clase de cliente no tiene en ella que sugiere está en uso en una arquitectura MVVM o incluso en una aplicación WPF nada. La clase fácilmente podría procedan de una biblioteca empresarial heredado.

Datos deben proceder de y residen en algún lugar. En esta aplicación, una instancia de la clase CustomerRepository carga y almacena todos los objetos de cliente. Sucede cargar los datos del cliente desde un archivo XML, pero el tipo de datos externos de origen es irrelevante. Los datos pueden proceder de una base de datos, un servicio Web, una canalización con nombre, un archivo en disco, o incluso pigeons Transportista: simplemente no importa. Mientras haya un objeto .NET con algunos datos en él, independientemente de dónde procede, el patrón MVVM puede obtener los datos en la pantalla.

La clase CustomerRepository expone algunos métodos que permiten obtener todos los clientes objetos disponibles, agregue nuevos un cliente en el repositorio y comprobar si un cliente está ya en el repositorio. Puesto que la aplicación no permite al usuario eliminar un cliente, el repositorio no permite eliminar un cliente. El evento CustomerAdded se desencadena cuando un nuevo cliente introduce CustomerRepository, mediante el método AddCustomer.

Claramente, modelo de datos de la aplicación de esta es muy pequeño comparado con lo que requieren las aplicaciones empresariales reales, pero que no es importante. Lo que es importante comprender es cómo las clases ViewModel hacen que la utilice de cliente y CustomerRepository. Tenga en cuenta que Customer­ViewModel es un contenedor de un objeto de cliente. Expone el estado de un cliente y otro estado utilizado por el control Customer­View, a través de un conjunto de propiedades. CustomerViewModel no duplica el estado de un cliente; simplemente expone a través de delegación, así:

public string FirstName
{
    get { return _customer.FirstName; }
    set
    {
        if (value == _customer.FirstName)
            return;
        _customer.FirstName = value;
        base.OnPropertyChanged("FirstName");
    }
}

Cuando el usuario crea un nuevo cliente y hace clic en el botón Guardar en el control CustomerView, la Customer­ViewModel asociado que vista agregar el nuevo objeto de cliente a la Customer­Repository. Hace que CustomerAdded evento el repositorio se desencadena, que permite la AllCustomers­ViewModel sabe que debe agregar un nuevo Customer­ViewModel a su colección AllCustomers. En cierto sentido, Customer­Repository actúa como un mecanismo de sincronización entre distintos ViewModels que tratan con objetos de cliente. Quizá uno podría considerar esto como utilizando el modelo de diseño mediador. Se revisar más de cómo esto funciona en las próximas secciones, sino por ahora, consulte en el diagrama en la figura 11 para obtener una descripción de alto nivel de cómo todo las encajan piezas.

fig11.gif

Figura 11 relaciones de clientes

Nuevo formulario de entrada de datos de cliente

Cuando el usuario hace clic en el vínculo "Crear nuevo cliente", MainWindowViewModel agrega un nuevo CustomerViewModel a su lista de áreas de trabajo y un control CustomerView lo muestra. Después de que el usuario escribe los valores válidos en los campos de entrada, el botón Guardar entra en el estado habilitado para que el usuario puede persistir la nueva información de cliente. No hay nada fuera de la normal aquí, sólo un formulario de entrada de datos regulares con validación de entrada y un botón Guardar.

La clase de cliente tiene validación integrada compatible con, disponible a través de su implementación de interfaz IDataErrorInfo. Que validación garantiza el cliente tiene un nombre, una dirección de correo electrónico con formato correcto, y, si el cliente es una persona, un apellido. Si IsCompany propiedad el cliente devuelve true, la propiedad LastName no puede tener un valor (la idea se que una compañía no tiene un apellido). Esta lógica de validación puede efectuar sentido desde perspectiva el objeto Customer, pero no cumple las necesidades de la interfaz de usuario. La interfaz de usuario requiere que un usuario seleccione si un nuevo cliente es una persona o una compañía. El selector de tipo de cliente tiene inicialmente el valor "(no especificado)". ¿Cómo puede la interfaz de usuario saber al usuario que el tipo de cliente es no especificado si la propiedad IsCompany de un cliente sólo se permite para un valor VERDADERO o FALSO?

Suponiendo que tenga control total sobre el sistema de software completo, se puede cambiar la propiedad IsCompany de tipo Nullable <bool>, que se permite para el valor "". Sin embargo, el mundo real no siempre es tan sencillo. Supongamos que no puede cambiar la clase de cliente porque procede de una biblioteca heredado que posee un equipo diferente de la compañía. ¿Qué ocurre si es no fácil de conservar que "" valor debido del esquema de base de datos existente? ¿Qué ocurre si otras aplicaciones ya utilizan la clase de cliente y dependen de la propiedad es un valor de tipo Boolean normal? Una vez más, con un ViewModel viene al rescate.

El método de prueba en la figura 12 muestra el funcionamiento de esta funcionalidad en CustomerViewModel. CustomerViewModel expone una propiedad CustomerTypeOptions para que el selector de tipo de cliente tenga tres cadenas que se va a mostrar. También expone una propiedad de CustomerType, que almacena la cadena seleccionada en el selector de. Cuando se establece CustomerType, asigna el valor de tipo String en un valor booleano de propiedad de IsCompany del objeto de cliente subyacente. la figura 13 muestra las dos propiedades.

Figura 12 el método de prueba

// In CustomerViewModelTests.cs
[TestMethod]
public void TestCustomerType()
{
    Customer cust = Customer.CreateNewCustomer();
    CustomerRepository repos = new CustomerRepository(
        Constants.CUSTOMER_DATA_FILE);
    CustomerViewModel target = new CustomerViewModel(cust, repos);

    target.CustomerType = "Company"
    Assert.IsTrue(cust.IsCompany, "Should be a company");

    target.CustomerType = "Person";
    Assert.IsFalse(cust.IsCompany, "Should be a person");

    target.CustomerType = "(Not Specified)";
    string error = (target as IDataErrorInfo)["CustomerType"];
    Assert.IsFalse(String.IsNullOrEmpty(error), "Error message should 
        be returned");
}

Figura 13 CustomerType propiedades

// In CustomerViewModel.cs

public string[] CustomerTypeOptions
{
    get
    {
        if (_customerTypeOptions == null)
        {
            _customerTypeOptions = new string[]
            {
                "(Not Specified)",
                "Person",
                "Company"
            };
        }
        return _customerTypeOptions;
    }
}
public string CustomerType
{
    get { return _customerType; }
    set
    {
        if (value == _customerType || 
            String.IsNullOrEmpty(value))
            return;

        _customerType = value;

        if (_customerType == "Company")
        {
            _customer.IsCompany = true;
        }
        else if (_customerType == "Person")
        {
            _customer.IsCompany = false;
        }

        base.OnPropertyChanged("CustomerType");
        base.OnPropertyChanged("LastName");
    }
}

El control CustomerView contiene un ComboBox que está enlazado a dichas propiedades, como se muestra aquí:

<ComboBox 
  ItemsSource="{Binding CustomerTypeOptions}"
  SelectedItem="{Binding CustomerType, ValidatesOnDataErrors=True}"
  />

Cuando se cambia el elemento seleccionado en ese ComboBox, IDataErrorInfo interfaz el origen de datos se consulta para ver si el nuevo valor es válido. Que se produce porque el enlace de la propiedad SelectedItem tiene ValidatesOnDataErrors establecido en true. Dado que el origen de datos es un objeto Customer­ViewModel, el sistema de enlace solicita que Customer­ViewModel de un error de validación de la propiedad CustomerType. La mayor parte del tiempo, CustomerViewModel delega todas las solicitudes de los errores de validación el objeto de cliente que contiene. Sin embargo, debido a que al cliente no tiene ninguna noción de tener un estado no seleccionado para la propiedad IsCompany, debe controlar la clase CustomerViewModel validar el nuevo elemento seleccionado en el control ComboBox. Ese código se ve en la figura 14 .

Figura 14 validar un objeto CustomerViewModel

// In CustomerViewModel.cs
string IDataErrorInfo.this[string propertyName]
{
    get
    {
        string error = null;

        if (propertyName == "CustomerType")
        {
            // The IsCompany property of the Customer class 
            // is Boolean, so it has no concept of being in
            // an "unselected" state. The CustomerViewModel
            // class handles this mapping and validation.
            error = this.ValidateCustomerType();
        }
        else
        {
            error = (_customer as IDataErrorInfo)[propertyName];
        }

        // Dirty the commands registered with CommandManager,
        // such as our Save command, so that they are queried
        // to see if they can execute now.
        CommandManager.InvalidateRequerySuggested();

        return error;
    }
}

string ValidateCustomerType()
{
    if (this.CustomerType == "Company" ||
       this.CustomerType == "Person")
        return null;

    return "Customer type must be selected";
}

El aspecto clave de este código es que implementación del CustomerViewModel de IDataErrorInfo puede controlar las solicitudes de validación de propiedad específicos de ViewModel y delegar las otras solicitudes del objeto de cliente. Esto permite hacer uso de lógica de validación en las clases de modelo y tiene adicionales de validación para las propiedades que sólo sentido a las clases de ViewModel.

La capacidad para guardar un CustomerViewModel está disponible a una vista a través de la propiedad SaveCommand. Ese comando utiliza la clase de RelayCommand Examinada anteriormente para permitir CustomerViewModel decidir si pueden guardar propio y qué hacer cuando se le indica que guarde su estado. En esta aplicación, guardar un nuevo cliente simplemente significa agregarlo a un CustomerRepository. Decidir si el nuevo cliente está preparado para guardarse requiere consentimiento de dos partes. El objeto de cliente debe ser le pregunte si es válido o no y la Customer­ViewModel debe decidir si es válido. Esta decisión de dos partes es necesaria debido a las propiedades específicas de ViewModel y validación examinado previamente. Al guardar en la figura 15 se muestra la lógica de Customer­ViewModel.

Figura 15 la guardar lógica para CustomerViewModel

// In CustomerViewModel.cs
public ICommand SaveCommand
{
    get
    {
        if (_saveCommand == null)
        {
            _saveCommand = new RelayCommand(
                param => this.Save(),
                param => this.CanSave
                );
        }
        return _saveCommand;
    }
}

public void Save()
{
    if (!_customer.IsValid)
        throw new InvalidOperationException("...");

    if (this.IsNewCustomer)
        _customerRepository.AddCustomer(_customer);

    base.OnPropertyChanged("DisplayName");
}

bool IsNewCustomer
{
    get 
    { 
        return !_customerRepository.ContainsCustomer(_customer); 
    }
}

bool CanSave
{
    get 
    { 
        return 
            String.IsNullOrEmpty(this.ValidateCustomerType()) && 
            _customer.IsValid; 
    }
}

El uso de un ViewModel aquí facilita en gran medida crear una vista que puede mostrar un objeto de cliente y se permiten para cosas como un estado "no seleccionado" de una propiedad booleana. También permite saber fácilmente al guardar el estado de su cliente. Si la vista se han enlazado directamente a un objeto de cliente, la vista requeriría una gran cantidad de código para realizar este trabajo correctamente. En una arquitectura MVVM bien diseñada, el código subyacente para la mayoría de las vistas debe estar vacía, o, como máximo, sólo contienen código que manipula los controles y los recursos incluidos en esa vista. A veces, también es necesario para escribir código en código subyacente una vista que interactúa con un objeto ViewModel, como el enlace de un evento o llamar a un método que de lo contrario sería muy difícil invocar desde el ViewModel propio.

Ver todos los clientes

La aplicación de demostración también contiene un área de trabajo que muestra todas de los clientes en un ListView. Los clientes de la lista se agrupan según si son una empresa o una persona. El usuario puede seleccionar uno o más clientes al mismo tiempo y ver la suma de sus ventas totales en la esquina inferior derecha.

La interfaz de usuario es el control AllCustomersView, que representa un objeto AllCustomersViewModel. Cada ListView­Item representa un objeto CustomerViewModel en la colección AllCustomers expuesta por el objeto AllCustomerViewModel. En la sección anterior, hemos visto cómo un CustomerViewModel puede representar como un formulario de entrada de datos, y ahora está representando el objeto de CustomerViewModel de mismo exactamente como un elemento en un ListView. La clase de CustomerViewModel no tiene sabe qué elementos visuales mostrarlo, lo que es posible que esta reutilización.

AllCustomersView crea los grupos en el ListView. Esto se realiza enlazando el ListView ItemsSource a un Collection­ViewSource configurado como figura 16 .

Figura 16 CollectionViewSource

<!-- In AllCustomersView.xaml -->
<CollectionViewSource
  x:Key="CustomerGroups" 
  Source="{Binding Path=AllCustomers}"
  >
  <CollectionViewSource.GroupDescriptions>
    <PropertyGroupDescription PropertyName="IsCompany" />
  </CollectionViewSource.GroupDescriptions>
  <CollectionViewSource.SortDescriptions>
    <!-- 
    Sort descending by IsCompany so that the ' True' values appear first,
    which means that companies will always be listed before people.
    -->
    <scm:SortDescription PropertyName="IsCompany" Direction="Descending" />
    <scm:SortDescription PropertyName="DisplayName" Direction="Ascending" />
  </CollectionViewSource.SortDescriptions>
</CollectionViewSource>

La asociación entre un ListViewItem y un objeto CustomerViewModel está establecida por propiedad de ItemContainerStyle del ListView. El estilo asignado a que la propiedad se aplica a cada ListViewItem, que permite que las propiedades de un ListViewItem que deben enlazarse a las propiedades en el CustomerViewModel. Un enlace importante en ese estilo crea un vínculo entre la propiedad IsSelected de un ListViewItem y la propiedad IsSelected de Customer­ViewModel, tal como muestra aquí:

<Style x:Key="CustomerItemStyle" TargetType="{x:Type ListViewItem}">
  <!--   Stretch the content of each cell so that we can 
  right-align text in the Total Sales column.  -->
  <Setter Property="HorizontalContentAlignment" Value="Stretch" />
  <!-- 
  Bind the IsSelected property of a ListViewItem to the 
  IsSelected property of a CustomerViewModel object.
  -->
  <Setter Property="IsSelected" Value="{Binding Path=IsSelected, 
    Mode=TwoWay}" />
</Style>

Cuando un CustomerViewModel está activada o no seleccionado, que hace que la suma de ventas totales de los seleccionados todos los clientes para cambiar. La clase de AllCustomersViewModel es responsable del mantenimiento de ese valor, para que la ContentPresenter bajo el ListView pueda mostrar el número correcto. en la figura 17 muestra cómo en que AllCustomersViewModel supervisa cada cliente para que se está seleccionado o no seleccionado y notifica a la vista que debe actualizar el valor de visualización.

Figura 17 supervisión de seleccionado o no seleccionado

// In AllCustomersViewModel.cs
public double TotalSelectedSales
{
    get
    {
        return this.AllCustomers.Sum(
            custVM => custVM.IsSelected ? custVM.TotalSales : 0.0);
    }
}

void OnCustomerViewModelPropertyChanged(object sender, 
    PropertyChangedEventArgs e)
{
    string IsSelected = "IsSelected";

    // Make sure that the property name we're 
    // referencing is valid.  This is a debugging 
    // technique, and does not execute in a Release build.
    (sender as CustomerViewModel).VerifyPropertyName(IsSelected);

    // When a customer is selected or unselected, we must let the
    // world know that the TotalSelectedSales property has changed,
    // so that it will be queried again for a new value.
    if (e.PropertyName == IsSelected)
        this.OnPropertyChanged("TotalSelectedSales");
}

La interfaz de usuario se enlaza a la propiedad TotalSelectedSales y aplica (moneda) formato al valor de moneda. El objeto ViewModel pudo aplicar la moneda de formato, en lugar de la vista, devolviendo un valor de tipo String en lugar de un valor de tipo Double de la propiedad TotalSelectedSales. La propiedad ContentStringFormat de ContentPresenter se agregó en el SP1 de 3.5 de .NET Framework, por lo si debe destino una versión anterior de WPF, deberá aplicar el formato de código de moneda:

<!-- In AllCustomersView.xaml -->
<StackPanel Orientation="Horizontal">
  <TextBlock Text="Total selected sales: " />
  <ContentPresenter
    Content="{Binding Path=TotalSelectedSales}"
    ContentStringFormat="c"
  />
</StackPanel>

Ajustar hacia arriba

WPF tiene mucho para ofrecer a los programadores de aplicaciones, y aprendizaje para aprovechar la energía que requiere un turno de máxima. El modelo Model-View-ViewModel es un conjunto simple y eficaz de directrices para diseñar e implementar una aplicación WPF. Permite crear una separación segura entre datos, comportamiento y presentación, que facilita el caos que es el desarrollo de software de control.

Me gustaría agradecer John Gossman su ayuda con este artículo.

Josh Smith es besos acerca del uso WPF para crear experiencias de usuario excelente. Se obtienen el título del MVP de Microsoft para su trabajo de la comunidad WPF. Josh funciona para Infragistics en el grupo Diseño de experiencia. Cuando que no esté en un equipo, disfruta reproducir la piano, leer sobre el historial y explorar ciudad de Nueva York con su girlfriend. Puede visitar del Josh blog en joshsmithonwpf.wordpress.com .