Introdução ao WPF

Este passo a passo mostra como associar tipos POCO aos controles do WPF em um formulário de "detalhes principais". O aplicativo usa as APIs do Entity Framework para preencher objetos com dados do banco de dados, controlar alterações e persistir dados no banco de dados.

O modelo define dois tipos que participam da relação um-para-muitos: Categoria (entidade de segurança\principal) e Produto (dependente\detalhe). A estrutura de associação de dados do WPF permite a navegação entre objetos relacionados: selecionar linhas no modo de exibição mestre faz com que a exibição de detalhes seja atualizada com os dados filho correspondentes.

As capturas de tela e as listas de códigos neste passo a passo foram tiradas do Visual Studio 2019 16.6.5.

Dica

Veja o exemplo deste artigo no GitHub.

Pré-Requisitos

Você precisa ter o Visual Studio 2019 16.3 ou posterior instalado com a carga de trabalho do desktop .NET selecionada para concluir este passo a passo. Para obter mais informações sobre como instalar a versão mais recente do Visual Studio, confira Instalar o Visual Studio .

Criar o aplicativo

  1. Abra o Visual Studio
  2. Na tela Iniciar, selecione Criar projeto.
  3. Pesquise por "WPF", escolha Aplicativo WPF (.NET Core) e, em seguida, escolhaAvançar.
  4. Na próxima tela, dê um nome ao projeto, por exemplo, GetStartedWPF, e escolha Criar.

Instalar os pacotes NuGet do Entity Framework

  1. Clique com o botão direito do mouse na solução e escolha Gerenciar Pacotes NuGet para Solução...

    Manage NuGet Packages

  2. Digite entityframeworkcore.sqlite na caixa de pesquisa.

  3. Selecione o pacote Microsoft.EntityFrameworkCore.Sqlite.

  4. Verifique o projeto no painel direito e clique em Instalar

    Sqlite Package

  5. Repita as etapas para pesquisar entityframeworkcore.proxies e instalar Microsoft.EntityFrameworkCore.Proxies.

Observação

Ao instalar o pacote Sqlite, ele retira automaticamente o pacote base Microsoft.EntityFrameworkCore relacionado. O pacote Microsoft.EntityFrameworkCore.Proxies fornece suporte para dados de "carregamento lento". Isso significa que, quando você tem entidades com entidades filho, somente os pais são buscados na carga inicial. Os proxies detectam quando há uma tentativa de acessar as entidades filho e as carrega automaticamente sob demanda.

Definir um modelo

Neste passo a passo um modelo será implementado usando o "Code First". Isso significa que o EF Core criará as tabelas de dados e o esquema com base nas classes C# que você definir.

Adicionar uma nova classe. Forneça o nome: Product.cs e preencha dessa forma:

Product.cs

namespace GetStartedWPF
{
    public class Product
    {
        public int ProductId { get; set; }
        public string Name { get; set; }

        public int CategoryId { get; set; }
        public virtual Category Category { get; set; }
    }
}

Em seguida, adicione uma classe nomeada Category.cs e preencha-a com o seguinte código:

Category.cs

using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace GetStartedWPF
{
    public class Category
    {
        public int CategoryId { get; set; }
        public string Name { get; set; }

        public virtual ICollection<Product>
            Products
        { get; private set; } =
            new ObservableCollection<Product>();
    }
}

A propriedade Products na classe Category e a propriedade Category na classe Product são propriedades de navegação. No Entity Framework, as propriedades de navegação fornecem uma maneira de navegar em uma relação entre dois tipos de entidade.

Além de definir entidades, você precisa definir uma classe que deriva de DbContext e expõe as propriedades DbSet<TEntity>. As propriedades DbSet<TEntity> informam ao contexto quais tipos você deseja incluir no modelo.

Uma instância do tipo derivado DbContext gerencia os objetos de entidade durante o tempo de execução, o que inclui preencher objetos com dados de um banco de dados, controle de alterações e persistência de dados no banco de dados.

Adicione uma nova classe ProductContext.cs ao projeto com a seguinte definição:

ProductContext.cs

using Microsoft.EntityFrameworkCore;

namespace GetStartedWPF
{
    public class ProductContext : DbContext
    {
        public DbSet<Product> Products { get; set; }
        public DbSet<Category> Categories { get; set; }

        protected override void OnConfiguring(
            DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite(
                "Data Source=products.db");
            optionsBuilder.UseLazyLoadingProxies();
        }
    }
}
  • O DbSet informa ao EF Core quais entidades C# devem ser mapeadas para o banco de dados.
  • Há várias maneiras de configurar o DbContext do EF Core. É possível ler sobre eles em: Configurar um DbContext.
  • Este exemplo usa a substituição OnConfiguring para especificar um arquivo de dados Sqlite.
  • A chamada UseLazyLoadingProxies informa ao EF Core para implementar o carregamento lento, para que as entidades filho sejam carregadas automaticamente quando acessadas a partir do pai.

Pressione CTRL+SHIFT+B ou navegue até Compilar > Solução de compilação para compilar o projeto.

Dica

Saiba mais sobre a diferença de manter o banco de dados e os modelos do EF Core em sincronia: Gerenciando esquemas de banco de dados.

Carregamento lento

A propriedade Products na classe Category e a propriedade Category na classe Product são propriedades de navegação. No Entity Framework Core, as propriedades de navegação fornecem uma maneira de orientar uma relação entre dois tipos de entidade.

O EF Core oferece uma opção de carregar entidades relacionadas a partir do banco de dados de forma automática, na primeira vez que você acessar a propriedade de navegação. Com esse tipo de carregamento (chamado de carregamento lento), não esqueça que na primeira vez que acessar cada propriedade de navegação, uma consulta à parte será executada no banco de dados, se o conteúdo ainda não estiver no contexto.

Ao usar tipos de entidade "POCO" (Objeto CRL básico), o EF Core obtém o carregamento lento criando instâncias com tipos de proxy derivados durante o runtime e, em seguida, substituindo as propriedades virtuais em suas classes para adicionar o gancho de carregamento. Para obter o carregamento lento de objetos relacionados, você deve declarar os getters de propriedade de navegação como públicos evirtuais (Overridable no Visual Basic) e sua classe não deve ser selada (NotOverridable no Visual Basic). Ao usar o Database First, as propriedades de navegação automaticamente se tornam virtuais para habilitar o carregamento lento.

Associar objeto a controles

Adicione as classes definidas no modelo como fontes de dados para este aplicativo WPF.

  1. Clique duas vezes em MainWindow.xaml no Gerenciador de Soluções para abrir o formulário principal

  2. Escolha a guia XAML para editar o XAML.

  3. Imediatamente após a marca Window de abertura, adicione as seguintes fontes para se conectar às entidades do EF Core.

    <Window x:Class="GetStartedWPF.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:GetStartedWPF"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800" Loaded="Window_Loaded">
        <Window.Resources>
            <CollectionViewSource x:Key="categoryViewSource"/>
            <CollectionViewSource x:Key="categoryProductsViewSource" 
                                  Source="{Binding Products, Source={StaticResource categoryViewSource}}"/>
        </Window.Resources>
    
  4. Isso configura a origem para as categorias "pai" e a segunda fonte para os produtos "detalhes".

  5. Em seguida, adicione a marcação a seguir ao XAML após a marca Grid de abertura.

    <DataGrid x:Name="categoryDataGrid" AutoGenerateColumns="False" 
              EnableRowVirtualization="True" 
              ItemsSource="{Binding Source={StaticResource categoryViewSource}}" 
              Margin="13,13,43,229" RowDetailsVisibilityMode="VisibleWhenSelected">
        <DataGrid.Columns>
            <DataGridTextColumn Binding="{Binding CategoryId}"
                                Header="Category Id" Width="SizeToHeader"
                                IsReadOnly="True"/>
            <DataGridTextColumn Binding="{Binding Name}" Header="Name" 
                                Width="*"/>
        </DataGrid.Columns>
    </DataGrid>
    
  6. Observe que a CategoryId está definida como ReadOnly porque ela é atribuída pelo banco de dados e não pode ser alterada.

Adicionar uma grade de detalhes

Agora que a grade existe para exibir categorias, a grade de detalhes pode ser adicionada para mostrar produtos. Adicione isso dentro do elemento Grid, após o elemento DataGrid das categorias.

MainWindow.xaml

<DataGrid x:Name="productsDataGrid" AutoGenerateColumns="False" 
          EnableRowVirtualization="True" 
          ItemsSource="{Binding Source={StaticResource categoryProductsViewSource}}" 
          Margin="13,205,43,108" RowDetailsVisibilityMode="VisibleWhenSelected" 
          RenderTransformOrigin="0.488,0.251">
    <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding CategoryId}" 
                            Header="Category Id" Width="SizeToHeader"
                            IsReadOnly="True"/>
        <DataGridTextColumn Binding="{Binding ProductId}" Header="Product Id" 
                            Width="SizeToHeader" IsReadOnly="True"/>
        <DataGridTextColumn Binding="{Binding Name}" Header="Name" Width="*"/>
    </DataGrid.Columns>
</DataGrid>

Por fim, adicione um botão Save e uma transmissão no evento de clique para Button_Click.

<Button Content="Save" HorizontalAlignment="Center" Margin="0,240,0,0" 
        Click="Button_Click" Height="20" Width="123"/>

Seu modo de exibição de Design deve ter esta aparência:

Screenshot of WPF Designer

Adicionar código que manipula a interação de dados

É hora de adicionar alguns manipuladores de eventos à janela principal.

  1. Na janela XAML, clique no elemento <Janela>, para selecionar a janela principal.

  2. Na janela Propriedades, escolha Eventos na parte superior direita e clique duas vezes na caixa de texto à direita do rótulo Carregado.

    Main Window Properties

Isso o leva ao código por trás do formulário e agora editaremos o código para usar o ProductContext a fim de realizar o acesso aos dados. Atualizar o código, conforme mostrado abaixo.

O código declara uma instância de execução longa de ProductContext. O objeto ProductContext é usado para consultar e salvar dados no banco de dados. Então, o método Dispose() na instância ProductContext é chamado do método OnClosing substituído. Os comentários do código explicam o que faz cada etapa.

MainWindow.xaml.cs

using Microsoft.EntityFrameworkCore;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;

namespace GetStartedWPF
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private readonly ProductContext _context =
            new ProductContext();

        private CollectionViewSource categoryViewSource;

        public MainWindow()
        {
            InitializeComponent();
            categoryViewSource =
                (CollectionViewSource)FindResource(nameof(categoryViewSource));
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // this is for demo purposes only, to make it easier
            // to get up and running
            _context.Database.EnsureCreated();

            // load the entities into EF Core
            _context.Categories.Load();

            // bind to the source
            categoryViewSource.Source =
                _context.Categories.Local.ToObservableCollection();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            // all changes are automatically tracked, including
            // deletes!
            _context.SaveChanges();

            // this forces the grid to refresh to latest values
            categoryDataGrid.Items.Refresh();
            productsDataGrid.Items.Refresh();
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            // clean up database connections
            _context.Dispose();
            base.OnClosing(e);
        }
    }
}

Observação

O código usa uma chamada para EnsureCreated() compilar o banco de dados na primeira execução. Isso é aceitável em demonstrações, mas em aplicativos de produção você deve conferir as migrações para gerenciar seu esquema. O código também é executado de forma síncrona porque usa um banco de dados SQLite local. Para cenários de produção que normalmente envolvem um servidor remoto, considere usar as versões assíncronas dos métodos Load e SaveChanges.

Testar o aplicativo WPF

Compile e execute o aplicativo pressionando F5 ou escolhendo Depurar > Iniciar depuração. O banco de dados deve ser criado automaticamente com um arquivo chamado products.db. Insira um nome de categoria e pressione ENTER, depois adicione produtos à grade inferior. Clique em Salvar e inspecione a atualização da grade com as IDs fornecidas pelo banco de dados. Destaque uma linha e clique em Excluir para remover a linha. A entidade será excluída quando você clicar em Salvar.

Running application

Notificação de alteração de propriedade

Este exemplo conta com quatro etapas para sincronizar as entidades com a interface do usuário.

  1. A chamada _context.Categories.Load() inicial carrega os dados das categorias.
  2. Os proxies de carregamento lento carregam os dados de produtos dependentes.
  3. O controle de alterações interno do EF Core faz as modificações necessárias nas entidades, inclusive inserções e exclusões, ao chamar _context.SaveChanges().
  4. As chamadas para o DataGridView.Items.Refresh() força um recarregamento das IDs recém-geradas.

Isso funciona em nosso exemplo de introdução, mas código adicional pode ser necessário em outros cenários. Os controles do WPF renderizam a interface do usuário lendo os campos e as propriedades em suas entidades. Quando você edita um valor na interface do usuário, esse valor é passado para sua entidade. Ao alterar o valor de uma propriedade diretamente em sua entidade, como ao carregá-la a partir do banco de dados, o WPF não apresenta as alterações na interface do usuário imediatamente. O mecanismo de renderização deve ser notificado sobre as alterações. O projeto fez isso chamando Refresh() manualmente. Implementar a interface INotifyPropertyChanged é uma maneira fácil de automatizar essa notificação. Os componentes do WPF detectarão automaticamente a interface e se registrarão para eventos de alteração. A entidade é responsável por gerar esses eventos.

Dica

Para saber mais sobre como lidar com as alterações, leia: Como implementar a notificação de alteração de propriedade.

Próximas etapas

Saiba mais sobre como Configurar um DbContext.