WinForms를 사용한 데이터 바인딩

이 단계별 연습에서는 POCO 유형을 “마스터-세부” 양식의 WinForms(Window Forms) 컨트롤에 바인딩하는 방법을 보여 줍니다. 애플리케이션에서는 Entity Framework를 사용하여 데이터베이스의 데이터로 개체를 채우고, 변경 내용을 추적하며, 데이터를 데이터베이스에 유지합니다.

모델에서는 일대다 관계에 참여하는 범주(principal\main)와 제품(dependent\detail)의 두 가지 형식을 정의합니다. 그런 다음 Visual Studio 도구를 사용하여 모델에 정의된 형식을 WinForms 컨트롤에 바인딩합니다. WinForms 데이터 바인딩 프레임워크를 사용하면 관련 개체 간을 탐색할 수 있습니다. 즉, 마스터 보기에서 행을 선택하면 세부 정보 보기가 해당 자식 데이터로 업데이트됩니다.

이 연습의 스크린샷과 코드 목록은 Visual Studio 2013에서 가져온 것이지만 Visual Studio 2012 또는 Visual Studio 2010을 사용하여 이 연습을 완료할 수 있습니다.

필수 구성 요소

이 연습을 완료하려면 Visual Studio 2013, Visual Studio 2012 또는 Visual Studio 2010이 설치되어 있어야 합니다.

Visual Studio 2010을 사용하는 경우 NuGet도 설치해야 합니다. 자세한 내용은 NuGet 설치를 참조하세요.

애플리케이션 만들기

  • Visual Studio를 엽니다.
  • 파일 -> 새로 만들기 -> 프로젝트….
  • 왼쪽 창에서 Windows를 선택하고 오른쪽 창에서 Windows FormsApplication을 선택합니다.
  • 이름으로 WinFormswithEFSample을 입력합니다.
  • 확인을 선택합니다.

Entity Framework NuGet 패키지 설치

  • 솔루션 탐색기에서 WinFormswithEFSample 프로젝트를 마우스 오른쪽 단추로 클릭합니다.
  • NuGet 패키지 관리…를 선택합니다.
  • NuGet 패키지 관리 대화 상자에서 온라인 탭과 EntityFramework 패키지를 차례로 선택합니다.
  • 설치를 클릭합니다.

    참고 항목

    EntityFramework 어셈블리뿐만 아니라 System.ComponentModel.DataAnnotations에 대한 참조도 추가됩니다. 프로젝트에 System.Data.Entity에 대한 참조가 있는 경우 EntityFramework 패키지가 설치될 때 제거됩니다. System.Data.Entity 어셈블리는 Entity Framework 6 애플리케이션에서 더 이상 사용되지 않습니다.

컬렉션에서 IListSource 구현

Windows Forms 사용할 때 정렬을 통해 양방향 데이터 바인딩을 사용하도록 설정하려면 컬렉션 속성에서 IListSource 인터페이스를 구현해야 합니다. 이를 위해 ObservableCollection을 확장하여 IListSource 기능을 추가하겠습니다.

  • 프로젝트에 ObservableListSource 클래스를 추가합니다.
    • 프로젝트 이름을 마우스 오른쪽 단추로 클릭합니다.
    • 추가 -> 새 항목을 선택합니다.
    • 클래스를 선택하고 클래스 이름에 ObservableListSource를 입력합니다.
  • 기본값으로 생성된 코드를 다음 코드로 바꿉니다.

이 클래스를 사용하면 정렬뿐만 아니라 양방향 데이터 바인딩도 사용할 수 있습니다. 클래스는 ObservableCollection<T>에서 파생되고 IListSource의 명시적 구현을 추가합니다. IListSource의 GetList() 메서드는 ObservableCollection과 동기화된 상태로 유지되는 IBindingList 구현을 반환하도록 구현됩니다. ToBindingList에서 생성된 IBindingList 구현은 정렬을 지원합니다. ToBindingList 확장 메서드는 EntityFramework 어셈블리에 정의되어 있습니다.

    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Diagnostics.CodeAnalysis;
    using System.Data.Entity;

    namespace WinFormswithEFSample
    {
        public class ObservableListSource<T> : ObservableCollection<T>, IListSource
            where T : class
        {
            private IBindingList _bindingList;

            bool IListSource.ContainsListCollection { get { return false; } }

            IList IListSource.GetList()
            {
                return _bindingList ?? (_bindingList = this.ToBindingList());
            }
        }
    }

모델 정의

이 연습에서는 Code First 또는 EF 디자이너를 사용하여 모델을 구현하도록 선택할 수 있습니다. 다음 두 섹션 중 하나를 완료합니다.

옵션 1: Code First를 사용해 모델 정의

이 섹션에서는 Code First를 사용하여 모델 및 관련 데이터베이스를 만드는 방법을 보여 줍니다. Database First를 사용하여 EF 디자이너로 데이터베이스에서 모델을 리버스 엔지니어링하려는 경우 다음 섹션(옵션 2: Database First를 사용하여 모델 정의)으로 건너뜁니다.

Code First 개발을 사용하는 경우 일반적으로 개념적(도메인) 모델을 정의하는 .NET Framework 클래스를 작성하는 것부터 시작합니다.

  • 프로젝트에 새 Product 클래스를 추가합니다.
  • 기본값으로 생성된 코드를 다음 코드로 바꿉니다.
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

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

            public int CategoryId { get; set; }
            public virtual Category Category { get; set; }
        }
    }
  • 프로젝트에 Category 클래스를 추가합니다.
  • 기본값으로 생성된 코드를 다음 코드로 바꿉니다.
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    namespace WinFormswithEFSample
    {
        public class Category
        {
            private readonly ObservableListSource<Product> _products =
                    new ObservableListSource<Product>();

            public int CategoryId { get; set; }
            public string Name { get; set; }
            public virtual ObservableListSource<Product> Products { get { return _products; } }
        }
    }

엔터티를 정의하는 것 외에도 DbContext에서 파생되고 DbSet<TEntity> 속성을 노출하는 클래스를 정의해야 합니다. DbSet 속성은 모델에 포함하려는 형식을 컨텍스트에 알립니다. DbContextDbSet 형식은 EntityFramework 어셈블리에 정의되어 있습니다.

DbContext 파생 형식의 인스턴스는 런타임 중에 엔터티 개체를 관리합니다. 여기에는 데이터베이스의 데이터로 개체 채우기, 변경 내용 추적 및 데이터베이스에 데이터 유지가 포함됩니다.

  • 새로운 ProductContext 클래스를 프로젝트에 추가합니다.
  • 기본값으로 생성된 코드를 다음 코드로 바꿉니다.
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;
    using System.Text;

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

프로젝트를 컴파일합니다.

옵션 2: Database First를 사용해 모델 정의

이 섹션에서는 Database First를 사용하여 EF 디자이너로 데이터베이스에서 모델을 리버스 엔지니어링하는 방법을 보여 줍니다. 이전 섹션(옵션 1: Code First를 사용해 모델 정의)을 완료한 경우 이 섹션을 건너뛰고 지연 로드 섹션으로 바로 이동합니다.

기존 데이터베이스 만들기

일반적으로 기존 데이터베이스를 대상으로 하는 경우 이미 만들어져 있겠지만 이 연습에서는 액세스할 데이터베이스를 만들어야 합니다.

Visual Studio와 함께 설치되는 데이터베이스 서버는 설치한 Visual Studio 버전에 따라 다릅니다.

  • Visual Studio 2010을 사용하는 경우 SQL Express 데이터베이스를 만듭니다.
  • Visual Studio 2012를 사용하는 경우 LocalDB 데이터베이스를 만듭니다.

그럼 계속해서 데이터베이스를 생성해 보겠습니다.

  • 보기 -> 서버 탐색기

  • 데이터 연결 -> 연결 추가...를 마우스 오른쪽 단추로 클릭합니다.

  • 서버 탐색기에서 데이터베이스에 연결하지 않은 경우 Microsoft SQL Server를 데이터 원본으로 선택해야 합니다.

    Change Data Source

  • 설치한 항목에 따라 LocalDB 또는 SQL Express에 연결하고 데이터베이스 이름으로 Products를 입력합니다.

    Add Connection LocalDB

    Add Connection Express

  • 확인을 선택하면 새 데이터베이스를 만들 것인지 묻는 메시지가 표시됩니다. 그러면 를 선택합니다.

    Create Database

  • 이제 새 데이터베이스가 서버 탐색기에 표시됩니다. 마우스 오른쪽 단추로 클릭하고 새 쿼리를 선택합니다.

  • 다음 SQL을 새 쿼리에 복사한 후 쿼리를 마우스 오른쪽 단추로 클릭하고 실행을 선택합니다.

    CREATE TABLE [dbo].[Categories] (
        [CategoryId] [int] NOT NULL IDENTITY,
        [Name] [nvarchar](max),
        CONSTRAINT [PK_dbo.Categories] PRIMARY KEY ([CategoryId])
    )

    CREATE TABLE [dbo].[Products] (
        [ProductId] [int] NOT NULL IDENTITY,
        [Name] [nvarchar](max),
        [CategoryId] [int] NOT NULL,
        CONSTRAINT [PK_dbo.Products] PRIMARY KEY ([ProductId])
    )

    CREATE INDEX [IX_CategoryId] ON [dbo].[Products]([CategoryId])

    ALTER TABLE [dbo].[Products] ADD CONSTRAINT [FK_dbo.Products_dbo.Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [dbo].[Categories] ([CategoryId]) ON DELETE CASCADE

리버스 엔지니어링 모델

Visual Studio의 일부로 포함되는 Entity Framework Designer를 사용하여 모델을 만들겠습니다.

  • 프로젝트 -> 새 항목 추가…

  • 왼쪽 창에서 데이터를 선택한 다음 ADO.NET 엔터티 데이터 모델을 선택합니다.

  • 이름으로 ProductModel을 입력하고 확인을 클릭합니다.

  • 그러면 엔터티 데이터 모델 마법사가 시작됩니다.

  • 데이터베이스에서 생성을 선택하고 다음을 클릭합니다.

    Choose Model Contents

  • 첫 번째 섹션에서 만든 데이터베이스에 대한 연결을 선택하고 연결 문자열의 이름으로 ProductContext를 입력한 후 다음을 클릭합니다.

    Choose Your Connection

  • '테이블' 옆의 확인란을 클릭하여 모든 테이블을 가져오고 '마침'을 클릭합니다.

    Choose Your Objects

리버스 엔지니어링 프로세스가 완료되면 새 모델이 프로젝트에 추가되고 Entity Framework Designer에서 볼 수 있도록 열립니다. 데이터베이스에 대한 연결 세부 정보가 포함된 App.config 파일도 프로젝트에 추가되었습니다.

Visual Studio 2010의 추가 단계

Visual Studio 2010에서 작업하는 경우 EF6 코드 생성을 사용할 수 있도록 EF 디자이너를 업데이트해야 합니다.

  • EF 디자이너에서 모델의 빈 지점을 마우스 오른쪽 단추로 클릭하고 코드 생성 항목 추가...를 선택합니다.
  • 왼쪽 메뉴에서 온라인 템플릿을 선택하고 DbContext를 검색합니다.
  • C#용 EF 6.x DbContext 생성기를 선택하고 이름으로 ProductsModel을 입력한 후 [추가]를 클릭합니다.

데이터 바인딩에 대한 코드 생성 업데이트

EF는 T4 템플릿을 사용하여 모델에서 코드를 생성합니다. Visual Studio와 함께 제공되거나 Visual Studio 갤러리에서 다운로드된 템플릿은 범용으로 사용됩니다. 즉, 이러한 템플릿에서 생성된 엔터티에는 간단한 ICollection<T> 속성이 있습니다. 그러나 데이터를 바인딩할 때 IListSource를 구현하는 컬렉션 속성을 포함하는 것이 좋습니다. 위에서 ObservableListSource 클래스를 만든 이유가 바로 이 때문입니다. 이제 이 클래스를 사용하도록 템플릿을 수정하겠습니다.

  • 솔루션 탐색기를 열고 ProductModel.edmx 파일을 찾습니다.

  • ProductModel.edmx 파일 아래에 중첩될 ProductModel.tt 파일을 찾습니다.

    Product Model Template

  • ProductModel.tt 파일을 두 번 클릭하여 Visual Studio 편집기에서 엽니다.

  • ICollection”의 두 항목을 찾아서 “ObservableListSource”로 바꿉니다. 이러한 항목은 대략 296번째 줄과 484번째 줄에 있습니다.

  • HashSet”의 첫 번째 항목을 찾아서 “ObservableListSource”로 바꿉니다. 이 항목은 대략 50번째 줄에 있습니다. 코드 뒷부분에 있는 두 번째 HashSet 항목을 바꾸지 마세요.

  • ProductModel.tt 파일을 저장합니다. 이렇게 하면 엔터티 코드가 다시 생성됩니다. 코드가 자동으로 다시 생성되지 않으면 ProductModel.tt를 마우스 오른쪽 단추로 클릭하고 “사용자 지정 도구 실행”을 선택합니다.

이제 Category.cs 파일(ProductModel.tt 아래에 중첩됨)을 열면 Products 컬렉션에 ObservableListSource<Product> 형식이 있는 것을 알 수 있습니다.

프로젝트를 컴파일합니다.

지연 로드

카테고리 클래스의 제품 속성과 제품 클래스의 카테고리 속성은 탐색 속성입니다. Entity Framework에서 탐색 속성은 두 엔터티 형식 간의 관계를 탐색하는 방법을 제공합니다.

EF는 탐색 속성에 처음 액세스할 때 데이터베이스에서 관련 엔터티를 자동으로 로드하는 옵션을 제공합니다. 이 유형의 로드(지연 로드라고 함)에서는 각 탐색 속성에 처음 액세스할 때 내용이 컨텍스트에 아직 없는 경우 데이터베이스에 대해 별도의 쿼리가 실행됩니다.

POCO 엔터티 형식을 사용할 때 EF는 런타임 중에 파생된 프록시 형식의 인스턴스를 만든 다음 클래스의 가상 속성을 재정의해 로딩 후크를 추가함으로써 지연 로드를 달성합니다. 관련 개체의 지연 로드를 얻으려면 탐색 속성 getter를 공개가상(Visual Basic에서 Overridable)으로 선언해야 하며, 클래스를 봉인(Visual Basic에서 NotOverridable)하면 안 됩니다. Database First를 사용하는 경우 탐색 속성이 가상으로 자동 설정되어 지연 로드를 사용할 수 있습니다. Code First 섹션에서는 같은 이유로 탐색 속성을 가상으로 설정하기로 선택했습니다.

개체를 컨트롤에 바인딩

모델에 정의된 클래스를 이 WinForms 애플리케이션의 데이터 원본으로 추가합니다.

  • 주 메뉴에서 프로젝트 -> 새 데이터 원본 추가…를 선택합니다(Visual Studio 2010에서는 데이터 -> 새 데이터 원본 추가…를 선택해야 함).

  • 데이터 원본 형식 선택 창에서 개체를 선택하고 다음을 클릭합니다.

  • 데이터 개체 선택 대화 상자에서 WinFormswithEFSample을 두 번 펼치고 범주를 선택합니다. 그러면 범주 데이터 원본의 제품 속성을 통해 가져올 수 있으므로 제품 데이터 원본을 선택할 필요가 없습니다.

    Data Source

  • 마침을 클릭합니다. 데이터 원본 창이 표시되지 않으면 보기 -> 기타 Windows-> 데이터 원본을 선택합니다.

  • 데이터 원본 창이 자동으로 숨겨지지 않도록 고정 아이콘을 누릅니다. 창이 이미 표시된 경우 새로 고침 단추를 눌러야 할 수 있습니다.

    Data Source 2

  • 솔루션 탐색기에서 Form1.cs 파일을 두 번 클릭하여 디자이너의 기본 양식을 엽니다.

  • 범주 데이터 원본을 선택하고 양식에서 끌어옵니다. 기본적으로 새 DataGridView(categoryDataGridView) 및 탐색 도구 모음 컨트롤은 디자이너에 추가되어 있습니다. 이러한 컨트롤은 만들어진 BindingSource(categoryBindingSource) 및 바인딩 탐색기(categoryBindingNavigator) 구성 요소에도 바인딩됩니다.

  • categoryDataGridView의 열을 편집합니다. CategoryId 열을 읽기 전용으로 설정하려고 합니다. CategoryId 속성의 값은 데이터를 저장하면 데이터베이스에서 생성됩니다.

    • DataGridView 컨트롤을 마우스 오른쪽 단추로 클릭하고 [열 편집...]을 선택합니다.
    • [CategoryId] 열을 선택하고 [ReadOnly]를 [True]로 설정합니다.
    • [확인]을 누릅니다.
  • [범주] 데이터 원본 아래에서 [제품]을 선택하고 양식에 끌어옵니다. productDataGridView 및 productBindingSource가 양식에 추가됩니다.

  • productDataGridView의 열을 편집합니다. CategoryId 및 Category 열을 숨기고 ProductId를 읽기 전용으로 설정하려고 합니다. ProductId 속성의 값은 데이터를 저장하면 데이터베이스에서 생성됩니다.

    • DataGridView 컨트롤을 마우스 오른쪽 단추로 클릭하고 열 편집...을 선택합니다.
    • ProductId 열을 선택하고 ReadOnlyTrue로 설정합니다.
    • CategoryId 열을 선택하고 제거 단추를 누릅니다. 범주 열에서 동일한 작업을 수행합니다.
    • 확인을 누릅니다.

    지금까지 DataGridView 컨트롤을 디자이너의 BindingSource 구성 요소와 연결했습니다. 다음 섹션에서는 코드 숨김에 코드를 추가하여 categoryBindingSource.DataSource를 현재 DbContext에서 추적하는 엔터티 컬렉션으로 설정합니다. 범주 아래에서 제품을 끌어서 놓으면 WinForms가 productsBindingSource.DataSource 속성을 categoryBindingSource로 설정하고 productsBindingSource.DataMember 속성을 Products로 설정했습니다. 이 바인딩으로 인해 현재 선택한 범주에 속해 있는 제품만 productDataGridView에 표시됩니다.

  • 마우스 오른쪽 단추를 클릭하고 사용을 선택하여 탐색 도구 모음에서 저장 단추를 사용하도록 설정합니다.

    Form 1 Designer

  • 단추를 두 번 클릭하여 저장 단추에 대한 이벤트 처리기를 추가합니다. 그러면 이벤트 처리기가 추가되고 양식 코드 숨김으로 이동됩니다. categoryBindingNavigatorSaveItem_Click 이벤트 처리기의 코드는 다음 섹션에서 추가합니다.

데이터 상호 작용을 처리하는 코드 추가

이제 ProductContext를 사용하여 데이터에 액세스하는 코드를 추가합니다. 아래와 같이 주 양식 창의 코드를 업데이트합니다.

이 코드는 ProductContext의 장기 실행 인스턴스를 선언합니다. ProductContext 개체는 데이터를 쿼리하고 데이터베이스에 저장하는 데 사용됩니다. 그러면 ProductContext 인스턴스의 Dispose() 메서드가 재정의된 OnClosing 메서드에서 호출됩니다. 코드 주석은 코드가 수행하는 작업에 대한 세부 정보를 제공합니다.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Forms;
    using System.Data.Entity;

    namespace WinFormswithEFSample
    {
        public partial class Form1 : Form
        {
            ProductContext _context;
            public Form1()
            {
                InitializeComponent();
            }

            protected override void OnLoad(EventArgs e)
            {
                base.OnLoad(e);
                _context = new ProductContext();

                // Call the Load method to get the data for the given DbSet
                // from the database.
                // The data is materialized as entities. The entities are managed by
                // the DbContext instance.
                _context.Categories.Load();

                // Bind the categoryBindingSource.DataSource to
                // all the Unchanged, Modified and Added Category objects that
                // are currently tracked by the DbContext.
                // Note that we need to call ToBindingList() on the
                // ObservableCollection<TEntity> returned by
                // the DbSet.Local property to get the BindingList<T>
                // in order to facilitate two-way binding in WinForms.
                this.categoryBindingSource.DataSource =
                    _context.Categories.Local.ToBindingList();
            }

            private void categoryBindingNavigatorSaveItem_Click(object sender, EventArgs e)
            {
                this.Validate();

                // Currently, the Entity Framework doesn’t mark the entities
                // that are removed from a navigation property (in our example the Products)
                // as deleted in the context.
                // The following code uses LINQ to Objects against the Local collection
                // to find all products and marks any that do not have
                // a Category reference as deleted.
                // The ToList call is required because otherwise
                // the collection will be modified
                // by the Remove call while it is being enumerated.
                // In most other situations you can do LINQ to Objects directly
                // against the Local property without using ToList first.
                foreach (var product in _context.Products.Local.ToList())
                {
                    if (product.Category == null)
                    {
                        _context.Products.Remove(product);
                    }
                }

                // Save the changes to the database.
                this._context.SaveChanges();

                // Refresh the controls to show the values         
                // that were generated by the database.
                this.categoryDataGridView.Refresh();
                this.productsDataGridView.Refresh();
            }

            protected override void OnClosing(CancelEventArgs e)
            {
                base.OnClosing(e);
                this._context.Dispose();
            }
        }
    }

Windows Forms 애플리케이션 테스트

  • 애플리케이션을 컴파일하고 실행하면 기능을 테스트할 수 있습니다.

    Form 1 Before Save

  • 저장하면 저장소에서 생성된 키가 화면에 표시됩니다.

    Form 1 After Save

  • Code First를 사용한 경우 WinFormswithEFSample.ProductContext 데이터베이스가 만들어지는 것도 볼 수 있습니다.

    Server Object Explorer