WPF 데이터 바인딩 완벽 가이드

WPF의 데이터 바인딩은 거의 모든 객체의 속성에서 정보를 가져와 UI 요소의 속성에 채울 수 있는 강력한 기능입니다.

사용자 정의 객체를 데이터베이스에 바인딩하기

데이터 접근 컴포넌트 구축

데이터 바인딩의 핵심은 데이터 객체를 UI 요소에 연결하는 것입니다. 먼저 데이터베이스에 접근하는 코드를 작성해야 합니다. 프로젝트에 데이터베이스 접근 클래스를 추가합니다.

public class StoreDB
{
    private string connectionString = @"Data Source=.;Initial Catalog=MyDB;Integrated Security=True";

    public Product GetProduct(int ID)
    {
        // 실제 구현에서는 데이터베이스 쿼리 로직이 들어감
        var product = new Product();
        return product;
    }
}

애플리케이션 클래스에서 데이터베이스 객체를 캐싱합니다:

public partial class App : System.Windows.Application
{
    private static StoreDB storeDB = new StoreDB();
    public static StoreDB StoreDB
    {
        get { return storeDB; }
    }
}

데이터 객체 정의

객체의 공용 속성은 UI 요소의 속성에 바인딩됩니다. 양방향 바인딩을 위해서는 속성이 읽기/쓰기 가능해야 합니다.

public class Product
{
    public string ModelNumber { get; set; }
    public string ModelName { get; set; }
    public decimal UnitCost { get; set; }
    public string Description { get; set; }
}

바인딩 객체 표시

StoreDB를 사용하여 런타임에 Product 객체를 생성하고, 이를 윈도우에 바인딩합니다.

다음은 사용자가 제품 ID를 입력하여 상세 정보를 조회하는 간단한 윈도우의 XAML 마크업입니다:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        
        <TextBlock Margin="7">Product ID:</TextBlock>
        <TextBox x:Name="txtID" Margin="5" Grid.Column="1">356</TextBox>
        <Button Click="cmdGetProduct_Click" Margin="5" Padding="2" Grid.Column="2">Get Product</Button>
    </Grid>

    <Border Grid.Row="1" Padding="7" Margin="7" Background="LightSteelBlue">
        <!-- 제품 상세 정보 표시 영역 -->
    </Border>
</Grid>

디자인 시점에는 실제 데이터가 없지만, 바인딩은 미리 설정할 수 있습니다. 다음은 제품 상세 정보를 표시하는 전체 마크업입니다:

<Grid x:Name="gridProductDetails">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <TextBlock Margin="7">Model Number:</TextBlock>
    <TextBox Margin="5" Grid.Column="1" 
             Text="{Binding Path=ModelNumber}"/>
    
    <TextBlock Margin="7" Grid.Row="1">Model Name:</TextBlock>
    <TextBox Margin="5" Grid.Row="1" Grid.Column="1" 
             Text="{Binding Path=ModelName}"/>
    
    <TextBlock Margin="7" Grid.Row="2">Unit Cost:</TextBlock>
    <TextBox Margin="5" Grid.Row="2" Grid.Column="1" 
             Text="{Binding Path=UnitCost}"/>
    
    <TextBlock Margin="7" Grid.Row="3">Description:</TextBlock>
    <TextBox Margin="7" Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2"
             VerticalScrollBarVisibility="Visible" TextWrapping="Wrap" 
             Text="{Binding Path=Description}"/>                
</Grid>

세부 정보를 감싸는 Grid 패널에 이름을 지정하면 코드에서 데이터 바인딩을 완료할 수 있습니다. Grid 컨테이너의 DataContext 속성을 설정하면 모든 자식 요소의 데이터 소스를 일괄 설정할 수 있습니다:

private void cmdGetProduct_Click(object sender, RoutedEventArgs e)
{
    var ID = Int32.Parse(txtID.Text);
    gridProductDetails.DataContext = App.StoreDB.GetProduct(ID);
}

null 값 바인딩

값 형식의 경우 nullable 타입(예: decimal?)을 사용하여 데이터베이스의 null 값을 처리할 수 있습니다. 참조 형식은 항상 null을 지원합니다. 바인딩에서 TargetNullValue 속성을 설정하면 null 값 처리 방식을 지정할 수 있습니다:

Text="{Binding Path=Description, TargetNullValue=[No Description Provided]}"

데이터베이스 업데이트

TextBox 같은 편집 가능한 요소는 기본적으로 양방향 바인딩이므로 WPF가 자동으로 데이터 객체를 업데이트합니다.

StoreDB 클래스에 UpdateProduct() 메서드를 추가하고, 윈도우에 Update 버튼을 추가합니다. 버튼 클릭 시 포커스를 이동시켜 데이터 객체를 업데이트한 후, Grid의 DataContext에서 현재 Product 객체를 가져와 데이터베이스를 업데이트합니다:

private void cmdUpdate_Click(object sender, RoutedEventArgs e)
{
    FocusManager.SetFocusedElement(this, (Button)sender);
    var product = (Product)gridProductDetails.DataContext;
    App.StoreDB.UpdateProduct(product);
}

변경 알림

데이터 객체의 데이터를 직접 수정해도 UI 요소는 자동으로 업데이트되지 않습니다. 이를 해결하려면 데이터 객체에 변경 알림을 추가해야 합니다.

System.ComponentModel.INotifyPropertyChanged 인터페이스를 구현하면 속성이 변경될 때 이벤트를 발생시켜 UI를 업데이트할 수 있습니다:

public class Product : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private decimal unitCost;
    public decimal UnitCost
    {
        get { return unitCost; }
        set {
            unitCost = value;
            OnPropertyChanged("UnitCost");
        }
    }
}

팁: 여러 속성이 동시에 변경될 경우, OnPropertyChanged()에 빈 문자열을 전달하면 WPF가 해당 클래스의 모든 바인딩을 재평가합니다.

컬렉션 바인딩

WPF에서 ItemsControl 클래스의 파생 클래스들은 항목 목록을 표시할 수 있습니다. ListBox, ComboBox, ListView, DataGrid, Menu, TreeView 등이 여기에 해당합니다.

컬렉션 바인딩을 지원하기 위해 ItemsControl 클래스는 세 가지 주요 속성을 정의합니다:

속성 이름설명
ItemsSource리스트에 표시될 객체 컬렉션을 지정
DisplayMemberPath각 항목의 표시 텍스트를 생성할 속성 이름
ItemTemplate각 항목의 시각적 모양을 정의하는 데이터 템플릿

ItemsSource 속성은 IEnumerable 인터페이스를 구현하는 모든 객체를 허용하지만, 기본 IEnumerable은 읽기 전용 바인딩만 지원합니다. 컬렉션을 수정(항목 추가/제거)하려면 더 많은 기능이 필요합니다.

컬렉션 항목 표시 및 편집

제품 목록을 표시하고 선택한 제품의 상세 정보를 편집할 수 있는 윈도우를 만들어 보겠습니다.

먼저 StoreDB에 GetProducts() 메서드를 추가하여 제품 컬렉션을 반환하도록 합니다. 그런 다음 코드 비하인드에서 products 필드를 선언하고 버튼 클릭 시 컬렉션을 채워 ListBox에 바인딩합니다:

private List<Product> products;

private void cmdGetProducts_Click(object sender, RoutedEventArgs e)
{
    products = App.StoreDB.GetProducts();
    lstProducts.ItemsSource = products;
}

ListBox에 데이터를 표시하는 방법은 세 가지입니다: DisplayMemberPath 사용, ToString() 재정의, 데이터 템플릿 사용. 다음은 DisplayMemberPath를 사용하는 예제입니다:

<ListBox x:Name="lstProducts" DisplayMemberPath="ModelName"/>

선택한 항목의 세부 정보를 표시하려면 Grid의 DataContext를 바인딩합니다:

<Grid DataContext="{Binding ElementName=lstProducts, Path=SelectedItem}">
    <!-- 상세 정보 표시 컨트롤 -->
</Grid>

이제 편집 내용이 자동으로 데이터 객체에 반영되며, 리스트의 표시 내용도 자동으로 업데이트됩니다.

컬렉션 항목 추가 및 제거

컬렉션 변경을 추적하려면 INotifyCollectionChanged 인터페이스를 구현하는 컬렉션을 사용해야 합니다. WPF는 ObservableCollection 클래스를 제공합니다:

var products = new ObservableCollection<Product>();

ADO.NET 객체 바인딩

ADO.NET 객체를 직접 바인딩할 수도 있습니다.

LINQ 표현식 바인딩

LINQ 쿼리 결과는 IEnumerable<T> 타입입니다. ObservableCollection<T>를 사용하여 변경을 추적하려면 변환이 필요합니다:

var products = App.StoreDB.GetProducts();
var matches = products.Where(p => p.UnitCost >= 100).ToList();
var trackedProducts = new ObservableCollection<Product>(matches);

긴 목록 성능 최적화

대량의 데이터를 처리할 때는 성능 최적화가 필요할 수 있습니다.

유효성 검사

데이터 객체 내 유효성 검사

속성 설정자에 유효성 검사 규칙을 추가할 수 있습니다:

public decimal UnitCost
{
    get { return unitCost; }
    set
    {
        if (value < 0)
            throw new ArgumentException("UnitCost는 음수일 수 없습니다.");
        else
        {
            unitCost = value;
            OnPropertyChanged("UnitCost");
        }
    }
}

예외를 UI에 전달하려면 ExceptionValidationRule을 사용합니다:

<TextBox Margin="5" Grid.Row="2" Grid.Column="1">
    <TextBox.Text>
        <Binding Path="UnitCost">
            <Binding.ValidationRules>
                <ExceptionValidationRule/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

INotifyDataErrorInfo 인터페이스

예외를 발생시키지 않고 오류를 보고하려면 INotifyDataErrorInfo 인터페이스를 사용할 수 있습니다. 이 인터페이스는 여러 속성 간의 상호 작용이나 유효하지 않은 값을 유지해야 하는 경우에 유용합니다.

구현 예제:

public class Product : INotifyPropertyChanged, INotifyDataErrorInfo
{
    private Dictionary<string, List<string>> errors = 
        new Dictionary<string, List<string>>();

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    private void SetErrors(string propertyName, List<string> propertyErrors)
    {
        errors.Remove(propertyName);
        errors.Add(propertyName, propertyErrors);
        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
    }
    
    private void ClearErrors(string propertyName)
    {            
        errors.Remove(propertyName);         
        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
    }

    public IEnumerable GetErrors(string propertyName)
    {
        if (string.IsNullOrEmpty(propertyName))
            return errors.Values;
        
        return errors.ContainsKey(propertyName) ? errors[propertyName] : null;
    }
    
    public bool HasErrors
    {
        get { return errors.Count > 0; }
    }

    private string modelNumber;
    public string ModelNumber
    {
        get { return modelNumber; }
        set
        {
            modelNumber = value; 
            bool valid = modelNumber.All(char.IsLetterOrDigit);
            
            if (!valid)
                SetErrors("ModelNumber", new List<string> { "ModelNumber는 문자와 숫자만 포함할 수 있습니다." });
            else
                ClearErrors("ModelNumber");
                                
            OnPropertyChanged("ModelNumber");
        }
    }
}

바인딩에서 ValidatesOnNotifyDataErrors 속성을 true로 설정합니다:

Text="{Binding Path=ModelNumber, Mode=TwoWay, ValidatesOnNotifyDataErrors=True, NotifyOnValidationError=True}"

사용자 정의 유효성 검사 규칙

ValidationRule 클래스를 상속받아 Validate() 메서드를 재정의하면 사용자 정의 검증 규칙을 만들 수 있습니다:

public class PositivePriceRule : ValidationRule
{
    public decimal Min { get; set; } = 0;
    public decimal Max { get; set; } = decimal.MaxValue;

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        if (!decimal.TryParse(value?.ToString(), NumberStyles.Any, cultureInfo, out decimal price))
            return new ValidationResult(false, "잘못된 문자입니다.");

        if (price < Min || price > Max)
            return new ValidationResult(false, $"값은 {Min}에서 {Max} 사이여야 합니다.");

        return ValidationResult.ValidResult;
    }
}

사용자 정의 규칙을 바인딩에 적용:

<TextBox Margin="5" Grid.Row="2" Grid.Column="1">
    <TextBox.Text>
        <Binding Path="UnitCost">
            <Binding.ValidationRules>
                <local:PositivePriceRule Max="999.99"/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

유효성 검사 오류 처리

오류 이벤트를 처리하여 사용자에게 추가 정보를 제공할 수 있습니다. NotifyOnValidationError를 true로 설정한 후:

<Grid Name="gridProductDetails" Validation.Error="validationError">
private void validationError(object sender, ValidationErrorEventArgs e)
{
    if (e.Action == ValidationErrorEventAction.Added)
        MessageBox.Show(e.Error.ErrorContent.ToString());
}

오류 목록 가져오기

현재 윈도우의 모든 오류를 수집하려면 시각적 트리를 탐색합니다:

private bool FormHasErrors(out string message)
{
    var sb = new StringBuilder();
    CollectErrors(sb, gridProductDetails);
    message = sb.ToString();
    return message != "";
}

private void CollectErrors(StringBuilder sb, DependencyObject obj)
{
    foreach (object child in LogicalTreeHelper.GetChildren(obj))
    {
        if (child is TextBox element && Validation.GetHasError(element))
        {
            sb.AppendLine($"{element.Text}에 오류가 있습니다:");
            foreach (ValidationError error in Validation.GetErrors(element))
                sb.AppendLine($"  {error.ErrorContent}");
        }
        CollectErrors(sb, element);
    }
}

사용자 정의 오류 표시기

오류 템플릿을 사용자 정의하여 시각적 오류 표시를 변경할 수 있습니다:

<Style TargetType="{x:Type TextBox}">
    <Setter Property="Validation.ErrorTemplate">
        <Setter.Value>
            <ControlTemplate>
                <DockPanel LastChildFill="True">
                    <TextBlock DockPanel.Dock="Right"
                               Foreground="Red" FontSize="14" FontWeight="Bold"
                               ToolTip="{Binding ElementName=adornerPlaceholder, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
                        *
                    </TextBlock>
                    <Border BorderBrush="Green" BorderThickness="1">
                        <AdornedElementPlaceholder x:Name="adornerPlaceholder"/>
                    </Border>
                </DockPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

또는 트리거를 사용하여 간단히 툴팁을 표시할 수도 있습니다:

<Style TargetType="{x:Type TextBox}">
    <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="True">
            <Setter Property="ToolTip"
                    Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
        </Trigger>
    </Style.Triggers>
</Style>

데이터 제공자

WPF는 데이터 바인딩을 위한 다양한 데이터 제공자를 지원합니다.

태그: WPF 데이터바인딩 INotifyPropertyChanged ObservableCollection XAML

7월 9일 19:53에 게시됨