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
- Abra o Visual Studio
- Na tela Iniciar, selecione Criar projeto.
- Pesquise por "WPF", escolha Aplicativo WPF (.NET Core) e, em seguida, escolhaAvançar.
- Na próxima tela, dê um nome ao projeto, por exemplo, GetStartedWPF, e escolha Criar.
Instalar os pacotes NuGet do Entity Framework
Clique com o botão direito do mouse na solução e escolha Gerenciar Pacotes NuGet para Solução...
Digite
entityframeworkcore.sqlite
na caixa de pesquisa.Selecione o pacote Microsoft.EntityFrameworkCore.Sqlite.
Verifique o projeto no painel direito e clique em Instalar
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.
Clique duas vezes em MainWindow.xaml no Gerenciador de Soluções para abrir o formulário principal
Escolha a guia XAML para editar o XAML.
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>
Isso configura a origem para as categorias "pai" e a segunda fonte para os produtos "detalhes".
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>
Observe que a
CategoryId
está definida comoReadOnly
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:
Adicionar código que manipula a interação de dados
É hora de adicionar alguns manipuladores de eventos à janela principal.
Na janela XAML, clique no elemento <Janela>, para selecionar a janela principal.
Na janela Propriedades, escolha Eventos na parte superior direita e clique duas vezes na caixa de texto à direita do rótulo Carregado.
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.
Notificação de alteração de propriedade
Este exemplo conta com quatro etapas para sincronizar as entidades com a interface do usuário.
- A chamada
_context.Categories.Load()
inicial carrega os dados das categorias. - Os proxies de carregamento lento carregam os dados de produtos dependentes.
- 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()
. - 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.