DataGrid and Dynamic Column -- Part1

프로그래밍 2011.04.01 15:08 Posted by 아일레프
public class Data
{
    public string Type { get; set; }
    public DateTime DateTime { get; set; }
    public string Detail { get; set; }
}

위 Type 으로 이루어진 데이터의 집합이 다음과 같을 때,

<local:Data Type="A" DateTime="10-01-2009 19:56:01" Detail="활동 1"/>
<local:Data Type="A" DateTime="10-01-2009 21:56:01" Detail="활동 2"/>
<local:Data Type="A" DateTime="10-01-2009 23:56:01" Detail="활동 3"/>
<local:Data Type="A" DateTime="10-02-2009 19:56:01" Detail="활동 1"/>
<local:Data Type="A" DateTime="10-02-2009 21:56:01" Detail="활동 1"/>
<local:Data Type="B" DateTime="10-02-2009 14:56:01" Detail="활동 4"/>
<local:Data Type="C" DateTime="10-03-2009 18:56:01" Detail="활동 2"/>

이 7개의 Data를 아래와 같이 표시해야 한다고 해보자. "Type"이라는 Key로 grouping한 데이터를 날짜 순으로 표시하는 것이 목적이다.

이 경우 역시 DataGrid를 떠올리게된다. 그런데 DataGrid를 이용하기 위해 가장 먼저 생각해야 하는 것은 Column의 설정이다. 보통 DataGrid.Columns에 이를 설정해야 하는데 이 경우에는 쉽지 않다. Column으로 사용되게 되는 날짜 정보를 컴파일 이전 타임에 결정할 수 없기 때문이다. 당장 위 경우, {D, 2009/10/04 07:21, 활동 1} 데이터가 추가되게 되면 "2009 /10/4"라는 Column이 필요하게 된다. 따라서 우리는 Runtime에 어떻게 DataGrid의 Column을 설정할 수 있는가? 라는 질문을 던져야 하고 이에 대한 해답을 얻어야 한다. 또한 Column을 Runtime에 결정하는 것 외에 또 한가지 문제가 남는다. DataGrid의 Column은 ItemSource에 바인딩된 개개의 Item의 Property에 대응되게 되므로 바인딩된 Item객체의 Property를 어떻게 Dynamic하게 만들수 있는가? 라는 질문에 답해야 하는 것이다.

해답은 다양하게 있을 수 있다. 나는 3개의 가능성을 떠올렸고 이 가운데 2개는 동작하는 것을 확인했다.(나머지 하나는 확실하지 않다.) 이번 포스트는 첫번째 해답에 대해 다룬다.

 

DataGrid에 바인딩될 객체 만들기

이를 해결하기 위해 DataBase에서 얻어온 객체를 그대로 사용하는것이 아니라 DataGrid의 Row를 표현할 수 있는 새로운 class를 정의하기로 한다. 그런데 어떻게 Property를 Dynamic하게 만들 수 있을까? 사실 모든 Type의 Property는 컴파일 타임 이전에 결정된다. 그런데 단 하나의 Property는 Runtime에 결정될 수 있다. 바로 index Property "[ ]"가 바로 그것이다. 이를 이용하자.

DataGrid에 표현될 Column들은 "Key"와 "날짜" 요소들이므로 바인딩을 위한 클래스는 다음과 같이 설계될 수 있을 것이다.

public class DataForBinding : INotifyPropertyChanged
{
    private IEnumerable<Data> originalDatas;
    public string Key { get; private set; }
    public string this[string day] //yyyy/MM/dd Format의 String이 Input으로 들어오게 된다.
    {
        get 
        {
            DateTime dateTime;
            if(!DateTime.TryParseExact(day, "yyyy/MM/dd",null, System.Globalization.DateTimeStyles.None, out dateTime))
            {
                throw new ArgumentException();
            }
            StringBuilder stringBuilder = new StringBuilder();
            foreach (Data data in originalDatas.Where(d => d.DateTime.Day == dateTime.Day && d.DateTime.Month == dateTime.Month))
            {
                stringBuilder.AppendLine(string.Format ("{0:00}:{1:00}", (int)data.DateTime.TimeOfDay.TotalHours,data.DateTime.TimeOfDay.Minutes) 
                                            + " - " + data.Detail);
            }
            //19 : 56 – 활동 1
            //21 : 56 – 활동 2
            //23: 56 – 활동 3 과 같은 string을 반환한다.
            return stringBuilder.ToString();
        } 
    }

    public DataForBinding(IEnumerable<Data> datas)
    {
        if (datas == null)
            throw new ArgumentNullException("datas");
        if (datas.Count() == 0)
            throw new ArgumentException();

        this.originalDatas = datas;
        this.Key = datas.First().Type;
        if (!datas.All(d => d.Type == Key))
        {
            //하나의 Row에 대응되는 Data들은 모두 Type이 동일해야 하므로
            throw new ArgumentException();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

이제 Service로 부터 얻은 Data집합을 DataForBinding 집합으로 변환해야 할 것이다.

IEnumerable<Data> datas = GetData(); //Data Service로 부터 원본 객체를 얻는다.
IEnumerable<DataForBinding> datasForBinding = GetBindingData(datas); //바인딩을 위한 객체로 변환한다.

이 경우 GetBindingData는 다음과 같이 구현될 수 있다.

private IEnumerable<DataForBinding> GetBindingData(IEnumerable<Data> datas)
{
    IEnumerable<IGrouping<string, Data>> groupDatas = datas.GroupBy(data => data.Type);

    foreach (IGrouping<string, Data> group in groupDatas)
    {
        yield return new DataForBinding(group);
    }
}   

DataGrid의 Column 설정하기

바인딩될 Data는 위와 같이 손쉽게 만들 수 있었다. 이제 DataGrid의 Column을 설정하는 일이 남았다. 이 경우 DataGrid의 Column객체를 다음과 같이 설정하면 된다. 이는 Binding객체가 index 프로퍼티에도 적용될 수 있기에 가능한 것이다.

private void SetDataGridColumn(IEnumerable<DataForBinding> datasForBinding, IEnumerable<Data> datas)
{
    dataGrid.ItemsSource = datasForBinding;

    //Add Key Columns
    DataGridTextColumn keyTextColumn = new DataGridTextColumn();
    keyTextColumn.Header = "Key";
    Binding binding = new Binding("Key");
    keyTextColumn.Binding = binding;
    dataGrid.Columns.Add(keyTextColumn);


    //Add Date Columns
    IEnumerable<string> columnStrings = datas.GroupBy(data => data.DateTime.Date).Select(group => group.Key.ToString("yyyy/MM/dd"));

    foreach (string columnString in columnStrings)
    {
        DataGridTextColumn dateTextColumn = new DataGridTextColumn();
        dateTextColumn.Header = columnString;
        Binding bindingForDate = new Binding("[" + columnString + "]");
        dateTextColumn.Binding = bindingForDate;
        dataGrid.Columns.Add(dateTextColumn);
    }
}

 



이 방법의 문제점

그런데 이 방법은 아쉬움이 깊게 남는다. 왜인가? View와 분리되어야 할 논리 클래스에서 DataGrid, DataGridTextColumn과 같은 UI 객체를 그대로 사용하고 있기 때문이다. 이 경우 View와 논리 클래스 사이의 의존도로 인해 View가 변경되었을 때 어쩔 수 없이 논리 클래스도 변경되어야 하는 안타까운 일이 생길 수 있다. 만약 여러분의 논리 클래스에서 UIElement객체를 직접 참조하거나 new 키워드를 통해 생성하는 코드가 있다면 부지런히 다른 방법을 탐구해보아야 한다. Part2에서는 이를 해결한 해답을 보여주기로 한다.

 

신고


 

티스토리 툴바