Este artigo foi traduzido por máquina.

Padrões do Silverlight

Modelo-modo de exibição-ViewModel em aplicativos do Silverlight 2

Shawn Wildermuth

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

Este artigo discute:

  • Desenvolvimento do Silverlight 2
  • Modelo-modo de exibição-ViewModel padrão
  • Modos de exibição e o modelo de modo de exibição
  • Concessions ao Silverlight 2
Este artigo usa as seguintes tecnologias:
O Silverlight 2, Visual Studio

Conteúdo

O problema
Aplicativo Layering no Silverlight 2
MVVM: um Walk-Through
Criando o modelo
Modos de exibição e o modelo de modo de exibição
Concessions ao Silverlight 2
Onde É estão

Agora que 2 do Silverlight foi liberado, o número de aplicativos criados na parte superior de está crescendo e com o que vem alguns sofre growing. A estrutura básica suporte para o modelo de Silverlight 2 implica uma integração justa entre a interface do usuário (UI) e quaisquer dados que você está trabalhando. Enquanto essa integração é útil para aprender a tecnologia, ele se torna um empecilho para teste, refatoração e manutenção. VOU mostrar como separar a interface do usuário dos dados usando maduros padrões de design do aplicativo.

O problema

O núcleo do problema é grande união, que é o resultado de mixagem as camadas do seu aplicativo em conjunto. Quando tiver uma camada intimate Conhecimento sobre como outra camada faz seu trabalho e seu aplicativo está intimamente ligado. Colocar um aplicativo de entrada de dados simples que permite que você consulta para casas para venda em uma cidade específica. Em um aplicativo totalmente fraco, você pode definir a consulta para executar a pesquisa em um manipulador de botão na sua interface do usuário. Como alterar as mudanças de esquema ou a semântica da pesquisa, a camada de dados e a camada de interface do usuário precisam ser atualizados.

Isso apresenta um problema em qualidade do código e complexidade. Toda vez que os dados de camada alterações, você precisará sincronizar e testar o aplicativo para ter certeza de que as alterações não são significativas alterações. Quando tudo o que é vinculado bem juntas, qualquer movimentação em uma parte do aplicativo pode causar alterações rippling entre o restante do código. Quando você estiver criando algo simples em Silverlight2, como um player de filme ou um menu widget, fortemente união componentes do aplicativo não é provável que ser um problema. Medida que aumenta o tamanho de um projeto, no entanto, você irá achar a dificuldade e mais.

A outra parte do problema é testes de unidade. Quando um aplicativo está intimamente ligado, você poderá apenas funcional (ou interface do usuário) teste do aplicativo. Novamente, isso não é um problema com um projeto pequeno, mas conforme o crescimento de tamanho e complexidade de um projeto, poder testar camadas de aplicativo separadamente se torna muito importante. Tenha em mente essa unidade de teste não está praticamente fazendo certeza de que uma unidade funciona quando você usá-lo em um sistema mas sobre como tornar-se de que ele continua a trabalhar em um sistema. Com testes de unidade para partes de um sistema adiciona garantia que medida que muda a sistema, problemas são revealed anteriormente no processo em vez de posterior (como aconteceria com testes funcionais). Regressão teste (de exemplo, executando testes de unidade em um sistema em cada versão), em seguida, se torna crucial para garantir que pequenas alterações que são adicionadas a um sistema não será para causar erros em cascata.

Criando um aplicativo, definindo diferentes camadas pode parecer alguns desenvolvedores a ser um caso de over-engineering. O fato é que, se você criar com camadas em mente ou não, você estiver trabalhando em uma plataforma de n camadas e o aplicativo terá camadas. Mas sem planejamento formal, você acabará com um um sistema totalmente muito fraco (e os problemas detalhado anteriormente) ou um aplicativo completo do código será uma dor de cabeça de manutenção.

É fácil assumir que a criação de um aplicativo com camadas separadas ou camadas requer muita infra-estrutura para fazê-lo funcionar bem, mas na verdade, simples separação das camadas é simples de implementar. (Você pode criar camadas mais complexa de um aplicativo usando a inversão de controle técnicas, mas que aborda um problema diferente que é discutido neste artigo.)

Aplicativo Layering no Silverlight 2

O Silverlight 2 não exige inventar algo novo para ajudá-lo a decidir como um aplicativo de camada. Há alguns padrões conhecidos que você pode usar para seu design.

Um padrão que as pessoas saber muito direita agora é o padrão MVC (Model-View-Controller). No padrão MVC, o modelo é que os dados, o modo de exibição é a interface do usuário e o controlador é a interface programática entre o modo de exibição, o modelo e a entrada do usuário. Esse padrão, no entanto, não funciona bem em interfaces de usuário declarativa como o Windows Presentation Foundation (WPF) ou o Silverlight porque o XAML que usam essas tecnologias pode definir algumas da interface entre a entrada e o modo de exibição (porque a vinculação de dados, disparadores e estados podem ser declarados em XAML).

Modelo-View-Presenter (MVP) é outro padrão comuns para aplicativos de camadas. No padrão MVP, o apresentador é responsável pela configuração e gerenciamento de estado para um modo de exibição. Como MVC, MVP não é bem adequado o modelo de Silverlight 2 como o XAML pode conter a ligação de dados declarativa, disparadores e gerenciamento de estado. Portanto, onde é que deixar nos?

Felizmente para o Silverlight 2, a comunidade WPF tem rallied atrás de um padrão denominado Model-View-ViewModel (MVVM). Esse padrão é uma adaptação dos padrões MVC e MVP no qual o modelo de modo de exibição fornece um modelo de dados e o comportamento para o modo de exibição mas permite a exibição para declarativamente ligar ao modelo de modo de exibição. O modo de exibição se tornará uma mistura de XAML e C# (como o Silverlight 2 controles), o modelo representa os dados disponíveis para o aplicativo e o modelo de modo de exibição prepara o modelo para vinculá-lo ao modo de exibição.

O modelo é especialmente importante porque ele encapsula o acesso aos dados, seja acesso através de um conjunto de serviços da Web, um serviço de dados do ADO.NET ou alguma outra forma de recuperação de dados. O modelo é separado do modelo de modo de exibição para que o dados da exibir (o modelo de modo de exibição) podem ser testados em isolamento dos dados reais. a Figura 1 mostra um exemplo do padrão MVVM.

fig01.gif

Figura 1 padrão Model-View-ViewModel

MVVM: um Walk-Through

Para ajudá-lo a compreender como implementar o padrão MVVM, vamos examinar um exemplo. Este exemplo não representa necessariamente código como real deve ser usado. Ele simplesmente é projetado para explicar o padrão.

Este exemplo é composto de cinco projetos separados em uma única solução Visual Studio. (Embora não seja necessário criar cada uma das camadas como um projeto separado, geralmente é uma boa idéia.) O exemplo mais separa os projetos colocando-os em pastas de cliente e servidor. Na pasta do Server são dois projetos: an ASP.NET Web Application (MVVMExample) que irá hospedar nossos projetos do Silverlight e serviços e um projeto de biblioteca de .NET que contém o modelo de dados.

Na pasta cliente são três projetos: modelo de INTERFACE de nosso aplicativo, uma biblioteca de cliente do Silverlight (MVVM.Client.Data) que contém o modelo e o modo de exibição de um projeto do Silverlight (MVVM.Client) para o principal, bem como referências de serviço e testa um projeto do Silverlight (MVVM.Client.Tests) que contém a unidade. Você pode ver a subdivisão desses projetos na Figura 2 .

fig02.gif

A Figura 2 layout de projeto

Para esse exemplo, usei ASP.NET, estrutura de entidades e um serviço de dados do ADO.NET no servidor. Basicamente, ter um modelo de dados simples no servidor que expõem através de um serviço com base em REST. Consulte meu artigo de setembro de 2008 como usar serviços de dados ADO.NET no Silverlight 2 "Serviços de dados: criar aplicativos da Web de dados centralizada com o Silverlight 2"para uma explicação mais sobre esses detalhes.

Criando o modelo

Para habilitar camadas em nosso aplicativo do Silverlight, primeiro precisamos definir o modelo para os dados do aplicativo no projeto MVVM.Client.Data. Parte da definição de modelo é determinar os tipos de entidades que você pretende trabalhar dentro de um aplicativo. Os tipos de entidades dependem de como o aplicativo interagirá com os dados do servidor. Por exemplo, se você estiver usando serviços da Web, suas entidades são provavelmente será classes de contrato de dados que são geradas pelo criando uma referência de serviço para o serviço da Web. Como alternativa, você pode usar elementos XML simples se você estiver recuperando XML não processado no acesso a dados. Aqui, usarei um serviço de dados do ADO.NET, então quando eu crio uma referência de serviço ao serviço de dados, um conjunto de entidades é criada para mim.

Neste exemplo, a referência de serviço criado três classes que nos interessa: jogo, fornecedor e GameEntities (o objeto de contexto para acessar o serviço de dados). As classes de jogo e de fornecedor são as entidades reais que nós usam para interagir com o modo de exibição e a classe GameEntities é usada internamente para acessar o serviço de dados para recuperar dados.

Antes que possa criar o modelo, no entanto, precisamos criar uma interface de comunicação entre o modelo e o modelo de modo de exibição. Essa interface normalmente inclui quaisquer métodos, propriedades e eventos que são necessárias para acessar os dados. Esse conjunto de funcionalidade é representado por uma interface para permitir que ele seja substituído por outras implementações conforme necessário (teste, por exemplo). A interface de modelo neste exemplo, mostrado aqui, é chamada IGameCatalog.

public interface IGameCatalog
{
  void GetGames();
  void GetGamesByGenre(string genre);
  void SaveChanges();

  event EventHandler<GameLoadingEventArgs> GameLoadingComplete;
  event EventHandler<GameCatalogErrorEventArgs> GameLoadingError;
  event EventHandler GameSavingComplete;
  event EventHandler<GameCatalogErrorEventArgs> GameSavingError;
}

A interface IGameCatalog contém métodos para recuperar e salvar os dados. Entretanto, nenhuma das operações de retornar dados reais. Em vez disso, eles têm eventos correspondentes para sucesso e falha. Esse comportamento permite execução assíncrona para resolver a necessidade de Silverlight 2 de atividade da rede assíncrona. Embora um design assíncrono no WPF geralmente é recomendado, esse design específico funciona bem em Silverlight 2 porque o Silverlight 2 requer asynchronicity.

Para habilitar a notificação dos resultados para o chamador da nossa interface, o exemplo implementa uma classe GameLoadingEventArgs que é usada nos eventos para enviar os resultados de uma solicitação. Essa classe expõe nosso tipo de entidade (jogo) como uma lista de entidades enumeráveis que contém os resultados o chamador solicitado, como você pode ver no código a seguir.

public class GameLoadingEventArgs : EventArgs
{
  public IEnumerable<Game> Results { get; private set; }

  public GameLoadingEventArgs(IEnumerable<Game> results)
  {
    Results = results;
  }
}

Agora que definimos nossa interface, podemos criar a classe de modelo (GameCatalog) que implementa a interface IGameCatalog. A classe GameCatalog simplesmente quebra o serviço de dados ADO.NET para que quando chegar uma solicitação de dados (GetGames ou GetGamesByGenre), ele executa a solicitação e lança um evento que contém os dados (ou um erro,), um caso. Esse código deve simplificar o acesso aos dados sem imparting qualquer conhecimento específico para o chamador. A classe inclui um construtor sobrecarregado para especificar o URI do serviço, mas que não é sempre necessário e pode ser implementado como um elemento de configuração em vez disso. a Figura 3 mostra o código para a classe GameCatalog.

Figura 3 A classe GameCatalog

public class GameCatalog : IGameCatalog
{
  Uri theServiceRoot;
  GamesEntities theEntities;
  const int MAX_RESULTS = 50;

  public GameCatalog() : this(new Uri("/Games.svc", UriKind.Relative))
  {
  }

  public GameCatalog(Uri serviceRoot)
  {
    theServiceRoot = serviceRoot;
  }

  public event EventHandler<GameLoadingEventArgs> GameLoadingComplete;
  public event EventHandler<GameCatalogErrorEventArgs> GameLoadingError;
  public event EventHandler GameSavingComplete;
  public event EventHandler<GameCatalogErrorEventArgs> GameSavingError;

  public void GetGames()
  {
    // Get all the games ordered by release date
    var qry = (from g in Entities.Games
               orderby g.ReleaseDate descending
               select g).Take(MAX_RESULTS) as DataServiceQuery<Game>;

    ExecuteGameQuery(qry);
  }

  public void GetGamesByGenre(string genre)
  {
    // Get all the games ordered by release date
    var qry = (from g in Entities.Games
               where g.Genre.ToLower() == genre.ToLower()
               orderby g.ReleaseDate
               select g).Take(MAX_RESULTS) as DataServiceQuery<Game>;

    ExecuteGameQuery(qry);
  }

  public void SaveChanges()
  {
    // Save Not Yet Implemented
    throw new NotImplementedException();
  }

  // Call the query asynchronously and add the results to the collection
  void ExecuteGameQuery(DataServiceQuery<Game> qry)
  {
    // Execute the query
    qry.BeginExecute(new AsyncCallback(a =>
    {
      try
      {
        IEnumerable<Game> results = qry.EndExecute(a);

        if (GameLoadingComplete != null)
        {
          GameLoadingComplete(this, new GameLoadingEventArgs(results));
        }
      }
      catch (Exception ex)
      {
        if (GameLoadingError != null)
        {
          GameLoadingError(this, new GameCatalogErrorEventArgs(ex));
        }
      }

    }), null);
  }

  GamesEntities Entities
  {
    get
    {
      if (theEntities == null)
      {
        theEntities = new GamesEntities(theServiceRoot);
      }
      return theEntities;
    }
  }
}

Observe o método ExecuteGameQuery, que utiliza a consulta de serviço de dados do ADO.NET e executa-lo. Esse método executa o resultado de forma assíncrona e retorna o resultado para o chamador.

Observe que o modelo executa a consulta, mas simplesmente aciona eventos quando ela for concluída. Você pode observar isso e estar se perguntando por que o modelo não garantir que os eventos empacotar as chamadas para o thread da interface do usuário no Silverlight 2. O motivo é que o Silverlight (como seus outros usuário interface brethren, como Windows Forms e WPF) só pode atualizar a interface do usuário de um principal ou o segmento de interface do usuário. Mas se é isso que empacotamento nesse código, ele deve vincular nosso modelo da interface de usuário, que é exatamente contador nosso propósito estabelecido (separando as questões). Se você pressupõe que os dados precisam ser retornado no thread da interface do usuário, você vincula essa classe a chamadas de interface do usuário, mas isso é antithetical de por que usar camadas separadas em um aplicativo.

Modos de exibição e o modelo de modo de exibição

Pode parecer óbvio para criar o modelo de modo de exibição para expor dados diretamente para o nosso class(es) de modo de exibição. O problema com essa abordagem é que o modelo de modo de exibição só deve expor dados necessária diretamente pelo modo de exibição; portanto, você precisa entender o que o modo de exibição precisa. Em muitos casos, você irá ser criando o modelo de modo de exibição e o modo de exibição em paralelo, refatoração o modelo de modo de exibição quando o modo de exibição tem novos requisitos. Embora o modelo de modo de exibição é expor dados para o modo de exibição, o modo de exibição também está interagindo com as classes de entidade (indiretamente porque entidades do modelo de estão sendo passadas para o modo de exibição pelo modelo do modo de exibição).

Neste exemplo, temos um design simples que é usado para procurar dados do jogo XBox 360, como mostrado na Figura 4 . Esse design significa que precisamos uma lista de entidades jogos de nosso modelo filtrado por gênero (selecionado por meio a lista drop-down). Para atender a esse requisito, o exemplo precisa de um modelo de modo de exibição que expõe o seguinte:

  • Uma lista de jogo ligação de dados para o gênero selecionado no momento.
  • Um método para fazer a solicitação para o gênero selecionado.
  • Um evento que chama a interface do usuário que a lista de jogos foi atualizada (porque nossas solicitações de dados serão assíncronas).

fig04.gif

A Figura 4 exemplo interface do usuário

Depois que nosso modelo do modo de exibição oferece suporte a esse conjunto de requisitos, ele pode ser vinculado ao XAML diretamente, conforme mostrado no GameView.XAML (localizado no projeto MVVM.Client). Essa ligação é implementada por criar uma nova instância do modelo de modo de exibição nos recursos do modo de exibição e, em seguida, vinculando o recipiente principal (uma grade neste caso) ao modelo de modo de exibição. Isso implica que o arquivo que XAML inteiro será dados vinculados com base no modo de exibição modelo diretamente. a Figura 5 mostra o código GameView.XAML.

A Figura 5 GameView.XAML

// GameView.XAML
<UserControl x:Class="MVVM.Client.Views.GameView"
             xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:data="clr-namespace:MVVM.Client.Data;assembly=MVVM.Client.Data">

  <UserControl.Resources>
    <data:GamesViewModel x:Key="TheViewModel" />
  </UserControl.Resources>

  <Grid x:Name="LayoutRoot"
        DataContext="{Binding Path=Games, Source={StaticResource TheViewModel}}">
    ...
  </Grid>
</UserControl>

Nosso modelo da exibição precisa atender esses requisitos usando uma interface IGameCatalog. Em geral, sua útil ter o construtor padrão de um modelo de modo de exibição criar um modelo padrão para que a ligação ao XAML é fácil, mas você também deve incluir uma sobrecarga do construtor no qual o modelo é fornecido para permitir cenários, como teste. O modelo de modo de exibição de exemplo (GameViewModel) é semelhante a Figura 6 .

A Figura 6 GameViewModel classe

public class GamesViewModel
{
  IGameCatalog theCatalog;
  ObservableCollection<Game> theGames = new ObservableCollection<Game>();

  public event EventHandler LoadComplete;
  public event EventHandler ErrorLoading;

 public GamesViewModel() : 
    this(new GameCatalog())
  {
  }

  public GamesViewModel(IGameCatalog catalog)
  {
    theCatalog = catalog;
    theCatalog.GameLoadingComplete += 
      new EventHandler<GameLoadingEventArgs>(games_GameLoadingComplete);
    theCatalog.GameLoadingError += 
      new EventHandler<GameCatalogErrorEventArgs>(games_GameLoadingError);
  }

  void games_GameLoadingError(object sender, GameCatalogErrorEventArgs e)
  {
    // Fire Event on UI Thread
    Application.Current.RootVisual.Dispatcher.BeginInvoke(() =>
      {
        if (ErrorLoading != null) ErrorLoading(this, null);
      });
  }

  void games_GameLoadingComplete(object sender, GameLoadingEventArgs e)
  {
    // Fire Event on UI Thread
    Application.Current.RootVisual.Dispatcher.BeginInvoke(() =>
      {
        // Clear the list
        theGames.Clear();

        // Add the new games
        foreach (Game g in e.Results) theGames.Add(g);

        if (LoadComplete != null) LoadComplete(this, null);
      });
  }

  public void LoadGames()
  {
    theCatalog.GetGames();
  }

  public void LoadGamesByGenre(string genre)
  {
    theCatalog.GetGamesByGenre(genre);
  }

  public ObservableCollection<Game> Games
  {
    get
    {
      return theGames;
    }
  }
}

De interesse particular no modelo de modo de exibição são os manipuladores de para GameLoadingComplete (e GameLoadingError). Esses manipuladores de receber eventos do modelo e, em seguida, disparar eventos para o modo de exibição. <game>O que é interessante aqui é que o modelo passa o modelo de modo de exibição lista de resultados, mas em vez de passar os resultados diretamente para o modo de exibição base, o modelo de modo de exibição armazena os resultados na própria lista ligável (ObservableCollection <jogo>).

Esse comportamento ocorre porque o modelo de modo de exibição está sendo vinculado diretamente ao modo de exibição, portanto, os resultados aparecerá no modo de exibição por meio de ligação de dados. Porque o modelo de modo de exibição tem conhecimento de interface de usuário (porque sua finalidade é atender a solicitações de interface do usuário), ele pode, em seguida, verifique se que os eventos que ele é acionado acontecerem no segmento de interface do usuário (via Dispatcher.BeginInvoke, embora você possa usar outros métodos para chamar no thread da interface do usuário se você preferir).

Concessions ao Silverlight 2

O padrão MVVM é usado em muitos projetos do WPF para grande sucesso. O problema usá-lo no Silverlight 2 é que para tornar esse padrão fácil e simples, Silverlight 2 realmente precisa oferecer suporte a comandos e disparadores. Se que o caso, nós poderia ter o XAML chamar os métodos do modelo de modo de exibição diretamente quando o usuário interage com o aplicativo.

No Silverlight 2 esse comportamento requer um pouco mais trabalho, mas felizmente envolve escrever apenas um pouco código. Por exemplo, quando o usuário seleciona um gênero diferente usando a lista drop-down, gostaríamos tem um comando que executa o método GameViewModel.GetGameByGenre para nós. Como a infra-estrutura necessária não está disponível, simplesmente precisamos usar código para fazer a mesma coisa. Quando a caixa de combinação (genreComboBox) seleção é alterada, as cargas de exemplo que os jogos do modo de exibição do modelo manualmente no código em vez de em um comando. Tudo o que é necessário aqui é que a solicitação para carregar os dados acontece — porque nós são vinculados a lista de jogos, o modelo de modo de exibição subjacente será simplesmente alterar a coleção que são vinculados a e os dados atualizados aparece automaticamente. Você pode ver este código na Figura 7 .

A Figura 7 atualização Data in a interface do usuário

void genreComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
  string item = genreComboBox.SelectedItem as string;
  if (item != null)
  {
    LoadGames(item);
  }
}

void LoadGames(string genre)
{
  loadingBar.Visibility = Visibility.Visible;
  if (genre == "(All)")
  {
    viewModel.LoadGames();
  }
  else
  {
    viewModel.LoadGamesByGenre(genre);
  }

}

Existem diversos lugares onde a falta de ligação de elemento e comandos forçará Silverlight 2 desenvolvedores manipular esse comportamento no código. Como o código faz parte do modo de exibição, isso não quebrará as camadas do aplicativo, não é tão simples como os exemplos de XAML todos os que você verá no WPF.

Onde É estão

O Silverlight 2 não exige a criação de aplicativos monolíticos. Layering aplicativos do Silverlight 2 é simples usando o padrão Model-View-ViewModel que nós Felizmente emprestado de nosso brethren do WPF. Além disso, usar essa abordagem camadas permite que você acoplar livremente as responsabilidades em seus aplicativos para que fiquem mais fácil de manter, estender, testar e implantar.

Gostaria agradecer a Bugnion Laurent (autor do Silverlight 2 Unleashed), bem como outras pessoas na lista de endereçamento de WPF Disciples para sua Ajuda na conclusão deste artigo. Blogs de Laurent em blog.galasoft.CH.

Shawn Wildermuth é um MVP da Microsoft (C#) e fundador da Wildermuth Consulting Services. Ele é autor de vários livros e artigos vários. Além disso, Shawn atualmente executa o tour do Silverlight ensinando 2 do Silverlight em torno do país. Ele pode ser contatado em Shawn@wildermuthconsulting.com.