본문 바로가기
인생은 실전/C#

[WPF] Data Binding - ③ : ICollectionView를 이용한 Data 정렬(Sort), 그룹화(Group), 필터링(Filter), 동기화(CurrentChanged)

by 나는영하 2022. 12. 9.

 ICollectionView를 이용한 Data 정렬, 그룹화, 필터링, 동기화

 

※ 참고 Site
1. https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=goldrushing&logNo=130186019141
2. https://www.wpf-tutorial.com/listview-control/introduction/
3. https://www.wpftutorial.net/DataViews.html#intro

오늘은 그동안 배웠던 DataBinding을 이용해서 간단한 데이터 컬렉션을 ListView라는 Control에 바인딩하고 데이터를 그리드화해서 보여주도록 하겠습니다.

 

먼저 WPF에서 데이터 컬렉션을 그리드화해서 보여줄 수 있는 컨트롤은 대표적으로 3개가 있습니다. 

ListView, ListBox, DataGrid .. 

위 3개의 차이점은 다른 블로그를 참고하길 바라며 오늘 저는 ListView를 사용해서 ICollectionView 라는 인터페이스를 사용해 컬렉션을 지지고 볶고 하도록 하겠습니다. ❗❗❗❗

 

먼저 본문에 앞서서 아래의 코드들은 MVVM의 형태를 완벽하게 갖추지 않은점을 고지하고 들어가겠습니다.

(아직 WPF를 배우는 초보자로서 MVVM에 대한 개념은 이해하고 가지만 본 코드에 적용시키기에는 아직 애로사항이 많습니다.😂) → CollectionView의 기능과 활용에 대해 알아가는 부분이라고 생각하시는게 좋겠네요..

 

 1. ListView에 ICollectionView 바인딩 및 데이터 입력(Insert)

오늘 실습에 사용될 기본 UI

기본적인 디자인 부분이나 그리드 설정하는 부분등은 생략하도록 하겠습니다.

1️⃣ ListView에 ICollectionView 바인딩 (초기 설정부)

✅ View - ViewModel DataContext

    <Window.DataContext>
        <local:ListViewModel />
    </Window.DataContext>

 

✅ MainWindow.xaml (부분발췌)

<ListView Grid.Row="0" ItemsSource="{Binding CustomerView}" IsSynchronizedWithCurrentItem="True">
    <ListView.View>
            <GridView>
                <GridViewColumn Header="선택">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <CheckBox IsChecked="{Binding IsChecked, Mode=TwoWay}" ></CheckBox>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
                <GridViewColumn DisplayMemberBinding="{Binding Name}" Header="이름" />
                <GridViewColumn DisplayMemberBinding="{Binding Major}" Header="전공" />
                <GridViewColumn DisplayMemberBinding="{Binding Subject}" Header="과목" />
                <GridViewColumn DisplayMemberBinding="{Binding Grade}" Header="학점" />
            </GridView>
        </ListView.View>
    </ListView>

ListView의 기본형태인 GridView로 설정하고 각 열마다 string 타입의 프로퍼티를 바인딩한 형태입니다.

ListView의 장점은 보여지는 형태(View)를 사용자가 다양하게 설정할 수 있다고 하는데 저는 기본형태만 사용해보았습니다. 그리고 ListView의 ItemSource에 바인딩 된 CustomerView는 ICollectionView의 인스턴스이름에 해당합니다.

(CheckBox의 경우 본 실습에서는 사용하지 않고 디자인적으로만 넣어두었으니 참고 바랍니다.)

 

✅ ListViewModel.cs (ViewModel)

private ICollectionView _customerView;
public ICollectionView CustomerView { get => _customerView; set => _customerView = value; }

ObservableCollection<cClass> _classes = new ObservableCollection<cClass>();
public ListViewModel()
{
    _classes.Add(new cClass("이철용", "정보통신공학", "광통신", "A+"));
    _classes.Add(new cClass("이철용", "정보통신공학", "정보통신개론", "B+"));
    _classes.Add(new cClass("이철용", "정보통신공학", "Matlab", "A-"));
    _classes.Add(new cClass("김진수", "물리치료학과", "경영학개론", "A0"));
    _classes.Add(new cClass("김진수", "물리치료학과", "스포츠매너", "B+"));
    _classes.Add(new cClass("이범진", "경영학과", "비즈니스영어", "F"));
    _customerView = CollectionViewSource.GetDefaultView(_classes);

    _customerView.CurrentChanged += _customerView_CurrentChanged;
}

먼저 ObservableCollection<T> 클래스는 MSDN에서 아래와 같이 정의하고 있습니다.

항목이 추가 또는 제거되거나 전체 목록을 새로 고칠 때 알림을 제공하는 동적 데이터 컬렉션을 나타냅니다.

ObservableCollection은 아래 사진과 같이 INotifyPropertyChanged를 상속받고 있기 때문에 별도로 Model에 PropertyChanged 이벤트를 연결하지 않아도 자동으로 연결됩니다. 

ObservableCollection은 INotifyPropertyChanged를 상속받고 있다.

저는 그냥 List + INotifyChanged 라는 개념으로 이해하고있고, WPF에서 특정 프로퍼티를 바인딩하고자 할때 사용합니다. 

 

그리곤 ICollectionView에 CollectionViewSource.GetDefaultView()메서드를 이용해서 Source를 입력하였습니다. 

ICollectionView는 MSDN에서 아래와 같이 정의하고 있습니다.

컬렉션이 현재 레코드 관리, 사용자 지정 정렬, 필터링 및 그룹화 기능을 갖도록 합니다.

 

✅ cClass.cs (Model)

   internal class cClass
    {
        public cClass(string name, string major, string subject, string grade)
        {
            this.Name = name;
            this.Major = major;
            this.Subject = subject;
            this.Grade = grade;
        }

        public string Name { set; get; }
        public string Major { set; get; }
        public string Subject { set; get; }
        public string Grade {set; get; }
    }

2️⃣ ICollectionView 데이터 입력

입력 후 TextBox를 공백으로 만드는 방법은 여러방법이 있을 수 있겠지만 MVVM 패턴을 이용하기 위해서는 Mode가 TwoWay가 되어야 할거로 예상됩니다. 따라서 우선은 입력 후 에도 TextBox는 비워지지 않는 형태로 구현하고 추후 학습해보도록 하겠습니다..(아시는 분은 댓글 부탁드립니다.😂)

 

✅ MainWindow.xaml (부분발췌)

<StackPanel Orientation="Horizontal" Margin="0,2,0,2">
    <Label Content="이름:" />
    <TextBox Text="{Binding insertName, Mode=OneWayToSource}" />
    <Label Content="전공:" />
    <TextBox Text="{Binding insertMajor, Mode=OneWayToSource}" />
    <Label Content="과목:" />
    <TextBox Text="{Binding insertSubject, Mode=OneWayToSource}" />
    <Label Content="학점:" />
    <TextBox Text="{Binding insertGrade, Mode=OneWayToSource}" />
    <Button Content="입력" Command="{Binding InsertClick}" />
</StackPanel>

각 TextBox의 Text에 해당하는 속성값을 string 타입의 변수에 각각 바인딩하였습니다.

대신 UI(View)에서 사용자가 입력한 값이 Source(ViewModel)로 이동해야하기 떄문에 Mode를 OneWayToSource로 변경하였습니다.

 

✅ ListViewMode.l.cs (해당부분 발췌)

#region ListView Insert
public string insertName { get; set; }
public string insertMajor { get; set; }
public string insertSubject { get; set; }
public string insertGrade { get; set; }

private ICommand insertClick;
public ICommand InsertClick
{
    get
    {
        return (this.insertClick) ?? (this.insertClick = new DelegateCommand(insert_Click));
    }
}
private void insert_Click()
{
    if (!string.IsNullOrEmpty(insertName) && !string.IsNullOrEmpty(insertMajor) && !string.IsNullOrEmpty(insertSubject) && !string.IsNullOrEmpty(insertGrade))
    {
        _classes.Add(new cClass(insertName, insertMajor, insertSubject, insertGrade));
    }
    else
    {
        MessageBox.Show("항목을 입력해주세요.", "ListView 항목 입력 오류", MessageBoxButton.OKCancel, MessageBoxImage.Error);
    }
}
#endregion

DelegateCommand 클래스 부분은 인터넷에 있는 수많은 코드들을 그대로 구현해두었으니 본문에서는 코드를 생략하였습니다. (별도의 클래스 파일로 구성)

사용자가 4개의 TextBox에 모두 입력을 하면 ObervableCollection에 Add되는 형태입니다. 

Button Click 이벤트를 ICommand를 이용해서 ViewModel에 구현하는 방법은 다른 블로그 글을 참고 부탁드립니다!!

 

 

 2. ICollectionView를 이용한 정렬(Sort) 

✅ MainWindow.xaml

<StackPanel Grid.Row="1" Orientation="Vertical">
    <WrapPanel Margin="0,4,0,2">
        <ComboBox Margin="10,0,2,0" SelectedItem="{Binding SelectCombo, Mode=OneWayToSource}" SelectedIndex="1" IsEditable="False" IsReadOnly="True">
            <ComboBoxItem Content="이름" />
            <ComboBoxItem Content="전공" />
            <ComboBoxItem Content="과목" />
            <ComboBoxItem Content="학점" />
        </ComboBox>
        <Button Command="{Binding SortClick}" Content="정렬" />
        <Rectangle Width="1" Margin="5,0,10,0" Stroke="#333333" VerticalAlignment="Stretch" />

사용자가 선택한 ComboBox의 Item을 읽어오면 되기 떄문에 Mode는 OneWayToSource.

Rectangle 태그는 단순히 기능간에 구분을 두기 위한 요소이기때문에 무시하셔도 됩니다. 

 

✅ ListViewModel.cs (ViewModel)

컬렉션을 정렬하는 방법은 크게 2가지로 나뉩니다. 

 

1️⃣ 일반 정렬

private void sort_Click()
    {
        // 일반 정렬           
        string strClass = SelectCombo.Split(':')[1].Trim();
        string strSort = string.Empty;
        switch (strClass)
        {
            case "이름":
                strSort = "Name";
                break;
            case "전공":
                strSort = "Major";
                break;
            case "과목":
                strSort = "Subject";
                break;
            case "학점":
                strSort = "Grade";
                break;
            default:
                strSort = string.Empty;
                break;
        }

        _customerView.SortDescriptions.Clear();
        _customerView.SortDescriptions.Add(new SortDescription(strSort, ListSortDirection.Descending));
    }

특이사항으로는 위의 코드는 컬렉션을 내림차순으로 정렬하였는데, 오름차순으로 정렬하고 싶다면 SortDescriptions.Add 메서드의 2번째 인자를 ListSortDirection.Ascending으로 수정해주시면 됩니다.

 

2️⃣ 빠른 정렬 (IComparer 인터페이스 활용)

// 빠른 정렬 : 성능 우수 
ListCollectionView customView = CollectionViewSource.GetDefaultView(_classes) as ListCollectionView;
customView.CustomSort = new CustomSort(SelectCombo);

※ CustomSort.cs 

internal class CustomSort : IComparer
{
    public string _selectCombo = "Name";
    public CustomSort(string selectCombo)
    {
        _selectCombo= selectCombo;
    }
    public int Compare(object x, object y)
    {
        cClass custX = x as cClass;
        cClass custY = y as cClass;
        string strClass = _selectCombo.Split(':')[1].Trim();

        switch (strClass)
        {
            case "이름":
                return custX.Name.CompareTo(custY.Name);
            case "전공":
                return custX.Major.CompareTo(custY.Major);
            case "과목":
                return custX.Subject.CompareTo(custY.Subject);
            case "학점":
                return custX.Grade.CompareTo(custY.Grade);
            default:
                return 0;
        }
    }
}

Compare(object x, object y) 메서드에 대한 설명은 아래와 같습니다.

예전에 알고리즘 공부할때 배웠던 Bubble정렬과 수행하는 방식이 동일한거 같네요..? 아마도..

 

 3. ICollectionView를 이용한 필터링(Filter

이철용 이라는 이름으로 필터링

✅ MainWindow.xaml 

<TextBox Text="{Binding FilterString, Mode=OneWayToSource}" />
<Button Command="{Binding FilterClick}" Content="이름 필터" />
<Rectangle Width="1" Margin="5,0,10,0" Stroke="#333333" VerticalAlignment="Stretch" />

 

✅ ListViewModel.cs (ViewModel)

#region Filter
public string FilterString { get; set; }
private ICommand filterClick;
public ICommand FilterClick
{
    get
    {
        return (this.filterClick) ?? (this.filterClick = new DelegateCommand(Filter_Click));
    }
}
private void Filter_Click()
{
    _customerView.Filter = CustomFilter;
}

private bool CustomFilter(object obj)
{
    cClass customer = obj as cClass;
    return customer.Name.Contains(FilterString);
}
#endregion

특이사항은 없어 보이네요.

오로지 이름만 필터링 하도록 설정하였기 때문에 cClass의 Name 변수의 Contains 메서드를 사용하였습니다. 

 

 4. ICollectionView를 이용한 그룹화(Group)

전공으로 그룹화한 사진

✅ MainWindow.xaml 

<ListView.GroupStyle>
    <GroupStyle>
        <GroupStyle.HeaderTemplate>
            <DataTemplate>
                <TextBlock Margin="2" Foreground="AntiqueWhite" Background="DimGray" FontWeight="SemiBold" FontSize="11" Text="{Binding Name}" />
            </DataTemplate>
        </GroupStyle.HeaderTemplate>
    </GroupStyle>
</ListView.GroupStyle>

-------------------------------------------------------- 중략 --------------------------------------------------------

<ComboBox Margin="10,0,2,0" SelectedItem="{Binding GroupCombo, Mode=OneWayToSource}" SelectedIndex="1" IsEditable="False" IsReadOnly="True">
    <ComboBoxItem Content="이름" />
    <ComboBoxItem Content="전공" />
    <ComboBoxItem Content="과목" />
    <ComboBoxItem Content="학점" />
</ComboBox>
<Button Command="{Binding GroupClick}" Content="그룹화" />

그룹화할때는 xaml내에 그룹의 Header부분을 정의해주어야 합니다. 

ListView의 GroupStyle 태그에서 정의해주면 됩니다.

 

여기서 중요한점은 Text를 Name으로 바인딩 하였는데 Name은 Model(cClass)의 Name프로퍼티를 의미하는것이 아니고 CollectionViewGroup.Name이라는 속성과 바인딩 했다는 점입니다. 

자세한건 아래의 링크를 참고 바랍니다!!😊 제가 고민고민하다 이해가 안되서 올린 게시글이였습니다.. 

https://forum.dotnetdev.kr/t/wpf-groupstyle-icollectionview/5350/2

 

WPF GroupStyle에 대한 질문입니다.(ICollectionView 사용)

CollectionViewGroup.Name 속성하고 헷갈리신거 같습니다. Name은 cClass의 Name이 아닌 Group.Name인거 같습니다. Name이 아닌것으로 그룹화 했을때 그 그룹화된 이름을 보여주는 거기때문에 제가 봤을땐 정상

forum.dotnetdev.kr

 

✅ ListViewModel.cs 

#region Group
public string GroupCombo { get; set; }
private ICommand groupClick;
public ICommand GroupClick
{
    get
    {
        return (this.groupClick) ?? (this.groupClick = new DelegateCommand(Group_Click));
    }
}
private void Group_Click()
{
    string strGroup = string.Empty; 
    switch (GroupCombo.Split(':')[1].Trim())
    {
        case "이름":
            strGroup = "Name";
            break;
        case "전공":
            strGroup = "Major";
            break;
        case "과목":
            strGroup = "Subject";
            break;
        case "학점":
            strGroup = "Grade";
            break;
        default:
            strGroup = string.Empty;
            break;
    }
    _customerView.GroupDescriptions.Clear();
    _customerView.GroupDescriptions.Add(new PropertyGroupDescription(strGroup));
}
#endregion

 

 5. ICollectionView를 이용한 동기화(CurrentChanged Event)

마우스로 Item을 선택하면 하단의 StatusBar 변경

 

동기화라고 적었지만 적당한 용어는 아닌것 같습니다.

그냥 사용자가 ListView의 Item을 선택하면 현재값(CurrentItem)이 변경되었다는 이벤트가 발생합니다. 그러기 위해선 ListView의 IsSynchronizedWithCurrentItem속성을 True로 변경해주시기만 하면 됩니다. 

<ListView Grid.Row="0" ItemsSource="{Binding CustomerView}" IsSynchronizedWithCurrentItem="True">

하단의 StatusBar에 대한 코드는 아래와 같습니다.

<StatusBar Grid.Row="2" VerticalAlignment="Center" Background="Beige">
    <StatusBarItem >
        <TextBlock Text="{Binding StatusStr}" />
    </StatusBarItem>
</StatusBar>

 

✅ ListViewModel.cs

ViewModel 부분에서는 CurrentChanged 이벤트를 생성해주어야 합니다. 아래의 코드부분은 생성자 내부에 위치해 있습니다. 

_customerView.CurrentChanged += _customerView_CurrentChanged;

사용자가 View에서 ListViewItem을 선택할때마다 CurrentChanged 이벤트가 발생해서 해당 메서드를 타게 됩니다.

 #region Status
private void _customerView_CurrentChanged(object sender, EventArgs e)
{
    CollectionView cV = sender as CollectionView;
    cClass cC = cV.CurrentItem as cClass;
    StatusStr = String.Format("이름 : {0} / 학과 : {1} / 과목 : {2} / 학점 : {3}", cC.Name, cC.Major, cC.Subject, cC.Grade);

}
private string statusStr = string.Empty;
public string StatusStr
{
    get => statusStr;
    set
    {
        statusStr = value;
        OnPropertyChanged("StatusStr");
    }
}
#endregion

 

✅ ViewModelBase.cs 

 public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

사용자가 Item을 클릭하면 현재값이 바뀌면서 프로퍼티가 변경되기때문에 관련 이벤트를 생성해주어야 합니다. 흔히들 PropertyChanged라고 하지요. 저는 이 부분을 ViewModelBase라는 클래스를 만들어서 INotifyPropertyChanged 인터페이스를 상속받고 이벤트를 구현하였습니다. 

그리고 ViewModel에 위 클래스를 상속받도록 구성하였습니다. 

 

계층 구조가 아닌 List 타입 컬렉션은 ICollectionView를 사용하도록 하자!! 

댓글