Share via


Windows Forms 시작

이 단계별 연습에서는 SQLite 데이터베이스에서 지원되는 간단한 Windows Forms(WinForms) 애플리케이션을 빌드하는 방법을 보여 줍니다. 애플리케이션은 EF Core(Entity Framework Core)를 사용하여 데이터베이스에서 데이터를 로드하고, 이 데이터에 대한 변경 내용을 추적하고, 이러한 변경 내용을 다시 데이터베이스에 다시 유지합니다.

이 연습의 스크린샷 및 코드 목록은 Visual Studio 2022 17.3.0에서 가져온 것입니다.

GitHub에서 이 문서의 샘플을 볼 수 있습니다.

사전 요구 사항

이 연습을 완료하려면 Visual Studio 2022 17.3 이상이 설치되어 있고 .NET 데스크톱 워크로드가 선택되어야 합니다. 최신 버전의 Visual Studio 설치에 대한 자세한 내용은 Visual Studio 설치를 참조하세요.

애플리케이션 만들기

  1. Visual Studio를 엽니다.

  2. 시작 창에서 새 프로젝트 만들기를 선택합니다.

  3. Windows Forms 앱을 선택한 다음 다음을 선택합니다.

    Create a new Windows Forms project

  4. 다음 화면에서 프로젝트 이름(예: GetStartedWPF)을 지정하고 다음을 선택합니다.

  5. 다음 화면에서 사용할 .NET 버전을 선택합니다. 이 연습은 .NET 7을 사용하여 만들어졌지만 이후 버전에서도 작동해야 합니다.

  6. 만들기를 선택합니다.

EF Core NuGet 패키지 설치

  1. 솔루션을 마우스 오른쪽 단추로 클릭하고 솔루션용 NuGet 패키지 관리를 선택합니다.

    Manage NuGet Packages for Solution

  2. 찾아보기 탭을 선택하고 “Microsoft.EntityFrameworkCore.Sqlite”를 검색합니다.

  3. Microsoft.EntityFrameworkCore.Sqlite 패키지를 선택합니다.

  4. 오른쪽 창에서 GetStartedWinForms 프로젝트를 확인합니다.

  5. 최신 버전을 선택합니다. 시험판 버전을 사용하려면 시험판 포함 상자가 선택되어 있는지 확인합니다.

  6. 설치를 클릭합니다.

    Install the Microsoft.EntityFrameworkCore.Sqlite package

참고 항목

Microsoft.EntityFrameworkCore.Sqlite 는 SQLite 데이터베이스와 함께 EF Core를 사용하기 위한 “데이터베이스 공급자” 패키지입니다. 다른 데이터베이스 시스템에도 비슷한 패키지를 사용할 수 있습니다. 데이터베이스 공급자 패키지를 설치하면 해당 데이터베이스 시스템에서 EF Core를 사용하는 데 필요한 모든 종속성이 자동으로 적용됩니다. 여기에는 Microsoft.EntityFrameworkCore 기본 패키지가 포함됩니다.

모델 정의

이 연습에서는 “Code First”를 사용하여 모델을 구현합니다. 즉, EF Core는 사용자가 정의하는 C# 클래스를 기반으로 데이터베이스 테이블 및 스키마를 만듭니다. 대신 기존 데이터베이스를 사용하는 방법을 보려면 데이터베이스 스키마 관리를 참조하세요.

  1. 프로젝트를 마우스 오른쪽 단추로 클릭하고 추가, 클래스...를 차례로 선택하여 새 클래스를 추가합니다.

    Add new class

  2. 파일 이름을 Product.cs로 사용하고 클래스의 코드를 다음으로 바꿉니다.

    using System.ComponentModel;
    
    namespace GetStartedWinForms;
    
    public class Product
    {
        public int ProductId { get; set; }
    
        public string? Name { get; set; }
    
        public int CategoryId { get; set; }
        public virtual Category Category { get; set; } = null!;
    }
    
  3. 반복하여 다음 코드를 사용하여 Category.cs를 만듭니다.

    using Microsoft.EntityFrameworkCore.ChangeTracking;
    
    namespace GetStartedWinForms;
    
    public class Category
    {
        public int CategoryId { get; set; }
    
        public string? Name { get; set; }
    
        public virtual ObservableCollectionListSource<Product> Products { get; } = new();
    }
    

Category 클래스의 Products 속성과 Product 클래스의 Category 속성을 “탐색”이라고 합니다. EF Core에서 탐색은 두 엔터티 형식 간의 관계를 정의합니다. 이 경우 Product.Category 탐색은 지정된 제품이 속한 범주를 참조합니다. 마찬가지로 Category.Products 컬렉션 탐색에는 지정된 범주에 대한 모든 제품이 포함됩니다.

Windows Forms를 사용하는 경우 IListSource를 구현하는 ObservableCollectionListSource는 컬렉션 탐색에 사용할 수 있습니다. 이는 필수적이지는 않지만 양방향 데이터 바인딩 환경을 향상시킵니다.

DbContext 정의

EF Core에서는 DbContext로부터 파생된 클래스가 모델에서 엔터티 형식을 구성하고 데이터베이스와 상호 작용하기 위한 세션 역할을 하는 데 사용됩니다. 가장 간단한 경우 DbContext 클래스는 다음과 같습니다.

  • 모델의 각 엔터티 형식에 대한 DbSet 속성을 포함합니다.
  • 사용할 데이터베이스 공급자 및 연결 문자열을 구성하도록 OnConfiguring 메서드를 재정의합니다. 자세한 내용은 DbContext구성을 참조하세요.

또한 이 경우 DbContext 클래스는 애플리케이션에 대한 몇 가지 샘플 데이터를 제공하기 위해 OnModelCreating 메서드를 재정의합니다.

다음 코드를 사용하여 새 ProductsContext.cs 클래스를 프로젝트에 추가합니다.

using Microsoft.EntityFrameworkCore;

namespace GetStartedWinForms;

public class ProductsContext : 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");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Category>().HasData(
            new Category { CategoryId = 1, Name = "Cheese" },
            new Category { CategoryId = 2, Name = "Meat" },
            new Category { CategoryId = 3, Name = "Fish" },
            new Category { CategoryId = 4, Name = "Bread" });

        modelBuilder.Entity<Product>().HasData(
            new Product { ProductId = 1, CategoryId = 1, Name = "Cheddar" },
            new Product { ProductId = 2, CategoryId = 1, Name = "Brie" },
            new Product { ProductId = 3, CategoryId = 1, Name = "Stilton" },
            new Product { ProductId = 4, CategoryId = 1, Name = "Cheshire" },
            new Product { ProductId = 5, CategoryId = 1, Name = "Swiss" },
            new Product { ProductId = 6, CategoryId = 1, Name = "Gruyere" },
            new Product { ProductId = 7, CategoryId = 1, Name = "Colby" },
            new Product { ProductId = 8, CategoryId = 1, Name = "Mozzela" },
            new Product { ProductId = 9, CategoryId = 1, Name = "Ricotta" },
            new Product { ProductId = 10, CategoryId = 1, Name = "Parmesan" },
            new Product { ProductId = 11, CategoryId = 2, Name = "Ham" },
            new Product { ProductId = 12, CategoryId = 2, Name = "Beef" },
            new Product { ProductId = 13, CategoryId = 2, Name = "Chicken" },
            new Product { ProductId = 14, CategoryId = 2, Name = "Turkey" },
            new Product { ProductId = 15, CategoryId = 2, Name = "Prosciutto" },
            new Product { ProductId = 16, CategoryId = 2, Name = "Bacon" },
            new Product { ProductId = 17, CategoryId = 2, Name = "Mutton" },
            new Product { ProductId = 18, CategoryId = 2, Name = "Pastrami" },
            new Product { ProductId = 19, CategoryId = 2, Name = "Hazlet" },
            new Product { ProductId = 20, CategoryId = 2, Name = "Salami" },
            new Product { ProductId = 21, CategoryId = 3, Name = "Salmon" },
            new Product { ProductId = 22, CategoryId = 3, Name = "Tuna" },
            new Product { ProductId = 23, CategoryId = 3, Name = "Mackerel" },
            new Product { ProductId = 24, CategoryId = 4, Name = "Rye" },
            new Product { ProductId = 25, CategoryId = 4, Name = "Wheat" },
            new Product { ProductId = 26, CategoryId = 4, Name = "Brioche" },
            new Product { ProductId = 27, CategoryId = 4, Name = "Naan" },
            new Product { ProductId = 28, CategoryId = 4, Name = "Focaccia" },
            new Product { ProductId = 29, CategoryId = 4, Name = "Malted" },
            new Product { ProductId = 30, CategoryId = 4, Name = "Sourdough" },
            new Product { ProductId = 31, CategoryId = 4, Name = "Corn" },
            new Product { ProductId = 32, CategoryId = 4, Name = "White" },
            new Product { ProductId = 33, CategoryId = 4, Name = "Soda" });
    }
}

이 시점에서 솔루션을 빌드해야 합니다.

폼에 컨트롤 추가

애플리케이션에는 범주 목록과 제품 목록이 표시됩니다. 첫 번째 목록에서 범주를 선택하면 두 번째 목록이 변경되어 해당 범주의 제품이 표시됩니다. 제품 및 범주를 추가, 제거 또는 편집하도록 이러한 목록을 수정할 수 있으며, 저장 단추를 클릭하여 이러한 변경 내용을 SQLite 데이터베이스에 저장할 수 있습니다.

  1. 기본 양식의 이름을 Form1에서 MainForm으로 변경합니다.

    Rename Form1 to MainForm

  2. 제목을 “제품 및 범주”로 변경합니다.

    Title MainForm as

  3. 도구 상자를 사용하여 나란히 정렬된 두 개의 DataGridView 컨트롤을 추가합니다.

    Add DataGridView

  4. 첫 번째DataGridView속성에서 이름dataGridViewCategories로 변경합니다.

  5. 두 번째 DataGridView속성에서 이름dataGridViewProducts로 변경합니다.

  6. 도구 상자를 사용하여 Button 컨트롤을 추가합니다.

  7. 단추 buttonSave 이름을 지정하고 “저장” 텍스트를 지정합니다. 양식은 다음과 같이 표시됩니다.

    Form layout

데이터 바인딩

다음 단계는 모델에서 DataGridView 컨트롤에 ProductCategory 형식을 연결하는 것입니다. 이렇게 하면 EF Core에서 추적하는 엔터티가 컨트롤에 표시된 엔터티와 동기화된 상태로 유지되도록 EF Core에서 로드한 데이터를 컨트롤에 바인딩합니다.

  1. 첫 번째 DataGridView에서 디자이너 작업 문자 모양을 클릭합니다. 컨트롤의 오른쪽 위 모서리에 있는 작은 단추입니다.

    The Designer Action Glyph

  2. 그러면 데이터 원본 선택 드롭다운에 액세스할 수 있는 작업 목록이 열립니다. 데이터 원본을 아직 만들지 않았으므로 맨 아래로 이동하여 새 개체 데이터 원본 추가를 선택합니다.

    Add new Object Data Source

  3. 범주를 선택하여 범주에 대한 개체 데이터 원본을 만들고 확인을 클릭합니다.

    Choose Category data source type

    여기에 데이터 원본 형식이 표시되지 않으면 Product.cs, Category.cs, ProductsContext.cs가 프로젝트에 추가되고 솔루션이 빌드되었는지 확인합니다.

  4. 이제 데이터 원본 선택 드롭다운에 방금 만든 개체 데이터 원본이 포함됩니다. 다른 데이터 원본을 확장한 다음, 프로젝트 데이터 원본을 확장하고 범주를 선택합니다.

    Choose Category data source

    두 번째 DataGridView는 제품에 바인딩됩니다. 그러나 최상위 Product 형식에 바인딩하는 대신 첫 번째 DataGridViewCategory 바인딩에서 Products 탐색에 바인딩됩니다. 즉, 첫 번째 보기에서 범주를 선택하면 이 범주의 제품이 두 번째 보기에서 자동으로 사용됩니다.

  5. 두 번째 DataGridView디자이너 작업 문자 모양을 사용하여 데이터 원본 선택을 선택한 다음 categoryBindingSource를 확장하고 Products를 선택합니다.

    Choose Products data source

표시되는 내용 구성

기본적으로 열은 바인딩된 형식의 모든 속성에 대해 DataGridView에 만들어집니다. 또한 이러한 각 속성의 값은 사용자가 편집할 수 있습니다. 그러나 기본 키 값과 같은 일부 값은 개념적으로 읽기 전용이므로 편집해서는 안 됩니다. 또한 CategoryId 외래 키 속성 및 Category 탐색과 같은 일부 속성은 사용자에게 유용하지 않으므로 숨겨야 합니다.

실제 애플리케이션에서 기본 키 속성을 숨기는 것은 일반적입니다. EF Core가 백그라운드에서 수행하는 작업을 쉽게 볼 수 있도록 여기에 표시됩니다.

  1. 첫 번째 DataGridView를 마우스 오른쪽 버튼으로 클릭하고 열 편집...을 선택합니다.

    Edit DataGridView columns

  2. 기본 키를 나타내는 CategoryId 열을 읽기 전용으로 만들고 확인을 클릭합니다.

    Make CategoryId column read-only

  3. 두 번째 DataGridView를 마우스 오른쪽 버튼으로 클릭하고 열 편집...을 선택합니다. ProductId 열을 읽기 전용으로 만들고 CategoryIdCategory 열을 제거한 다음 확인을 클릭합니다.

    Make ProductId column read-only and remove CategoryId and Category columns

EF Core에 연결

이제 애플리케이션은 데이터 바인딩된 컨트롤에 EF Core를 연결하기 위해 소량의 코드가 필요합니다.

  1. 파일을 마우스 오른쪽 버튼으로 클릭하고 코드 보기를 선택하여 MainForm 코드를 엽니다.

    View Code

  2. 세션에 대한 DbContext를 보유할 프라이빗 필드를 추가하고 OnLoadOnClosing 메서드에 대한 재정의를 추가합니다. 코드는 다음과 유사합니다.

using Microsoft.EntityFrameworkCore;
using System.ComponentModel;

namespace GetStartedWinForms
{
    public partial class MainForm : Form
    {
        private ProductsContext? dbContext;

        public MainForm()
        {
            InitializeComponent();
        }

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            this.dbContext = new ProductsContext();

            // Uncomment the line below to start fresh with a new database.
            // this.dbContext.Database.EnsureDeleted();
            this.dbContext.Database.EnsureCreated();

            this.dbContext.Categories.Load();

            this.categoryBindingSource.DataSource = dbContext.Categories.Local.ToBindingList();
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            base.OnClosing(e);

            this.dbContext?.Dispose();
            this.dbContext = null;
        }
    }
}

OnLoad 메서드는 양식이 로드될 때 호출됩니다. 현재

  • 애플리케이션에 표시되는 제품 및 범주의 변경 내용을 로드하고 추적하는 데 사용할 ProductsContext의 인스턴스가 만들어집니다.
  • SQLite 데이터베이스가 아직 존재하지 않는 경우 EnsureCreatedDbContext에서 호출되어 SQLite 데이터베이스를 만듭니다. 이 방법은 애플리케이션을 프로토타입으로 만들거나 테스트할 때 데이터베이스를 빠르게 만드는 방법입니다. 그러나 모델이 변경되면 데이터베이스를 다시 만들 수 있도록 데이터베이스를 삭제해야 합니다. (애플리케이션이 실행될 때 데이터베이스를 쉽게 삭제하고 다시 만들 수 있도록 EnsureDeleted 줄의 주석을 달지 않을 수 있습니다.) 대신 EF Core 마이그레이션을 사용하여 데이터를 잃지 않고 데이터베이스 스키마를 수정하고 업데이트할 수 있습니다.
  • EnsureCreated는 또한 ProductsContext.OnModelCreating 메서드에 정의된 데이터로 새 데이터베이스를 채웁니다.
  • Load 확장 메서드는 데이터베이스의 모든 범주를 DbContext로 로드하는 데 사용됩니다. 이러한 엔티티는 이제 DbContext에 의해 추적되며 사용자가 범주를 편집할 때 변경된 내용을 검색합니다.
  • categoryBindingSource.DataSource 속성은 DbContext가 추적 중인 범주로 초기화됩니다. 이 작업은 CategoriesDbSet 속성에서 Local.ToBindingList()를 호출하여 수행됩니다. Local은 추적된 범주의 로컬 보기에 대한 액세스를 제공하며, 이벤트는 로컬 데이터가 표시된 데이터와 동기화된 상태로 유지되도록 연결되고 그 반대의 경우도 마찬가지입니다. ToBindingList()는 이 데이터를 IBindingList로 노출하며 이는 Windows Forms 데이터 바인딩으로 이해됩니다.

OnClosing 메서드는 양식을 닫을 때 호출됩니다. 이때 DbContext는 삭제되어 모든 데이터베이스 리소스가 해제되고 dbContext 필드는 다시 사용할 수 없도록 null로 설정됩니다.

제품 보기 채우기

이 시점에서 애플리케이션이 시작되면 다음과 같이 표시됩니다.

Fist run of the application

범주가 데이터베이스에서 로드되었지만 제품 테이블은 비어 있습니다. 또한 저장 단추가 작동하지 않습니다.

제품 테이블을 채웁니다. EF Core는 선택한 범주에 대한 데이터베이스에서 제품을 로드해야 합니다. 이 작업을 수행하려면 다음을 수행합니다.

  1. 기본 양식의 디자이너에서 범주용 DataGridView를 선택합니다.

  2. DataGridView속성에서 이벤트(번개 버튼)를 선택하고 SelectionChanged 이벤트를 두 번 클릭합니다.

    Add the SelectionChanged event

    범주 선택이 변경될 때마다 이벤트가 발생하도록 기본 양식 코드에 스텁이 만들어집니다.

  3. 이벤트에 대한 코드를 입력합니다.

private void dataGridViewCategories_SelectionChanged(object sender, EventArgs e)
{
    if (this.dbContext != null)
    {
        var category = (Category)this.dataGridViewCategories.CurrentRow.DataBoundItem;

        if (category != null)
        {
            this.dbContext.Entry(category).Collection(e => e.Products).Load();
        }
    }
}

이 코드에서 활성(null이 아닌) DbContext 세션이 있는 경우 DataViewGrid의 현재 선택된 행에 바인딩된 Category 인스턴스를 가져옵니다. (보기의 마지막 행이 선택된 경우 새 범주를 만드는 데 사용되는 null일 수 있습니다.) 선택한 범주가 있는 경우 DbContext는 이 범주와 연결된 제품을 로드하도록 지시됩니다. 이렇게 하는 방법은 다음과 같습니다.

  • Category 인스턴스(dbContext.Entry(category))에 대한 EntityEntry 불러오기
  • Category(.Collection(e => e.Products))의 Products 컬렉션 탐색에서 작동할 것임을 EF Core에 알리기
  • 마지막으로 데이터베이스에서 제품 컬렉션을 로드할 것임을 EF Core에 알려 줍니다(.Load();).

Load가 호출될 때 EF Core는 아직 로드되지 않은 경우에만 데이터베이스에 액세스하여 제품을 로드합니다.

이제 애플리케이션이 다시 실행되면 범주를 선택할 때마다 적절한 제품을 로드해야 합니다.

Products are loaded

변경 내용 저장

마지막으로 제품 및 범주에 대한 변경 내용이 데이터베이스에 저장되도록 저장 단추를 EF Core에 연결할 수 있습니다.

  1. 기본 양식의 디자이너에서 저장 단추를 선택합니다.

  2. Button속성에서 이벤트(번개 버튼)를 선택하고 클릭 이벤트를 두 번 클릭합니다.

    Add the Click event for Save

  3. 이벤트에 대한 코드를 입력합니다.

private void buttonSave_Click(object sender, EventArgs e)
{
    this.dbContext!.SaveChanges();

    this.dataGridViewCategories.Refresh();
    this.dataGridViewProducts.Refresh();
}

이 코드는 DbContext에서 SaveChanges를 호출하며 이는 SQLite 데이터베이스에 대한 모든 변경 사항을 저장합니다. 변경이 수행되지 않은 경우 이는 no-op이며 데이터베이스 호출이 수행되지 않습니다. 저장 후 DataGridView 컨트롤이 새로 고쳐집니다. EF Core가 데이터베이스에서 새 제품 및 범주에 대해 생성된 기본 키 값을 읽기 때문입니다. 호출 Refresh하면 이러한 생성된 값으로 디스플레이가 업데이트됩니다.

최종 애플리케이션

기본 양식의 전체 코드는 다음과 같습니다.

using Microsoft.EntityFrameworkCore;
using System.ComponentModel;

namespace GetStartedWinForms
{
    public partial class MainForm : Form
    {
        private ProductsContext? dbContext;

        public MainForm()
        {
            InitializeComponent();
        }

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            this.dbContext = new ProductsContext();

            // Uncomment the line below to start fresh with a new database.
            // this.dbContext.Database.EnsureDeleted();
            this.dbContext.Database.EnsureCreated();

            this.dbContext.Categories.Load();

            this.categoryBindingSource.DataSource = dbContext.Categories.Local.ToBindingList();
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            base.OnClosing(e);

            this.dbContext?.Dispose();
            this.dbContext = null;
        }

        private void dataGridViewCategories_SelectionChanged(object sender, EventArgs e)
        {
            if (this.dbContext != null)
            {
                var category = (Category)this.dataGridViewCategories.CurrentRow.DataBoundItem;

                if (category != null)
                {
                    this.dbContext.Entry(category).Collection(e => e.Products).Load();
                }
            }
        }

        private void buttonSave_Click(object sender, EventArgs e)
        {
            this.dbContext!.SaveChanges();

            this.dataGridViewCategories.Refresh();
            this.dataGridViewProducts.Refresh();
        }
    }
}

이제 애플리케이션을 실행할 수 있으며 제품 및 범주를 추가, 삭제 및 편집할 수 있습니다. 애플리케이션을 닫기 전에 저장 단추를 클릭하면 변경 내용이 데이터베이스에 저장되고 애플리케이션을 재시작할 때 다시 로드됩니다. 저장을 클릭하지 않으면 애플리케이션을 재시작할 때 변경 내용이 손실됩니다.

컨트롤 맨 아래에 있는 빈 행을 사용하여 새 범주 또는 제품을 DataViewControl에 추가할 수 있습니다. 행을 선택하고 Del 키를 누르면 행을 삭제할 수 있습니다.

저장하기 전에

The running application before clicking Save

저장 후

The running application after clicking Save

저장을 클릭하면 추가된 범주 및 제품에 대한 기본 키 값이 채워집니다.

자세한 정보