DataGrid and Dynamic Column -- Part2

프로그래밍 2011.04.10 16:51 Posted by 아일레프

지난 Post, DataGrid and Dynamic Column – Part1에서 DataGrid의 Column을 컴파일 타임 이전이 아니라 Runtime에 결정할 수 있는 하나의 예시를 보였다. 이는 Type에 index Property, “[ ]” 가 존재한다는 것과 DataGridTextColumn의 Binding에 사용되는 Binding객체의 Path에 “[ ]”를 사용할 수 있기 때문에 가능한 것이었다. 하지만 이 방법은 논리와 View가 밀접하게 연관되어 MVVM과 같은 Pattern에서 기꺼이 사용하기에는 뭔가 아쉬움이 남게된다. Part2에서는 이를 해결할 수 있는 또 다른 해법을 제시한다.

TypeDescriptor 그리고 ICustomTypeDescriptor

지난 Post에 DataGrid에 사용될 Column은 DataGrid에 바인딩 되는 클래스의 Property에 연관을 가지기에 Property를 어떻게 Dynamic하게 설정할 수 있는지를 고민해야 한다고 말한 바 있는데, 이는 ICustomTypeDescriptor의 존재로 해결 될 수 있다. 

WPF의 ItemsControl와 ItemsControl을 상속받는 ListBox, ComboBox, DataGrid등의 컨트롤들은 ItemsSource에 바인딩된 IEnumerable 객체를 바로 Items에 사용하지 않고 ICollectionView라는 형태로 변환해 사용한다. 이 ICollectionView는(예를 들어 ListCollectionview)는 TypeDescriptor를 적극적으로 사용하는데, TypeDescriptor는 ICustomTypeDescriptor를 구현하는 Type에 대해 별도의 동작을 하게 된다. ICustomTypeDescriptor, Part 1 를 보면 이에 대해서 자세히 확인 할 수 있으며 그 내용의 핵심은 아래 그림으로 간단히 요약될 수 있다. 

즉 TypeDescriptor는 ICustomTypeDescriptor를 구현하는 객체에 대해서는 Reflection이 아니라 ICustomTypeDescriptor의 GetProperties라는 메소드로 Property를 얻어오게 된다는 것이다. 따라서 DataGrid에 바인딩 될 객체가 ICustomTypeDescriptor를 구현하게 하고, DataGrid의 AutoGenerateColumn을 true로 설정함으로써 별도의 Column설정 없이 원하는 화면을 얻을 수 있다. 이때 Column Header는 Property의 이름과 동일하게 된다.

 

ICustomTypeDescriptor를 구현하는 DataForBinding 객체를 제작

public interface ICustomTypeDescriptor
{
    AttributeCollection GetAttributes();
    string GetClassName();
    string GetComponentName();
    TypeConverter GetConverter();
    EventDescriptor GetDefaultEvent();
    PropertyDescriptor GetDefaultProperty();
    object GetEditor(Type editorBaseType);
    EventDescriptorCollection GetEvents();
    EventDescriptorCollection GetEvents(Attribute[] attributes);
    PropertyDescriptorCollection GetProperties();
    PropertyDescriptorCollection GetProperties(Attribute[] attributes);
    object GetPropertyOwner(PropertyDescriptor pd);
}

ICustomTypeDescriptor를 구현하기 위해선 위와 같은 여러 메소드들을 만들어야 한다는 부담감이 드는데, 여기서는 GetClassName, GetComponentName, GetPropertiess, GetPropertyOwner만을 설정해도 충분하다. 그리고 이중 GetProperties메소드가 가장 큰 역할을 하게 된다. 우리가 원하는 화면을 다시 한번 확인해보자.

위 화면을 보면 우리가 1개의 “Key” Property와 N개의 “날짜” Property을 필요로 한다는 것을 알 수 있다. 즉 GetProperties가 반환하는 PropertyDescriptorCollection내에 1개의 “Key”에 해당 되는 PropertyDescriptor와 N개의 “날짜” PropertyDescriptor가 포함되게 되면 된다. 좋다. 구현을 해보자.

public class DataForBinding : ICustomTypeDescriptor
{

    //중략..

    IEnumerable<Data> originalDatas;
    string key;

    PropertyDescriptorCollection propertyDescriptorCollection = new PropertyDescriptorCollection(null);

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

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

        propertyDescriptorCollection.Add(new KeyPropertyDescriptor(key));
        foreach (DateTime dateTime in datesForDisplay)
        {
            propertyDescriptorCollection.Add(new DatePropertyDescriptor(datas, dateTime));
        }
    }

    public PropertyDescriptorCollection GetProperties()
    {
        return propertyDescriptorCollection;
    }

    public string GetClassName()
    {
        return typeof(DataForBinding).Name;
    }

    public string GetComponentName()
    {
        return typeof(DataForBinding).Name;
    }

    public object GetPropertyOwner(PropertyDescriptor pd)
    {
        return this;
    }
}

위에서 Bold체로 표시된 부분을 보면, 1개의 KeyPropertyDescriptor와 N개의 DatePropertyDescriptor객체가 PropertyDescroptorCollection에 포함되게 됨을 알 수 있다. 이제 남은 일은 이 각 Property에 해당되는 KeyPropertyDescriptor와 DatePropertyDescriptor를 구현하는 일이다.


KeyPropertyDescroptor와 DatePropertyDescroptor의 구현 

KeyPropertyDescriptor와 DatePropertyDescriptor는 PropertyDescriptor라는 Abstract클래스를 상속받아 구현된다. 이를 위해 다음과 같은 메소드들을 구현해야 하는데 우리가 보여주는 DataGrid는 모두 ReadOnly로 동작하기 때문에 Set, Reset 등의 프로퍼티는 구현할 필요가 없다.

public override bool CanResetValue(object component)
public override Type ComponentType
public override object GetValue(object component)
public override bool IsReadOnly
public override Type PropertyType
public override void ResetValue(object component)
public override void SetValue(object component, object value)
public override bool ShouldSerializeValue(object component)
그럼 KeyPropertyDescriptor부터 구현해보자. 
public class KeyPropertyDescriptor : PropertyDescriptor
{
    string key;
    public KeyPropertyDescriptor(string key):base("Key", null)
    {
        this.key = key;
    }

    #region PropertyDescroptor methods
    public override bool CanResetValue(object component)
    {
        return false;
    }

    public override Type ComponentType
    {
        get { return typeof(DataForBinding); }
    }

    public override object GetValue(object component)
    {
        return key;
    }

    public override bool IsReadOnly
    {
        get { return true; }
    }

    public override Type PropertyType
    {
        get { return typeof(string); }
    }

    public override void ResetValue(object component)
    {
        throw new InvalidOperationException("이 프로퍼티는 ReadOnly로만 동작해야 한다.");
    }

    public override void SetValue(object component, object value)
    {
        throw new InvalidOperationException("이 프로퍼티는 ReadOnly로만 동작해야 한다.");
    }

    public override bool ShouldSerializeValue(object component)
    {
        return false;
    }
    #endregion
}

Key 프로퍼티의 구현은 위와 같이 간단하다. 이는 DatePropertyDescriptor도 마찬가지이다. 단, 차이가 있다면 Property이름의 제약조건으로 인해 “2009/10/2”라는 Property를 그대로 사용할 수 없다. 따라서 약간의 변환이 필요하다.

public class DatePropertyDescriptor : PropertyDescriptor
{
    private IEnumerable<Data> datas;
    private DateTime dateTime;

    public DatePropertyDescriptor(IEnumerable<Data> datas, DateTime dateTime) : 
        base(string.Format("P{0}_{1}_{2}",dateTime.Year, dateTime.Month, dateTime.Day), null)
    {
        // TODO: Complete member initialization
        this.datas = datas;
        this.dateTime = dateTime;
    }

    #region PropertyDescroptor methods
    public override bool CanResetValue(object component)
    {
        return false;
    }

    public override Type ComponentType
    {
        get { return typeof(DataForBinding); }
    }

    public override object GetValue(object component)
    {
        StringBuilder stringBuilder = new StringBuilder();
        foreach (Data data in datas.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);
        }
        return stringBuilder.ToString();
    }
    //후략…
    #endregion
}



DataGrid ItemssSource설정, Header설정

이제 모든 준비가 끝났다. 남은 것은 서비스로 부터 얻은 데이타를 DataForBinding객체로 변환해 DataGrid의 ItemsSource로 설정하는 일이다.

public MainWindow()
{
    InitializeComponent();
    IEnumerable<Data> datas = GetData(); //Data Service로 부터 원본 Data집합을 얻어낸다.
    this.dataGrid.ItemsSource = new ArrayList(GetBindingData(datas).ToList());//바인딩을 위한 객체로 변환한다.       
}

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, datas.GroupBy(d => d.DateTime.Date).OrderBy(s => s.Key).Select(s => s.Key));
    }
}
<DataGrid x:Name="dataGrid"  AutoGenerateColumns="True" IsReadOnly="true" />

주목해야 할 것은 별도의 Column설정이 필요하지 않으므로 이 부분이 View와 독립적으로 이루어질 수 있다는 점이다. 따라서 이를 MVVM패턴으로 변환하는 것도 간단하다.  또한 유의 해야 할 점은 new ArrayList(GetBindingData(datas).ToList());  와 같이 ArrayList로 변환해 ItemsSource를 설정해야 한다는 점이다. List나 IEnumerable객체를 바로 사용하면 DataGrid가 ICustomTypeDescriptor의 GetProperties를 이용하지 않는다.(이 부분은 미지로 남아있다.) 여하튼, 이 코드를 실행시키면 다음과 같은 화면을 얻을 수 있다.

하지만 보시다시피 Column의 Header가 P2009_10_1과 같이 표현되므로 이를 2009/10/1 로 표시하는 작업을 따로 해주어야 한다.

    <DataGrid x:Name="dataGrid"  AutoGenerateColumns="True" IsReadOnly="true" >
        <DataGrid.Resources>
            <local:GridColumnConverter x:Key="GridColumnConverter"/>
            <Style TargetType="DataGridColumnHeader">
                <Setter Property="ContentTemplate">
                    <Setter.Value>
                        <DataTemplate>
                            <TextBlock Text="{Binding Converter={StaticResource GridColumnConverter}}"/>
                        </DataTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </DataGrid.Resources>
    </DataGrid>

위 설정을 마치면 다음과 같이 우리가 워하는 화면을 볼 수 있다.

 

이 방법의 문제점

ICustomTypeDescriptor를 사용하는 이 방법은 Part1의 방법과 같은 DataGridColumn의 작업을 별도로 요구하지 않기에 View와 논리를 분리할 수 있다는 장점이 있다. 그러나 아쉬운 것은 이를 위해 ICustomTypeDescriptor와 PropertyDescriptor를 구현하는 별도의 객체를 매번 만들어 줘야 한다는 것이다. 이를 해결할 수 있을까? .NET 4에는 ExpandoObject라는 객체가 추가됨으로 이 문제에 대한 해결의 가능성을 보여준다. 그러나 ExpandoObject만을 사용하는 것으로는 문제가 해결되지 않음을 확인했다. 몇가지 작업이 필요한 것 같은데 이 작업이 성공한다면 Part3 포스트가 나올 수 있을 것이다.

신고


 

티스토리 툴바