WinForms를 사용하여 데이터 바인딩

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

모델은 일대다 관계에 참여하는 두 가지 형식인 범주(principal\master) 및 Product(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를 엽니다.
  • 파일 -> 새로 만들기 -> Project....
  • 왼쪽 창에서 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 Designer를 사용하여 모델을 구현하도록 선택할 수 있습니다. 다음 두 섹션 중 하나를 완료합니다.

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

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

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를 데이터 원본으로 선택하기 전에 서버 탐색기에서 데이터베이스에 연결하지 않은 경우

    데이터 원본 변경

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

    LocalDB 연결 추가

    Connection Express 추가

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

    데이터베이스 만들기

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

  • 다음 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을 입력하고 확인을 클릭합니다.

  • 엔터티 데이터 모델 마법사가 실행됩니다.

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

    모델 콘텐츠 선택

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

    연결 선택

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

    개체 선택

리버스 엔지니어링 프로세스가 완료되면 새 모델이 프로젝트에 추가되고 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 파일을 찾습니다.

    제품 모델 템플릿

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

  • "ICollection"의 두 항목을 찾아서 "ObservableListSource"로 바꿉니다. 이들은 대략 296 및 484 줄에 있습니다.

  • "HashSet"의 첫 번째 항목을 찾아서 "ObservableListSource"로 바꿉니다. 이 발생은 대략 50번째 줄에 위치해 있습니다. 코드의 뒷부분에 있는 두 번째 HashSet 항목을 대체하지 마세요.

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

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

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

지연 로드

Product 클래스의 Category 클래스 및 Category 속성에 있는 Products 속성 은 탐색 속성입니다. Entity Framework에서 탐색 속성은 두 엔터티 형식 간의 관계를 탐색하는 방법을 제공합니다.

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

POCO 엔터티 형식을 사용하는 경우 EF는 런타임 중에 파생된 프록시 형식의 인스턴스를 만든 다음 클래스의 가상 속성을 재정의하여 로드 후크를 추가하여 지연 로드를 수행합니다. 관련 개체의 지연 로드를 사용하려면 탐색 속성 getter를 공용가상으로 선언해야 하며, Visual Basic에서는 Overridable로 선언해야 합니다. 또한 클래스가 봉인되지 않아야 합니다(Visual Basic에서는 NotOverridable). 데이터베이스 첫 번째 탐색 속성을 사용하는 경우 지연 로드를 사용하도록 자동으로 가상으로 설정됩니다. Code First 섹션에서는 동일한 이유로 탐색 속성을 가상으로 만들기로 결정했습니다.

컨트롤에 개체 바인딩

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

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

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

  • 데이터 개체 선택 대화 상자에서 WinFormswithEFSample 을 두 번 펼치고 범주 를 선택하면 제품 데이터 원본을 선택할 필요가 없습니다. 범주 데이터 원본의 Product 속성을 통해 가져올 수 있기 때문입니다.

    데이터 원본

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

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

    데이터 원본 2

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

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

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

    • DataGridView 컨트롤을 마우스 오른쪽 버튼으로 클릭하고 '열 편집'을 선택하십시오…
    • CategoryId 열을 선택하고 ReadOnly를 True로 설정
    • OK 키를 누릅니다.
  • 범주 데이터 원본 아래에서 제품을 선택하고 폼에 끌어옵니다. productDataGridView 및 productBindingSource가 양식에 추가됩니다.

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

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

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

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

    양식 1 디자이너

  • 단추를 두 번 클릭하여 저장 단추에 대한 이벤트 처리기를 추가합니다. 그러면 이벤트 처리기가 추가되고 양식의 비하인드 코드로 이동합니다. 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 애플리케이션 테스트

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

    저장 전 양식 1

  • 저장소를 저장한 후 생성된 키가 화면에 표시됩니다.

    저장 후 양식 1

  • Code First를 사용한 경우 WinFormswithEFSample.ProductContext 데이터베이스도 만들어집니다.

    서버 개체 탐색기