Compartilhar via


Este artigo foi traduzido por máquina.

Padrões

Aplicativos com O padrão de design Model-View-ViewModel WPF

Josh Smith

Este artigo discute:

  • Padrões e WPF
  • Padrão MVP
  • Por que MVVM é melhor para WPF
  • Criando um aplicativo com MVVM
Este artigo usa as seguintes tecnologias:
O WPF, ligação de dados

Download do código disponível na Galeria de código do MSDN
Procure o código on-line

Conteúdo

Ordem vs. Caos
A evolução do Model-View-ViewModel
Por que os desenvolvedores do WPF adoro MVVM
O aplicativo de demonstração
Retransmissão lógica de comando
Hierarquia de classe ViewModel
Classe ViewModelBase
Classe CommandViewModel
Classe MainWindowViewModel
Aplicar um modo de exibição a um ViewModel
Os dados do modelo e o repositório
Novo formulário de entrada de dados de cliente
Exibir todos os clientes
Quebra automática para cima

não é fácil de desenvolver a interface do usuário de um aplicativo de software profissionais. Ele pode ser uma mistura murky de dados, design de interação, design visual, conectividade, multithreading, segurança, internacionalização, validação, testes de unidade e um toque de voodoo. Considerar que uma interface de usuário expõe o sistema subjacente e deve atender aos stylistic requisitos de seus usuários a imprevisíveis, ele pode ser área de muitos aplicativos mais volátil.

Há padrões de design comuns que podem ajudar a controle este besta complicada, mas corretamente separando e a variedade de problemas de endereçamento podem ser difícil. O mais complicada os padrões são, mais provável que atalhos serão usado mais tarde que anular todos os esforços anteriores para fazer coisas a maneira correta.

Não é sempre os padrões de design com defeito. Às vezes, usamos padrões de design complicado, que precisam escrever muito código porque a plataforma de interface do usuário em uso emprestar propriamente dito não bem a um padrão mais simples. O que é necessário é uma plataforma que torna fácil criar interfaces do usuário usando padrões de design simples, time-tested e desenvolvedores aprovados. Felizmente, o Windows Presentation Foundation (WPF) fornece exatamente que.

Como o mundo de software continua a adotar o WPF a uma taxa crescente, a comunidade WPF tem sido desenvolvendo seu próprio ecossistema de padrões e práticas recomendadas. Neste artigo, analisarei algumas das práticas recomendadas para criar e implementar aplicativos de cliente com o WPF. Aproveitando a alguns recursos principais do WPF em conjunto com o padrão de design Model-View-ViewModel (MVVM), orientará através de um programa de exemplo que demonstra como simples é criar um aplicativo WPF "maneira certa".

No final deste artigo, será limpar como modelos de dados, comandos, ligação de dados, o sistema de recurso e o padrão MVVM todos os se combinam para criar uma estrutura simples, teste e robusta no qual qualquer WPF aplicativo pode prosperar. O programa de demonstração que acompanha este artigo pode servir como um modelo para um aplicativo de WPF real que usa MVVM como sua arquitetura principal. Os testes de unidade na solução demonstração mostram como é fácil testar a funcionalidade da interface de usuário do aplicativo quando essa funcionalidade existe em um conjunto de classes ViewModel. Antes de mergulhar em detalhes, vamos rever por que você deve usar um padrão como MVVM em primeiro lugar.

Ordem vs. Chaos

É desnecessário e contraproducentes usar padrões de design em um programa simples "Hello, World!". Qualquer desenvolvedor competente pode compreender algumas linhas de código em um piscar de olhos. No entanto, à medida que aumenta o número de recursos do programa, o número de linhas de código e partes móveis aumentar adequadamente. Finalmente, a complexidade de um sistema e os problemas recorrentes nela, encoraja os desenvolvedores a organizar seu código de tal forma que é mais fácil para compreender, discuta, estender e solucionar problemas. Nós diminui o caos percepção de um sistema complexo, aplicando nomes conhecidos a determinados entidades no código-fonte. Determine o nome para aplicar a uma parte do código, considerando sua função funcional no sistema.

Os desenvolvedores geralmente intencionalmente estruturar seu código acordo com um padrão de design, em oposição a permitir que os padrões surgem organicamente. Não há nada de errado com o método, mas neste artigo, eu examinar as vantagens de usar explicitamente MVVM como a arquitetura de um aplicativo WPF. Os nomes de determinadas classes incluir termos conhecidos do padrão MVVM, tais como terminando com "ViewModel" se a classe for uma abstração de um modo de exibição. Essa abordagem ajuda a evitar o caos cognitivas mencionado anteriormente. Em vez disso, Felizmente você pode existir em um estado de caos controlado, que é o estado natural de assuntos em projetos de desenvolvimento de software mais profissionais!

A evolução do Model-View-ViewModel

Desde que as pessoas iniciado para criar interfaces de usuário de software, não há padrões de design populares para ajudar a torná-lo mais fácil. Por exemplo, o padrão Model-View-Presenter (MVP) tenha aproveitado popularidade em várias plataformas de programação da interface do usuário. MVP é uma variação do padrão Model-View-Controller, que foi ao redor há décadas. No caso de você nunca tiver usado o padrão MVP antes, eis uma explicação simplificada. O que você vê na tela é o modo de exibição, os dados que ele exibe são o modelo e o apresentador conecta os dois juntos. O modo de exibição se baseia em um apresentador para preenchê-la com dados de modelo, reagir a entrada do usuário, fornecer validação de entrada (por exemplo, delegando para o modelo) e outras tarefas. Se você deseja saber mais sobre o Model View Presenter, sugiro que você ler do Jean-Paul BoodhooColuna de padrões de design de agosto de 2006.

Volta em 2004, Martin Fowler publicado um artigo sobre um padrão nomeadoModelo de apresentação(PM). O padrão do gerente é semelhante ao MVP que ele separa um modo de exibição de seu comportamento e estado. A parte interessante do padrão PM é que uma abstração de um modo de exibição é criada, conhecido como o modelo de apresentação. Um modo de exibição, passará simplesmente um processamento de um modelo de apresentação. Na explicação do Fowler, ele mostra que o modelo de apresentação com freqüência atualiza seu modo de exibição, para que os dois permanecem em sincronia com uns aos outros. Essa lógica de sincronização existe como código nas classes de modelo de apresentação.

Em 2005, John Gossman, atualmente uma das WPF e arquitetos de Silverlight na Microsoft, divulgou oModelo-modo de exibição-ViewModel (MVVM) padrãoem seu blog. MVVM é idêntico ao modelo de apresentação do Fowler, uma abstração de um modo de exibição, que contém o estado e o comportamento de um modo de exibição de recurso de ambos os padrões. Fowler introduziu o modelo de apresentação como um meio de criar uma abstração de independente de plataforma de interface do usuário de um modo de exibição, enquanto Gossman introduzidos MVVM como uma maneira padronizada para aproveitar os principais recursos do WPF para simplificar a criação de interfaces do usuário. Sentido, considero MVVM a ser uma especialização do mais geral PM padrão, feito sob medida para as plataformas WPF e o Silverlight.

No excelente artigo bloco de Glenn"Prisma: padrões para criação de aplicativos compostos com WPF"o problema de setembro de 2008, ele explica as orientações de aplicativos compostos Microsoft para WPF. O termo que ViewModel nunca é usado. Em vez disso, o termo Modelo de apresentação é usado para descrever a abstração de um modo de exibição. No decorrer deste artigo, no entanto, vai consulte o padrão como MVVM e a abstração de um modo de exibição como um ViewModel. Acho que essa terminologia é muito mais prevelant em comunidades WPF e o Silverlight.

Ao contrário do orador em MVP, um ViewModel não é necessário uma referência a um modo de exibição. O modo de exibição liga um ViewModel, que, por sua vez, expõe dados contidos em objetos de modelo e outro estado específicos para o modo de exibição. As ligações entre modo de exibição e ViewModel são simples para construir porque um objeto ViewModel está definido como o DataContext de um modo de exibição. Se a propriedade valores em alteração ViewModel, os novos valores automaticamente se propagarão para o modo de exibição via ligação de dados. Quando o usuário clica em um botão no modo de exibição, um comando da ViewModel executa para executar a ação solicitada. O ViewModel, nunca o modo de exibição, executa todas as modificações feitas nos dados de modelo.

As classes de modo de exibição não terão nenhuma idéia de que as classes do modelo existem, enquanto o ViewModel e o modelo estiver sabem do modo de exibição. Na verdade, o modelo é completamente oblivious ao fato da ViewModel e o modo de exibição existem. Isso é um design muito rígido, que paga dividendos de várias maneiras, como você verá em breve.

Por que os desenvolvedores do WPF adoro MVVM

Depois que um desenvolvedor ficar familiarizado com WPF e MVVM, ele pode ser difícil diferenciar os dois. MVVM é o franca língua dos desenvolvedores do WPF, pois ele é bastante adequado para a plataforma do WPF e WPF foi projetado para facilitar a criação de aplicativos usando o padrão MVVM (entre outros). Na verdade, Microsoft estava usando MVVM internamente para desenvolver aplicativos do WPF, como o Microsoft Expression Blend, enquanto a plataforma WPF principais estava em construção. Muitos aspectos do WPF, como os modelos look-menos controle modelo e os dados, utilizam a separação de alta segurança de exibição de estado e comportamento promovidos por MVVM.

O único aspecto mais importante do WPF que torna MVVM um ótimo padrão para usar é a infra-estrutura de ligação de dados. Propriedades de vinculação de um modo de exibição para um ViewModel, você obter flexível união entre os dois e remover totalmente a necessidade de escrever código em um ViewModel que atualiza diretamente um modo de exibição. O sistema de ligação de dados também oferece suporte a validação de entrada, que fornece uma maneira padronizada de transmitir erros de validação a um modo de exibição.

Dois outros recursos do WPF que tornam esse padrão tão útil são modelos de dados e o sistema de recurso. Modelos de dados se aplicam a modos de exibição a objetos de ViewModel mostrados na interface do usuário. Você pode declarar modelos no XAML e deixe o sistema de recurso localizar e aplicar esses modelos para você em tempo de execução automaticamente. Você poderá aprender mais sobre vinculação e modelos de dados em meu artigo julho de 2008 "Dados e o WPF: Personalizar a exibição de dados com ligação de dados e WPF."

Se não fosse para o suporte para comandos no WPF, o padrão MVVM seria muito menos eficiente. Neste artigo, mostrarei como um ViewModel pode expor comandos para um modo de exibição, permitindo a exibição para consumir sua funcionalidade. Se você não estiver familiarizado com comandando, eu recomendo que você leia abrangente artigo Brian Noyes "WPF avançada: Noções básicas sobre roteada eventos e comandos no WPF" de 2008 de setembro.

Adicionalmente aos recursos WPF (e 2 do Silverlight) que fazem MVVM uma maneira natural de estrutura de um aplicativo, o padrão também é popular porque ViewModel classes são fáceis de teste de unidade. Quando a lógica de interação de um aplicativo reside em um conjunto de classes ViewModel, você pode escrever facilmente código que testa-lo. De certa forma, modos de exibição e testes de unidade são apenas dois tipos diferentes de ViewModel consumidores. Ter um conjunto de testes para ViewModels um aplicativo fornece gratuito e rápido regressão de teste, que ajuda a reduzir o custo de manutenção de um aplicativo com o tempo.

Bem como promover a criação de testes de regressão automatizada, o testability das classes ViewModel pode ajudar corretamente criar interfaces do usuário que são fáceis de capa. Quando você está criando um aplicativo, você geralmente pode decidir se algo deve estar em modo de exibição ou ViewModel pelo espelhamento que você deseja escrever um teste de unidade para consumir o ViewModel. Se você pode escrever testes de unidade para o ViewModel sem criar os objetos de interface do usuário, você pode também completamente capa a ViewModel porque não possui nenhuma dependência nos elementos visuais específicos.

Por fim, para os desenvolvedores que trabalham com os designers visuais, uso MVVM facilita muito criar um fluxo de trabalho suave de desenvolvedor/Designer. Como um modo de exibição é apenas um consumidor arbitrário um ViewModel, é fácil para apenas copiar um modo check-out e soltar em um novo modo de exibição para processar um ViewModel. Esta etapa simples permite para rápida criação de protótipos e avaliação das interfaces de usuário feitas pelos designers.

A equipe de desenvolvimento pode enfocar criar classes de ViewModel robustas; a equipe de design pode enfocar fazer modos de exibição amigáveis. Conectando-se a saída de ambas as equipes pode envolver pouco mais de garantir que as ligações corretas existem no arquivo XAML de um modo de exibição.

O aplicativo de demonstração

Neste ponto, eu revisaram do MVVM histórico e teoria de operação. Eu também examinadas por que ele é tão popular entre os desenvolvedores do WPF. Agora é hora para seu mangas e ver o padrão em ação. O aplicativo de demonstração que acompanha este artigo usa MVVM em uma variedade de formas. Ele fornece uma fonte fertile de exemplos para ajudar a colocar os conceitos em um contexto significativo. Criei o aplicativo de demonstração no Visual Studio 2008 SP1, com o Microsoft .NET Framework 3.5 SP1. Os testes de unidade executados no sistema de teste de unidade do Visual Studio.

O aplicativo pode conter qualquer número de espaços de "trabalho" cada uma das quais o usuário pode abrir clicando em um link de comando na área de navegação à esquerda. Todos os espaços de trabalho ao vivo em um TabControl na área de conteúdo principal. O usuário pode fechar um espaço de trabalho clicando no botão Fechar no item do guia do espaço de trabalho. O aplicativo tem dois espaços de trabalho disponíveis: "todos os clientes" e "New Customer." Depois de executar o aplicativo e abrir alguns espaços de trabalho, a interface do usuário é semelhante a Figura 1 .

fig01.gif

A Figura 1 espaços de trabalho

Apenas uma instância do espaço de trabalho "todos os clientes" pode ser aberta por vez, mas qualquer número de espaços de trabalho "novo cliente" pode ser aberto ao mesmo tempo. Quando o usuário decide criar um novo cliente, ele deve preencher o formulário entrada de dados na Figura 2 .

fig02.gif

A Figura 2 novo formulário de entrada de dados de cliente

Depois de preencher o formulário de entrada de dados com os valores válidos e clique no botão de salvar, o novo nome do cliente aparece na guia item e esse cliente é adicionado à lista de todos os clientes. O aplicativo não tem suporte para excluir ou editar um cliente existente, mas essa funcionalidade e muitos outros recursos similares a ele, são fácil de implementar, criando na parte superior da arquitetura do aplicativo existente. Agora que você tem uma compreensão geral do que o aplicativo de demonstração faz, vamos investigar como ela foi projetada e implementada.

Retransmissão lógica de comando

Cada exibição no aplicativo possui um arquivo code-behind vazia, exceto para o código clichê padrão que chama InitializeComponent no construtor da classe. Na verdade, você pode remover arquivos de code-behind as exibições do projeto e o aplicativo ainda pode compilar e executar corretamente. Apesar da falta de métodos de manipulação de eventos nos modos de exibição, quando o usuário clica nos botões, o aplicativo reage e atende solicitações do usuário. Isso funciona por causa das ligações que foram estabelecidas na propriedade comando de controles de hiperlink, Button e MenuItem exibidos na interface do usuário. Essas ligações Certifique-se que quando o usuário clica nos controles, objetos ICommand expostos pelo ViewModel executar. Você pode considerar o objeto de comando como um adaptador que facilita a consumir a funcionalidade de um ViewModel de um modo de exibição declarado em XAML.

Quando um ViewModel expõe uma propriedade de instância do tipo I­Command, o objeto de comando normalmente usa esse objeto de ViewModel para fazer seu trabalho. Uma possível implementação padrão é criar uma classe privada aninhada dentro da classe ViewModel, para que o comando tem acesso a membros privados de seu ViewModel contendo e não poluir o espaço para nome. Essa classe aninhada implementa a interface de ICommand, e uma referência ao objeto recipiente ViewModel é injetada em seu construtor. No entanto, criando uma classe aninhada que implementa ICommand para cada comando exposto por um ViewModel pode aumentarão o tamanho da classe ViewModel. Código mais significa um maior potencial para erros.

No aplicativo de demonstração, a classe RelayCommand resolve esse problema. RelayCommand permite que você insira lógica do comando por meio de representantes passado para o construtor. Essa abordagem permite para implementação de comando concisa, concisa em classes ViewModel. RelayCommand é uma variação simplificada do DelegateCommand encontrado noBiblioteca de aplicativos Microsoft composto. A classe Relay­Command é mostrada na Figura 3 .

Figura 3 A classe 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
}

O evento CanExecuteChanged, que faz parte da implementação de interface ICommand, tem alguns recursos interessantes. Ele delega a inscrição do evento para o evento CommandManager.RequerySuggested. Isso garante que o WPF comandando infra-estrutura faz todos os que RelayCommand objetos se pode executar sempre que ele solicita os comandos internos. O código a seguir da classe CustomerViewModel, que eu examinará detalhadas posteriormente, mostra como configurar um RelayCommand com expressões lambda:

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

Hierarquia de classe ViewModel

A maioria das classes de ViewModel precisa os mesmos recursos. Freqüentemente precisam implementar a interface INotifyPropertyChanged, geralmente precisam ter um nome de exibição amigável e, no caso de espaços de trabalho precisam a capacidade de fechar (ou seja, ser removido da interface do usuário). Esse problema naturalmente presta para as criações de uma classe de base ViewModel ou dois, para que novas classes ViewModel podem herde toda a funcionalidade comum de uma classe base. As classes ViewModel formam a hierarquia de herança mostrada na Figura 4 .

fig04.gif

A Figura 4 hierarquia de herança

Ter uma classe base para todos os seus ViewModels não é um requisito. Se você preferir obter recursos suas classes, compondo muitas classes menores juntos, em vez de usar a herança, que não é um problema. Assim como qualquer outro padrão de design, MVVM é um conjunto de diretrizes, não regras.

Classe ViewModelBase

ViewModelBase é a classe de raiz na hierarquia, por isso, ele implementa a interface utilizada do INotifyPropertyChanged e tem uma propriedade de nome de exibição. A interface INotifyPropertyChanged contém um evento chamado PropertyChanged. Sempre que uma propriedade em um objeto ViewModel tiver um novo valor, ele pode dispara o evento PropertyChanged para notificar o sistema de ligação do WPF do novo valor. Ao receber essa notificação, o sistema de ligação de consulta a propriedade e a propriedade limite em algum elemento da interface do usuário recebe o novo valor.

Em ordem para WPF saber qual propriedade no objeto ViewModel é alterada, a classe PropertyChangedEventArgs expõe uma propriedade de PropertyName do tipo String. Você deve ser cuidado para passar o nome da propriedade correto para esse argumento de evento; caso contrário, WPF acabará consultando a propriedade errada para um novo valor.

Um aspecto interessante de ViewModelBase é que ele fornece a capacidade de verificar se uma propriedade com um determinado nome, na verdade, existe no objeto ViewModel. Isso é muito útil quando refatoração, porque alterar nome de uma propriedade via o recurso de refatoração do Visual Studio 2008 não atualizará as seqüências de caracteres em seu código-fonte que ocorrem conter o nome da propriedade (nem deve ele). Disparar o evento de PropertyChanged com um nome de propriedade incorreto no argumento pode levar a bugs sutis que são difíceis de rastrear, portanto, esse recurso pouco pode ser um enorme timesaver de eventos. O código de ViewModelBase que adiciona esse suporte útil é mostrado na Figura 5 .

A Figura 5 Verificando uma propriedade

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

Classe CommandViewModel

A subclasse ViewModelBase concreta mais simples é CommandViewModel. Ela expõe uma propriedade chamada comando do tipo I­Command. MainWindowViewModel expõe uma coleção desses objetos através de sua propriedade de comandos. A área de navegação no lado esquerdo da janela principal exibirá um link para cada CommandViewModel exposto pelo MainWindowView­Model, como "Exibir todos os clientes "e " Criar novo cliente". Quando o usuário clica em um link, assim, executar um desses comandos, é um espaço de trabalho aberta no TabControl na janela principal. A definição de classe Command­ViewModel é mostrada aqui:

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

No arquivo MainWindowResources.xaml existe um Data­Template cuja chave é "CommandsTemplate". MainWindow usa esse modelo para processar a coleção de CommandViewModels mencionado anteriormente. O modelo simplesmente processa cada objeto CommandViewModel como um link em um ItemsControl. Propriedade de comando do cada hiperlink é acoplado à propriedade comando de um Command­ViewModel. Esse XAML é mostrado na Figura 6 .

A Figura 6 Render a 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>

Classe MainWindowViewModel

Como visto anteriormente no diagrama de classe, a classe WorkspaceViewModel deriva de ViewModelBase e acrescenta a capacidade de fechar. Por fechar, QUERO dizer que algo remove o espaço de trabalho a interface do usuário em tempo de execução. Três classes derivam WorkspaceViewModel: MainWindowViewModel, AllCustomersViewModel e CustomerViewModel. Solicitação do MainWindowViewModel para fechar é tratada pela classe de aplicativo, que cria o MainWindow e seu ViewModel, conforme mostrado na Figura 7 .

A Figura 7 criar o 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 contém um item de menu cuja propriedade do comando está ligada à CloseCommand propriedade do MainWindowViewModel. Quando o usuário clica no item menu, o responde de classe do aplicativo chamando o método Close da janela, desta forma:

<!-- 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 contém um conjunto observáveis de objetos de WorkspaceViewModel, chamados de espaços de trabalho. A janela principal contém um TabControl cuja propriedade ItemsSource é acoplado a essa coleção. Cada item da guia tem um botão Fechar cuja propriedade do comando está vinculada ao CloseCommand da sua instância WorkspaceViewModel correspondente. Uma versão resumida do modelo que define cada item da guia é mostrada no código que segue. O código é encontrado no MainWindowResources.xaml e o modelo explica como processar um item da guia com um botão Fechar:

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

Quando o usuário clica no botão Fechar em um item de guia, que CloseCommand do Workspace­ViewModel executa, fazendo com que seu evento Request­Close seja acionado. MainWindowViewModel monitora o evento RequestClose dos seus espaços de trabalho e remove o espaço de trabalho da coleção Workspaces mediante solicitação. Como do Main­Window TabControl tem sua propriedade ItemsSource vinculado a coleção observáveis de WorkspaceViewModels, remover um item da coleção faz com que o espaço de trabalho correspondente ser removido do TabControl. Essa lógica de Main­WindowViewModel é mostrada na Figura 8 .

A Figura 8 removendo espaço de trabalho de interface do usuário

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

No projeto UnitTests, o arquivo MainWindowViewModelTests.cs contém um método de teste que verifica se essa funcionalidade está funcionando corretamente. A facilidade com que você pode criar testes de unidade para classes ViewModel é um grande ponto de venda do padrão MVVM, porque ela permite que para simples de teste de funcionalidade do aplicativo sem escrever código que toca a interface do usuário. Esse método de teste é mostrado na Figura 9 .

Figura 9 O método de teste

// 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 um modo de exibição a um ViewModel

MainWindowViewModel indiretamente adiciona e remove objetos de Workspace­ViewModel para e a partir Tab­Control da janela principal. Por contar com ligação de dados, a propriedade content de um TabItem recebe um objeto derivado ViewModelBase para exibição. ViewModelBase não é um elemento de interface do usuário, portanto, ele tem suporte inerente para processamento propriamente dito. Por padrão, no WPF um objeto não-visuais é processado exibindo os resultados de uma chamada para seu método ToString em um TextBlock. Que claramente é não o que você precisa, a menos que os usuários tiverem um desejo de gravação para ver o nome de tipo de nossas classes ViewModel!

Você pode saber facilmente WPF como processar um objeto ViewModel usando digitado DataTemplates. Um DataTemplate digitado não tem um valor de x: Key atribuído a ele, mas ele tem a propriedade de tipo de dados definida como uma instância da classe tipo. Se o WPF tentar processar um dos seus objetos ViewModel, ele verificará a Consulte se o sistema de recurso tiver um DataTemplate digitado no escopo cujo tipo de dados é o mesmo (ou uma classe base de) o tipo de seu objeto ViewModel. Se ele encontrar um, ele usa esse modelo para processar o objeto de ViewModel referenciado pela propriedade conteúdo do item de guia.

O arquivo MainWindowResources.xaml tem um Resource­Dictionary. Esse dicionário é adicionado à hierarquia de recursos da janela principal, que significa que os recursos que ele contém estão no escopo do recurso da janela. Quando conteúdo de um item guia for definido como um objeto ViewModel, um DataTemplate digitado desse dicionário fornece um modo de exibição (ou seja, um controle de usuário) para processá-lo, conforme mostrado na Figura 10 .

A Figura 10 fornecendo um modo de exibição

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

Você não precisará escrever qualquer código que determina qual exibir para mostrar um objeto ViewModel. O sistema de recurso do WPF faz todo o trabalho pesado para você, liberando você para enfatizar pontos mais importantes. No cenários mais complexos, é possível programaticamente selecionar o modo de exibição, mas na maioria das situações é desnecessário.

Os dados do modelo e o repositório

Você viu como objetos ViewModel são carregados, exibidos e fechados pelo shell do aplicativo. Agora que o direcionamento geral está no lugar, você poderá examinar detalhes de implementação mais específicos para o domínio do aplicativo. Antes de obter nos espaços de duas trabalho do aplicativo "todos os clientes" e "novo cliente," Vamos primeiro examine o modelo de dados e classes de acesso de dados. O design dessas classes tem quase nada para fazer com o padrão MVVM, porque você pode criar uma classe ViewModel para adaptar praticamente qualquer objeto de dados em algo amigável para o WPF.

A classe de modelo único no programa de demonstração é cliente. Essa classe tem algumas poucas propriedades que representam informações sobre um cliente de uma empresa, como seu nome, sobrenome e endereço de email. Ele fornece mensagens de validação Implementando a interface IDataErrorInfo padrão, que existia para anos antes da rua de visitas do WPF. A classe de cliente possui nada que sugere que ele está sendo usado em uma arquitetura MVVM ou mesmo em um aplicativo WPF. A classe pode facilmente ter vêm de uma biblioteca de negócios herdados.

Dados devem ser provenientes e residem em algum lugar. Neste aplicativo, uma instância da classe CustomerRepository carrega e armazena todos os objetos de cliente. Isso acontece carregar os dados do cliente de um arquivo XML, mas o tipo de fonte de dados externa é irrelevante. Os dados podem vir de um banco de dados, um serviço da Web, um pipe nomeado, um arquivo no disco, ou até mesmo pigeons de portadora: simplesmente não importa. Contanto que você tenha um objeto do .NET com alguns dados, independentemente de onde ele veio, o padrão MVVM pode obter esse dados na tela.

A classe CustomerRepository expõe alguns métodos que permitem a você obter todos os disponíveis cliente objetos, adicionar novo um cliente no repositório e verificar se um cliente já está no repositório. Como o aplicativo não permite que o usuário excluir um cliente, o repositório não permite a remoção de um cliente. O evento CustomerAdded é acionado quando um novo cliente insere o CustomerRepository, via método AddCustomer.

Obviamente, modelo de dados do aplicativo é muito pequeno, em comparação com o que precisar aplicativos comerciais reais, mas que não é importante. O que é importante compreender é como as classes ViewModel fazer uso de cliente e CustomerRepository. Observe que Customer­ViewModel é um wrapper para um objeto Customer. Ela apresenta o estado de um cliente e outro estado usado pelo controle Customer­View, por meio de um conjunto de propriedades. CustomerViewModel não duplicar o estado de um cliente; ele simplesmente expõe-lo por meio de delegação, assim:

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

Quando o usuário cria um novo cliente e clica no botão salvar no controle CustomerView, o Customer­ViewModel associado que modo de exibição adicionará o novo objeto de cliente para o Customer­Repository. Que faz com que CustomerAdded evento o repositório disparado, que permite que o AllCustomers­ViewModel sabe que ele deve adicionar um novo Customer­ViewModel à sua coleção AllCustomers. De certa forma, Customer­Repository atua como um mecanismo de sincronização entre vários ViewModels que lidam com objetos de cliente. Talvez um pode pensar nisso como usando o padrão de design mediador. Eu analisará mais como isso funciona nas seções futuras, mas por ora referir-se para o diagrama na Figura 11 para uma compreensão geral de como todas as partes se combinam.

fig11.gif

A Figura 11 relações com cliente

Novo formulário de entrada de dados de cliente

Quando o usuário clica no link "Criar novo cliente", MainWindowViewModel adiciona um novo CustomerViewModel à sua lista de espaços de trabalho e um controle CustomerView exibe-lo. Depois que o usuário digita valores válidos para os campos de entrada, o botão Salvar entra no estado ativado para que o usuário pode manter as novas informações de cliente. Não há nada de comum aqui, apenas um formulário de entrada de dados regular com a validação de entrada e um botão Salvar.

A classe de cliente tem suporte, disponíveis através de sua implementação de interface IDataErrorInfo de validação internos. Se a validação garante o cliente tem um nome, um endereço de email válido, e, se o cliente é uma pessoa, e um sobrenome. Se IsCompany propriedade o cliente retornar true, a propriedade LastName não pode ter um valor a idéia sendo que uma empresa não tem um sobrenome. Essa lógica de validação pode fazer sentido da perspectiva do objeto cliente, mas ele não atender as necessidades da interface do usuário. A interface do usuário exige que um usuário selecionar se um novo cliente é uma pessoa ou uma empresa. O seletor de tipo de cliente inicialmente tem o valor "(não especificado)". Como pode a interface do usuário saber o usuário que o tipo de cliente é não especificado se a propriedade IsCompany de um cliente permite somente para um valor verdadeiro ou falso?

Supondo que você tem total controle sobre o sistema todo software, você pode alterar a propriedade IsCompany para ser do tipo Nullable <bool>, que permite que o valor "desmarcado". No entanto, o mundo real não é sempre tão simples. Suponha que você não pode alterar a classe de cliente porque ele vem de uma biblioteca herdada pertencente a uma equipe diferente em sua empresa. Se é não fácil forma persistir que "desmarcado" valor devido o esquema de banco de dados existente? E se outros aplicativos já usar a classe de cliente e contam com a propriedade sendo um valor booleano normal? Uma vez, ter um ViewModel vem a salvação.

O método de teste na Figura 12 mostra como essa funcionalidade funciona em CustomerViewModel. CustomerViewModel expõe uma propriedade de CustomerTypeOptions para que o seletor de tipo de cliente tenha seqüências de caracteres de três para serem exibidas. Ele também expõe uma propriedade CustomerType, que armazena a seqüência de caracteres selecionada no Seletor. Quando CustomerType é definido, ele mapeia o valor de seqüência de caracteres para um valor booleano para IsCompany propriedade do objeto cliente subjacente. a Figura 13 mostra as duas propriedades.

Figura 12 O método de teste

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

A Figura 13 CustomerType propriedades

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

O controle CustomerView contém uma caixa de combinação vinculada a essas propriedades, como mostrado aqui:

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

Quando o item selecionado em que caixa de combinação é alterado, IDataErrorInfo interface a fonte de dados é consultada para ver se o novo valor é válido. Que ocorre porque a ligação da propriedade SelectedItem tem ValidatesOnDataErrors definido como true. Como a fonte de dados é um objeto Customer­ViewModel, o sistema de ligação perguntará que Customer­ViewModel para um erro de validação na propriedade CustomerType. Na maioria das vezes, CustomerViewModel delega todas as solicitações de erros de validação para o objeto Customer que ele contém. No entanto, como cliente tem há noção de ter um estado não selecionado para a propriedade IsCompany, a classe CustomerViewModel deve tratar validar o novo item selecionado no controle ComboBox. Esse código é visto na Figura 14 .

A Figura 14 Validando um 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";
}

O aspecto chave desse código é que implementação do CustomerViewModel IDataErrorInfo pode manipular as solicitações para validação de propriedade específico de ViewModel e delegar as outras solicitações para o objeto Customer. Isso lhe permite fazer usar da lógica de validação em classes de modelo e ter validação adicional para propriedades que só faz sentido para ViewModel classes.

A capacidade de salvar um CustomerViewModel está disponível para um modo de exibição por meio da propriedade SaveCommand. Esse comando usa a classe de RelayCommand examinada anteriormente para permitir CustomerViewModel decidir se ele pode salvar o próprio e o que fazer quando instruído a salvar seu estado. Neste aplicativo, salvar um novo cliente significa simplesmente adicioná-lo a um CustomerRepository. Decidir se o novo cliente está pronto para ser salvo requer o consentimento de duas partes. O objeto de cliente deve ser solicitado se ele é válido ou não e o Customer­ViewModel deve decidir se ela é válida. Essa decisão de duas partes é necessário devido a propriedades específicas de ViewModel e validação examinado anteriormente. O salvamento lógica para Customer­ViewModel é mostrada na Figura 15 .

Figura 15 O salvar 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; 
    }
}

O uso de um ViewModel aqui torna muito mais fácil criar uma exibição que pode exibir um objeto Customer e permitir a coisas como um estado "desmarcado" de uma propriedade booleana. Ele também permite saber facilmente ao cliente para salvar seu estado. Se o modo de exibição foi vinculado diretamente a um objeto Customer, o modo de exibição exigiria muito código para que isso funcione corretamente. Em uma arquitetura MVVM bem projetada, code-behind para a maioria dos modos de exibição deve ser deixado em branco ou, no máximo, só contém código que manipula os controles e recursos contidos nesse modo de exibição. Às vezes, também é necessário escrever código no code-behind um modo de exibição que interage com um objeto, como conectar um evento ViewModel ou chamando um método que seria muito difícil invocar do ViewModel propriamente dito.

Exibir todos os clientes

O aplicativo de demonstração também contém um espaço de trabalho que exibe todos os clientes em uma ListView. Os clientes na lista são agrupados de acordo com se eles são uma empresa ou uma pessoa. O usuário pode selecionar um ou mais clientes de cada vez e exibir a soma de suas vendas totais no canto direito inferior.

A interface do usuário é o controle AllCustomersView, que processa um objeto AllCustomersViewModel. Cada ListView­Item representa um objeto CustomerViewModel na coleção AllCustomers exposto pelo objeto AllCustomerViewModel. Na seção anterior, você viu como um CustomerViewModel pode processar como um formulário de entrada de dados e agora o mesmo objeto CustomerViewModel exato é processado como um item em uma ListView. A classe de CustomerViewModel não tem nenhuma idéia de quais elementos visuais exibem-lo, por isso, essa reutilização é possível.

AllCustomersView cria os grupos de visto na ListView. Ele faz isso pela vinculação do ListView ItemsSource a um Collection­ViewSource configurado como Figura 16 .

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

A associação entre um ListViewItem e um objeto CustomerViewModel é estabelecida pela propriedade de ItemContainerStyle do ListView. O estilo atribuído a essa propriedade será aplicada a cada ListViewItem, que permite a propriedades em um ListViewItem deve ser vinculado propriedades on the CustomerViewModel. Uma ligação importante no estilo que cria um vínculo entre a propriedade IsSelected de um ListViewItem e a propriedade IsSelected de um Customer­ViewModel, conforme ilustrado aqui:

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

Quando um CustomerViewModel estiver marcada ou desmarcada, que faz com que a soma de total de vendas dos clientes de todos os selecionados para alterar. A classe AllCustomersViewModel é responsável por manter esse valor, para que o ContentPresenter abaixo ListView possa exibir o número correto. a Figura 17 mostra como AllCustomersViewModel monitora cada cliente que está sendo marcada ou desmarcada e notifica o modo de exibição que ele precisa atualizar o valor de exibição.

Figura 17 monitoramento para selecionada ou desmarcada

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

A interface do usuário vincula à propriedade TotalSelectedSales e aplica moeda (dinheiro) formatação para o valor. O objeto ViewModel pôde aplicar a formatação, em vez do modo de exibição, retornando um String em vez de um valor duplo da propriedade TotalSelectedSales de moeda. A propriedade ContentStringFormat do ContentPresenter foi adicionada no .NET Framework 3.5 SP1, portanto, se você deve direcionar uma versão mais antiga do WPF, será necessário aplicar a formatação no código de moeda:

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

Quebra automática para cima

O WPF tem muito a oferecer os desenvolvedores de aplicativos e treinamento para aproveitar esse poder exige uma mudança de hábito. O padrão Model-View-ViewModel é um conjunto simples e eficiente de diretrizes para projetar e implementar um aplicativo WPF. Ele permite que você criar uma forte separação entre dados, comportamento e apresentação, facilitando o controle o caos que é o desenvolvimento de software.

Eu gostaria de agradecer a John Gossman por sua ajuda com este artigo.

Josh Smith é beijos sobre como usar WPF para criar experiências de usuário grande. Ele foi concedido o título de MVP da Microsoft para o seu trabalho na comunidade do WPF. Josh funciona para Infragistics no grupo de design de experiência. Quando ele não está em um computador, ele goza de reproduzir o piano, ler sobre o histórico e explorando cidade de Nova York com seu girlfriend. Você pode visitar blog do Josh em joshsmithonwpf.wordpress.com.