WPF를 사용한 데이터 바인딩
Important
이 문서는 .NET Framework WPF에 한해 유효합니다.
이 문서에서는 .NET Framework WPF에 대한 데이터 바인딩을 설명합니다. 새 .NET Core 프로젝트의 경우 Entity Framework 6 대신 EF Core를 사용하는 것이 좋습니다. EF Core 데이터 바인딩에 대한 설명서는 WPF 시작에 있습니다.
이 단계별 연습은 POCO 유형을 "마스터-세부 정보" 양식의 WPF 컨트롤에 바인딩하는 방법을 보여 줍니다. 이 애플리케이션은 Entity Framework API를 사용하여 데이터베이스의 데이터로 개체를 채우고, 변경 내용을 추적하고, 데이터를 데이터베이스에 유지합니다.
이 모델은 일대다 관계에 참여하는 범주(principal\main)와 제품(dependent\detail)의 두 가지 형식을 정의합니다. 그런 다음 Visual Studio 도구를 사용하여 모델에 정의된 형식을 WPF 컨트롤에 바인딩합니다. WPF 데이터 바인딩 프레임워크를 사용하면 관련 개체 간 탐색이 가능합니다. 즉, 마스터 보기에서 행을 선택하면 세부 정보 보기가 해당 자식 데이터로 업데이트됩니다.
이 연습의 스크린샷과 코드 목록은 Visual Studio 2013에서 가져온 것이지만 Visual Studio 2012 또는 Visual Studio 2010을 사용하여 이 연습을 완료할 수 있습니다.
'개체' 옵션을 사용하여 WPF 데이터 원본 만들기
이전 버전의 Entity Framework에서 EF 디자이너를 사용해 만든 모델을 기반으로 새 데이터 원본을 만들 때 데이터베이스 옵션을 사용하는 것이 좋습니다. 이는 디자이너가 EntityObject에서 파생된 ObjectContext 및 엔터티 클래스에서 파생된 컨텍스트를 생성하기 때문입니다. 데이터베이스 옵션을 사용하면 이 API 표면과 상호 작용하기 위한 최상의 코드를 작성하는 데 도움이 됩니다.
Visual Studio 2012 및 Visual Studio 2013용 EF 디자이너는 간단한 POCO 엔터티 클래스와 함께 DbContext에서 파생되는 컨텍스트를 생성합니다. Visual Studio 2010에서는 이 연습의 뒷부분에 설명된 대로 DbContext를 사용하는 코드 생성 템플릿으로 바꾸는 것이 좋습니다.
DbContext API 표면을 사용하는 경우 이 연습과 같이 새 데이터 원본을 만들 때 개체 옵션을 사용해야 합니다.
필요한 경우 EF 디자이너를 사용하여 만든 모델에 대해 ObjectContext 기반 코드 생성으로 되돌릴 수 있습니다.
필수 구성 요소
이 연습을 완료하려면 Visual Studio 2013, Visual Studio 2012 또는 Visual Studio 2010이 설치되어 있어야 합니다.
Visual Studio 2010을 사용하는 경우 NuGet도 설치해야 합니다. 자세한 내용은 NuGet 설치를 참조하세요.
애플리케이션 만들기
- Visual Studio를 엽니다.
- 파일 -> 새로 만들기 -> 프로젝트….
- 왼쪽 창에서 Windows를, 오른쪽 창에서 WPFApplication을 선택합니다.
- 이름으로 WPFwithEFSample을 입력합니다.
- 확인을 선택합니다.
Entity Framework NuGet 패키지 설치
- 솔루션 탐색기에서 WinFormswithEFSample 프로젝트를 마우스 오른쪽 단추로 클릭합니다.
- NuGet 패키지 관리…를 선택합니다.
- NuGet 패키지 관리 대화 상자에서 온라인 탭과 EntityFramework 패키지를 차례로 선택합니다.
- 설치를 클릭합니다.
참고 항목
EntityFramework 어셈블리뿐만 아니라 System.ComponentModel.DataAnnotations에 대한 참조도 추가됩니다. 프로젝트에 System.Data.Entity에 대한 참조가 있는 경우 EntityFramework 패키지가 설치될 때 제거됩니다. System.Data.Entity 어셈블리는 Entity Framework 6 애플리케이션에서 더 이상 사용되지 않습니다.
모델 정의
이 연습에서는 Code First 또는 EF 디자이너를 사용하여 모델을 구현하도록 선택할 수 있습니다. 다음 두 섹션 중 하나를 완료합니다.
옵션 1: Code First를 사용해 모델 정의
이 섹션에서는 Code First를 사용하여 모델 및 관련 데이터베이스를 만드는 방법을 보여 줍니다. Database First를 사용하여 EF 디자이너로 데이터베이스에서 모델을 리버스 엔지니어링하려는 경우 다음 섹션(옵션 2: Database First를 사용하여 모델 정의)으로 건너뜁니다.
Code First 개발을 사용하는 경우 일반적으로 개념적(도메인) 모델을 정의하는 .NET Framework 클래스를 작성하는 것부터 시작합니다.
- 새 클래스를 WPFwithEFSample에 추가합니다.
- 프로젝트 이름을 마우스 오른쪽 단추로 클릭합니다.
- 추가를 선택한 다음 새 항목을 선택합니다.
- 클래스를 선택하고 클래스 이름에 Product를 입력합니다.
- Product 클래스 정의를 다음 코드로 바꿉니다.
namespace WPFwithEFSample
{
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.Collections.ObjectModel;
namespace WPFwithEFSample
{
public class Category
{
public Category()
{
this.Products = new ObservableCollection<Product>();
}
public int CategoryId { get; set; }
public string Name { get; set; }
public virtual ObservableCollection<Product> Products { get; private set; }
}
}
카테고리 클래스의 제품 속성과 제품 클래스의 카테고리 속성은 탐색 속성입니다. Entity Framework에서 탐색 속성은 두 엔터티 형식 간의 관계를 탐색하는 방법을 제공합니다.
엔터티를 정의하는 것 외에도 DbContext에서 파생되고 DbSet<TEntity> 속성을 노출하는 클래스를 정의해야 합니다. DbSet<TEntity> 속성은 모델에 포함하려는 형식을 컨텍스트에 알립니다.
DbContext 파생 형식의 인스턴스는 런타임 중에 엔터티 개체를 관리합니다. 여기에는 데이터베이스의 데이터로 개체 채우기, 변경 내용 추적 및 데이터베이스에 데이터 유지가 포함됩니다.
- 다음 정의를 사용하여 새 ProductContext 클래스를 프로젝트에 추가합니다.
using System.Data.Entity;
namespace WPFwithEFSample
{
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에 연결하고 데이터베이스 이름으로 Products를 입력합니다.
확인을 선택하면 새 데이터베이스를 만들 것인지 묻는 메시지가 표시됩니다. 그러면 예를 선택합니다.
이제 새 데이터베이스가 서버 탐색기에 표시됩니다. 마우스 오른쪽 단추로 클릭하고 새 쿼리를 선택합니다.
다음 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> 속성이 있습니다. 그러나 WPF를 사용하여 데이터를 바인딩하는 경우 WPF가 컬렉션에 변경된 내용을 추적할 수 있도록 컬렉션 속성에 ObservableCollection을 사용하는 것이 좋습니다. 이를 위해 ObservableCollection을 사용하도록 템플릿을 수정하겠습니다.
솔루션 탐색기를 열고 ProductModel.edmx 파일을 찾습니다.
ProductModel.edmx 파일 아래에 중첩될 ProductModel.tt 파일을 찾습니다.
ProductModel.tt 파일을 두 번 클릭하여 Visual Studio 편집기에서 엽니다.
“ICollection”의 두 항목을 찾아서 “ObservableCollection”으로 바꿉니다. 이러한 항목은 대략 296번째 줄과 484번째 줄에 있습니다.
“HashSet”의 첫 번째 항목을 찾아서 “ObservableCollection”으로 바꿉니다. 이 항목은 약 50번째 줄에 있습니다. 코드 뒷부분에 있는 두 번째 HashSet 항목을 바꾸지 마세요.
"System.Collections.Generic"의 유일한 항목을 찾아서 "System.Collections.ObjectModel"로 바꿉니다. 이 항목은 대략 424번째 줄에 있습니다.
ProductModel.tt 파일을 저장합니다. 이렇게 하면 엔터티 코드가 다시 생성됩니다. 코드가 자동으로 다시 생성되지 않으면 ProductModel.tt를 마우스 오른쪽 단추로 클릭하고 “사용자 지정 도구 실행”을 선택합니다.
이제 Category.cs 파일(ProductModel.tt 아래에 중첩됨)을 열면 Products 컬렉션에 ObservableCollection<Product> 형식이 있는 것을 확인할 수 있습니다.
프로젝트를 컴파일합니다.
지연 로드
카테고리 클래스의 제품 속성과 제품 클래스의 카테고리 속성은 탐색 속성입니다. Entity Framework에서 탐색 속성은 두 엔터티 형식 간의 관계를 탐색하는 방법을 제공합니다.
EF는 탐색 속성에 처음 액세스할 때 데이터베이스에서 관련 엔터티를 자동으로 로드하는 옵션을 제공합니다. 이 유형의 로드(지연 로드라고 함)에서는 각 탐색 속성에 처음 액세스할 때 내용이 컨텍스트에 아직 없는 경우 데이터베이스에 대해 별도의 쿼리가 실행됩니다.
POCO 엔터티 형식을 사용할 때 EF는 런타임 중에 파생된 프록시 형식의 인스턴스를 만든 다음 클래스의 가상 속성을 재정의해 로딩 후크를 추가함으로써 지연 로드를 달성합니다. 관련 개체의 지연 로드를 얻으려면 탐색 속성 getter를 공개 및 가상(Visual Basic에서 Overridable)으로 선언해야 하며, 클래스가 봉인(Visual Basic에서 NotOverridable)되지 않아야 합니다. Database First를 사용하는 경우 탐색 속성이 가상으로 자동 설정되어 지연 로드를 사용할 수 있습니다. Code First 섹션에서는 같은 이유로 탐색 속성을 가상으로 설정하기로 선택했습니다.
개체를 컨트롤에 바인딩
모델에서 정의된 클래스를 이 WPF 애플리케이션의 데이터 원본으로 추가합니다.
솔루션 탐색기에서 MainWindow.xaml을 두 번 클릭하여 기본 양식을 엽니다.
주 메뉴에서 프로젝트 -> 새 데이터 원본 추가…를 선택합니다(Visual Studio 2010에서는 데이터 -> 새 데이터 원본 추가…를 선택해야 함).
데이터 원본 형식 선택 창에서 개체를 선택하고 다음을 클릭합니다.
데이터 개체 선택 대화 상자에서 WPFwithEFSample을 두 번 펼치고 범주를 선택합니다.
제품 데이터 원본은 선택할 필요가 없습니다. 제품 속성(범주 데이터 원본에 있음)을 통해 가져올 것이기 때문입니다.Finish를 클릭합니다.
데이터 원본 창이 MainWindow.xaml 창 옆에 열립니다. 데이터 원본 창이 표시되지 않으면 보기 -> 기타 Windows -> 데이터 원본을 선택합니다.
데이터 원본 창이 자동으로 숨겨지지 않도록 고정 아이콘을 누릅니다. 창이 이미 표시된 경우 새로 고침 단추를 눌러야 할 수 있습니다.
범주 데이터 원본을 선택하고 양식에서 끌어옵니다.
이 원본을 끌면 다음과 같은 일이 발생합니다.
- categoryViewSource 리소스 및 categoryDataGrid 컨트롤이 XAML에 추가되었습니다.
- 부모 그리드 요소의 DataContext 속성이 "{StaticResource categoryViewSource }"로 설정되었습니다. categoryViewSource 리소스가 outer\parent 그리드 요소에 대한 바인딩 소스 역할을 합니다. 그런 다음 내부 그리드 요소가 부모 그리드에서 DataContext 값을 상속합니다(categoryDataGrid의 ItemsSource 속성이 "{Binding}"으로 설정됨).
<Window.Resources>
<CollectionViewSource x:Key="categoryViewSource"
d:DesignSource="{d:DesignInstance {x:Type local:Category}, CreateList=True}"/>
</Window.Resources>
<Grid DataContext="{StaticResource categoryViewSource}">
<DataGrid x:Name="categoryDataGrid" AutoGenerateColumns="False" EnableRowVirtualization="True"
ItemsSource="{Binding}" Margin="13,13,43,191"
RowDetailsVisibilityMode="VisibleWhenSelected">
<DataGrid.Columns>
<DataGridTextColumn x:Name="categoryIdColumn" Binding="{Binding CategoryId}"
Header="Category Id" Width="SizeToHeader"/>
<DataGridTextColumn x:Name="nameColumn" Binding="{Binding Name}"
Header="Name" Width="SizeToHeader"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
세부 정보 그리드 추가
이제 범주를 표시하는 그리드가 있으므로 연결된 제품을 표시하는 세부 정보 그리드를 추가해 보겠습니다.
- 범주 데이터 원본에서 제품을 선택하여 양식에 끌어옵니다.
- categoryProductsViewSource 리소스 및 productDataGrid 그리드가 XAML에 추가됩니다.
- 이 리소스에 대한 바인딩 경로가 [제품]으로 설정됩니다.
- WPF 데이터 바인딩 프레임워크는 선택한 범주와 관련된 제품만 productDataGrid에 표시되도록 합니다.
- 도구 상자에서 단추를 양식으로 끌어옵니다. 이름 속성을 buttonSave로 설정하고 콘텐츠 속성을 저장으로 설정합니다.
양식은 다음과 같아야 합니다.
데이터 상호 작용을 처리하는 코드 추가
이제 주 창에 일부 이벤트 처리기를 추가해야 합니다.
XAML 창에서 <창 요소를 클릭하여 주 창을 선택합니다.
속성 창에서 오른쪽 위에 있는 이벤트를 선택한 다음 로드됨 레이블 오른쪽에 있는 텍스트 상자를 두 번 클릭합니다.
또한 디자이너에서 [저장] 단추를 두 번 클릭하여 저장 단추에 대한 클릭 이벤트를 추가합니다.
그러면 양식 뒤에 있는 코드로 이동하게 됩니다. 이제 ProductContext를 사용하여 데이터에 액세스하도록 코드를 편집합니다. 아래와 같이 MainWindow의 코드를 업데이트합니다.
이 코드는 ProductContext의 장기 실행 인스턴스를 선언합니다. ProductContext 개체는 데이터를 쿼리하고 데이터베이스에 저장하는 데 사용됩니다. 그러면 ProductContext 인스턴스의 Dispose()가 재정의된 OnClosing 메서드에서 호출됩니다. 코드 주석은 코드가 수행하는 작업에 대한 세부 정보를 제공합니다.
using System.Data.Entity;
using System.Linq;
using System.Windows;
namespace WPFwithEFSample
{
public partial class MainWindow : Window
{
private ProductContext _context = new ProductContext();
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
System.Windows.Data.CollectionViewSource categoryViewSource =
((System.Windows.Data.CollectionViewSource)(this.FindResource("categoryViewSource")));
// Load is an extension method on IQueryable,
// defined in the System.Data.Entity namespace.
// This method enumerates the results of the query,
// similar to ToList but without creating a list.
// When used with Linq to Entities this method
// creates entity objects and adds them to the context.
_context.Categories.Load();
// After the data is loaded call the DbSet<T>.Local property
// to use the DbSet<T> as a binding source.
categoryViewSource.Source = _context.Categories.Local;
}
private void buttonSave_Click(object sender, RoutedEventArgs e)
{
// When you delete an object from the related entities collection
// (in this case Products), the Entity Framework doesn’t mark
// these child entities as deleted.
// Instead, it removes the relationship between the parent and the child
// by setting the parent reference to null.
// So we manually have to delete the products
// that have a Category reference set to null.
// The following code uses LINQ to Objects
// against the Local collection of Products.
// 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 use 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);
}
}
_context.SaveChanges();
// Refresh the grids so the database generated values show up.
this.categoryDataGrid.Items.Refresh();
this.productsDataGrid.Items.Refresh();
}
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
base.OnClosing(e);
this._context.Dispose();
}
}
}
WPF 애플리케이션 테스트
애플리케이션을 컴파일하고 실행합니다. Code First를 사용한 경우 WPFwithEFSample.ProductContext 데이터베이스가 만들어지는 것을 볼 수 있습니다.
맨 위 그리드에 범주 이름을 입력하고 아래쪽 그리드에 제품 이름을 입력합니다. 기본 키는 데이터베이스에서 생성되므로 입력합니다. ID 열에 아무것도 입력하지 마세요.
저장 단추를 눌러 데이터를 데이터베이스에 저장합니다.
DbContext의 SaveChanges()를 호출하면 ID가 데이터베이스에서 생성된 값으로 채워집니다. SaveChanges() 다음에 Refresh()를 호출했기 때문에 DataGrid 컨트롤도 새 값으로 업데이트됩니다.
추가 리소스
WPF를 사용한 컬렉션의 데이터 바인딩에 대해 자세히 알아보려면 WPF 설명서의 이 항목을 참조하세요.
.NET