ListView

ListView는 WPF에서 데이터를 표시하고 관리할 수 있는 강력한 컨트롤입니다. ListViewItemsControl을 확장한 컨트롤로, 데이터를 리스트 형식으로 보여주며, 다양한 레이아웃과 사용자 지정이 가능합니다. 특히 컬럼 기반의 그리드 형식으로 데이터를 표시하거나, 템플릿을 활용한 커스터마이징이 가능해 유연한 UI 설계를 지원합니다.

기본 구성

ListView는 데이터를 항목별로 나열하며, ItemsSource를 사용하여 데이터를 바인딩하거나, 개별 항목을 직접 추가할 수 있습니다.

<ListView>
    <ListViewItem>Item 1</ListViewItem>
    <ListViewItem>Item 2</ListViewItem>
    <ListViewItem>Item 3</ListViewItem>
</ListView>

데이터 바인딩

ItemsSource를 사용하면 컬렉션 데이터를 ListView에 바인딩할 수 있습니다.

데이터 바인딩 예제

<ListView ItemsSource="{Binding MyItems}">
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" />
            <GridViewColumn Header="Age" DisplayMemberBinding="{Binding Age}" />
        </GridView>
    </ListView.View>
</ListView>

코드 비하인드 (ViewModel 또는 코드에서)

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
public partial class MainWindow : Window
{
    public ObservableCollection<Person> MyItems { get; set; }
    public MainWindow()
    {
        InitializeComponent();
        MyItems = new ObservableCollection<Person>
        {
            new Person { Name = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 30 },
            new Person { Name = "Charlie", Age = 35 }
        };
        DataContext = this;
    }
}

ObservableCollection 사용

ObservableCollection은 데이터가 변경될 때 ListView를 자동으로 업데이트합니다.

public ObservableCollection<string> Items { get; set; } = new ObservableCollection<string>();
public MainWindow()
{
    InitializeComponent();
    Items.Add("Item 1");
    Items.Add("Item 2");
    DataContext = this;
}

네, List 자체를 ListView에 바인딩할 수 있습니다. 그러나 List는 변경 알림을 제공하지 않기 때문에 ListView가 데이터 변경 사항을 자동으로 업데이트하지 않습니다. 만약 데이터 변경 사항을 자동으로 반영하고 싶다면, ObservableCollection을 사용하는 것이 더 적합합니다.

List 바인딩

ListListViewItemsSource에 바인딩하면 데이터를 표시할 수 있습니다.

XAML

<ListView ItemsSource="{Binding MyList}">
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Item" DisplayMemberBinding="{Binding}" />
        </GridView>
    </ListView.View>
</ListView>

코드 비하인드

public partial class MainWindow : Window
{
    public List<string> MyList { get; set; }
    public MainWindow()
    {
        InitializeComponent();
        
        // List 데이터 초기화
        MyList = new List<string> { "Apple", "Banana", "Cherry" };
        // DataContext 설정
        DataContext = this;
    }
}

주요 특징

  • 바인딩 가능: List는 기본적으로 IEnumerable를 구현하므로, ItemsSource에 바로 바인딩할 수 있습니다.
  • 변경 알림 불가능: 데이터 추가, 삭제 등의 변경 사항은 자동으로 반영되지 않습니다. 데이터를 수정한 후 UI를 업데이트하려면 ListView.Items.Refresh()를 호출해야 합니다.

변경 알림이 필요한 경우

데이터가 변경되면 ListView에 자동으로 반영되기를 원한다면, ObservableCollection을 사용하는 것이 좋습니다.

데이터 모델

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public bool InStock { get; set; }
}

XAML

<ListView ItemsSource="{Binding Products}">
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" />
            <GridViewColumn Header="Price" DisplayMemberBinding="{Binding Price, StringFormat={}{0:C}}" />
            <GridViewColumn Header="In Stock">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <CheckBox IsChecked="{Binding InStock}" IsEnabled="False" />
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
        </GridView>
    </ListView.View>
</ListView>

코드 비하인드

public partial class MainWindow : Window
{
    public ObservableCollection<Product> Products { get; set; }
    public MainWindow()
    {
        InitializeComponent();
        Products = new ObservableCollection<Product>
        {
            new Product { Name = "Laptop", Price = 1200.99m, InStock = true },
            new Product { Name = "Smartphone", Price = 799.49m, InStock = false },
            new Product { Name = "Tablet", Price = 499.00m, InStock = true }
        };
        DataContext = this;
    }
}

GridView를 사용한 컬럼 기반 레이아웃

GridView를 사용하면 ListView를 테이블 형식으로 표시할 수 있습니다.

<ListView>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Product Name" DisplayMemberBinding="{Binding ProductName}" />
            <GridViewColumn Header="Price" DisplayMemberBinding="{Binding Price}" />
            <GridViewColumn Header="Stock" DisplayMemberBinding="{Binding Stock}" />
        </GridView>
    </ListView.View>
</ListView>

템플릿 사용

ListView는 항목의 UI를 커스터마이징할 수 있도록 DataTemplate을 지원합니다.

<ListView ItemsSource="{Binding MyItems}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Name}" Width="100" />
                <TextBlock Text="{Binding Age}" Width="50" />
            </StackPanel>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

선택 이벤트 처리

ListView에서 항목 선택을 감지하려면 SelectionChanged 이벤트를 사용합니다.

XAML

<ListView SelectionChanged="ListView_SelectionChanged" ItemsSource="{Binding MyItems}">
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" />
            <GridViewColumn Header="Age" DisplayMemberBinding="{Binding Age}" />
        </GridView>
    </ListView.View>
</ListView>

코드 비하인드

private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (e.AddedItems.Count > 0)
    {
        var selectedPerson = e.AddedItems[0] as Person;
        MessageBox.Show($"Selected: {selectedPerson.Name}, Age: {selectedPerson.Age}");
    }
}

항목 스타일 및 트리거

ListView의 항목 스타일을 변경하거나 조건부 스타일을 적용할 수 있습니다.

<ListView ItemsSource="{Binding MyItems}">
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="HorizontalContentAlignment" Value="Stretch" />
            <Setter Property="Background" Value="LightGray" />
            <Style.Triggers>
                <Trigger Property="IsSelected" Value="True">
                    <Setter Property="Background" Value="LightBlue" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </ListView.ItemContainerStyle>
</ListView>

스크롤링 및 가상화

많은 데이터를 표시해야 하는 경우 성능을 개선하려면 가상화를 활용할 수 있습니다. WPF의 ListView는 기본적으로 VirtualizingStackPanel을 사용하여 가상화 기능을 제공합니다.

<ListView VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling">
    <!-- Content -->
</ListView>

Drag-and-Drop 지원

ListView는 항목을 드래그 앤 드롭으로 재배치할 수 있습니다.

간단한 드래그 앤 드롭 예제

<ListView Name="listView" PreviewMouseLeftButtonDown="ListView_PreviewMouseLeftButtonDown" Drop="ListView_Drop" AllowDrop="True">
    <ListView.ItemsSource>
        <x:Array Type="sys:String" xmlns:sys="clr-namespace:System;assembly=mscorlib">
            <sys:String>Item 1</sys:String>
            <sys:String>Item 2</sys:String>
            <sys:String>Item 3</sys:String>
        </x:Array>
    </ListView.ItemsSource>
</ListView>

코드 비하인드

private void ListView_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    var listView = sender as ListView;
    var item = listView?.SelectedItem;
    if (item != null)
    {
        DragDrop.DoDragDrop(listView, item, DragDropEffects.Move);
    }
}
private void ListView_Drop(object sender, DragEventArgs e)
{
    var target = sender as ListView;
    var data = e.Data.GetData(typeof(string)) as string;
    if (data != null && target != null)
    {
        // 재배치 로직 구현
        MessageBox.Show($"Dropped: {data}");
    }
}

검색 및 필터링

컬렉션을 필터링하려면 CollectionViewSource를 사용합니다.

XAML

<StackPanel>
    <TextBox Width="200" Margin="5" Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged}" PlaceholderText="Filter by name..." />
    <ListView ItemsSource="{Binding FilteredProducts}">
        <ListView.View>
            <GridView>
                <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" />
                <GridViewColumn Header="Price" DisplayMemberBinding="{Binding Price, StringFormat={}{0:C}}" />
            </GridView>
        </ListView.View>
    </ListView>
</StackPanel>

ViewModel

public class MainViewModel : INotifyPropertyChanged
{
    public ObservableCollection<Product> Products { get; set; }
    public ICollectionView FilteredProducts { get; }
    private string _filterText;
    public string FilterText
    {
        get => _filterText;
        set
        {
            _filterText = value;
            OnPropertyChanged();
            FilteredProducts.Refresh();
        }
    }
    public MainViewModel()
    {
        Products = new ObservableCollection<Product>
        {
            new Product { Name = "Laptop", Price = 1200.99m },
            new Product { Name = "Smartphone", Price = 799.49m },
            new Product { Name = "Tablet", Price = 499.00m }
        };
        FilteredProducts = CollectionViewSource.GetDefaultView(Products);
        FilteredProducts.Filter = FilterProducts;
    }
    private bool FilterProducts(object obj)
    {
        if (obj is not Product product || string.IsNullOrEmpty(FilterText))
            return true;
        return product.Name.Contains(FilterText, StringComparison.InvariantCultureIgnoreCase);
    }
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

그룹화 데이터

그룹화는 데이터를 카테고리별로 묶어서 표시하는 기능입니다. 예를 들어, 제품 목록을 재고 상태가격 범위에 따라 그룹화하여 표시할 수 있습니다.

동작 원리

  • ListViewItemsSource에 바인딩된 데이터는 CollectionViewSource를 통해 그룹화할 수 있습니다.
  • PropertyGroupDescription을 사용하여 그룹화 기준이 되는 속성을 지정합니다.
  • 각 그룹은 GroupStyle로 커스터마이징할 수 있습니다.

데이터 모델

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public bool InStock { get; set; } // 그룹화 기준
}

XAML

<ListView ItemsSource="{Binding Products}">
    <ListView.GroupStyle>
        <GroupStyle>
            <GroupStyle.HeaderTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}" FontWeight="Bold" FontSize="16" />
                </DataTemplate>
            </GroupStyle.HeaderTemplate>
        </GroupStyle>
    </ListView.GroupStyle>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" />
            <GridViewColumn Header="Price" DisplayMemberBinding="{Binding Price, StringFormat={}{0:C}}" />
        </GridView>
    </ListView.View>
</ListView>

ViewModel (그룹화 추가)

public MainViewModel()
{
    Products = new ObservableCollection<Product>
    {
        new Product { Name = "Laptop", Price = 1200.99m, InStock = true },
        new Product { Name = "Smartphone", Price = 799.49m, InStock = false },
        new Product { Name = "Tablet", Price = 499.00m, InStock = true }
    };
    var collectionView = CollectionViewSource.GetDefaultView(Products);
    collectionView.GroupDescriptions.Add(new PropertyGroupDescription(nameof(Product.InStock)));
}

컨텍스트 메뉴

컨텍스트 메뉴는 마우스 오른쪽 버튼 클릭 시 표시되는 메뉴로, 일반적으로 항목에 대해 특정 작업(예: 편집, 삭제)을 수행하도록 합니다.

동작 원리

  • ContextMenuListView나 특정 ListViewItem에 연결합니다.
  • 메뉴 항목은 MenuItem으로 구성되며, 명령(ICommand)을 사용하여 동작을 정의할 수 있습니다.
  • CommandParameter로 클릭된 항목을 전달할 수 있습니다.

XAML

<ListView ItemsSource="{Binding Products}">
    <ListView.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Delete" Command="{Binding DeleteCommand}" CommandParameter="{Binding}" />
            <MenuItem Header="Edit" Command="{Binding EditCommand}" CommandParameter="{Binding}" />
        </ContextMenu>
    </ListView.ContextMenu>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" />
            <GridViewColumn Header="Price" DisplayMemberBinding="{Binding Price, StringFormat={}{0:C}}" />
        </GridView>
    </ListView.View>
</ListView>

ViewModel (Command 추가)

public ICommand DeleteCommand { get; }
public ICommand EditCommand { get; }
public MainViewModel()
{
    DeleteCommand = new RelayCommand(DeleteProduct);
    EditCommand = new RelayCommand(EditProduct);
}
private void DeleteProduct(object parameter)
{
    if (parameter is Product product)
        Products.Remove(product);
}
private void EditProduct(object parameter)
{
    if (parameter is Product product)
        MessageBox.Show($"Editing {product.Name}");
}

RelayCommand Class

public class RelayCommand : ICommand
{
    private readonly Action<object> _execute;
    private readonly Predicate<object> _canExecute;
    public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
    {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute;
    }
    public bool CanExecute(object parameter) => _canExecute?.Invoke(parameter) ?? true;
    public void Execute(object parameter) => _execute(parameter);
    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }
}
```