'개발/WPF'에 해당되는 글 2건

  1. 2009.04.16 HierarchicalDataTemplate 활용
  2. 2008.09.18 컨트롤의 상태정보 관리(xml)

먼저 HierarchicalDataTemplate에 대한 MSDN 예제를 간단히 살펴보고 계층적으로 똑같은 도메인 객체를 바인딩할 경우와 TreeView의 성능에 대한 문제에 대해서 살펴보도록 하겠습니다.

아래는 MSDN에서 HierarchicalDataTemplate 예제입니다.
http://msdn.microsoft.com/ko-kr/library/ms771440.aspx

아래는 위의 예제를 실행시킨 화면과 소스입니다.

public class Division

{

    public Division(string name)

    {

        _name = name;

        _teams = new List<Team>();

 

    }

 

    string _name;

 

    public string Name { get { return _name; } }

 

    List<Team> _teams;

 

    public List<Team> Teams { get { return _teams; } }

 

}

<src:ListLeagueList x:Key="MyList"/>


<HierarchicalDataTemplate DataType    = "{x:Type src:League}"

                        ItemsSource = "{Binding Path=Divisions}">

<TextBlock Text="{Binding Path=Name}"/>

</HierarchicalDataTemplate>

 

<HierarchicalDataTemplate DataType    = "{x:Type src:Division}"

                        ItemsSource = "{Binding Path=Teams}">

<TextBlock Text="{Binding Path=Name}"/>

</HierarchicalDataTemplate>

 

<DataTemplate DataType="{x:Type src:Team}">

<TextBlock Text="{Binding Path=Name}"/>

</DataTemplate>

<TreeView>

  <TreeViewItem ItemsSource="{Binding Source={StaticResource MyList}}" Header="My Soccer Leagues" />

</TreeView>

 

=> 먼저 바인딩되는 객체는 XAML코드에 정의된 key MyList ListLeagueList : List<League> 타입의 객체입니다. 이중에서 Division 클래스와 Team 클래스의 관계를 살펴보면 1:N의 관계로 설정이 된 것을 볼 수 있고 이렇게 계층적으로 나타낼 수 있는 객체를 HierarchicalDataTemplate를 이용해서 바인딩 하는 것을 볼 수 있습니다.


그렇다면 동일 클래스를 계층적으로 나타내고 싶을 경우에 도메인 모델을 어떻게 설계해야 할까요? 처음에는 단순하게 도메인 모델을 여러개로 나눠서 계층적으로 설계를 하다가 계층이 많아지면 계속 그 만큼 만들어주어야 하는 비효율적이고 수동적인 느낌이 들었습니다. 그래서 그냥 심플하게 자신의 클래스 안에 List<>형태로 자신을 가지고 있으면 된다는 생각이 들었습니다. 저는 기초코드성 코드를 계층적으로 나타낼 경우에 아래와 같이 도메인 모델 클래스를 만들었습니다.
public class BaseCode

{

    public BaseCode(string name)

    {

        this.Name = name;

        SubList = new List<BaseCode>();

    }

 

    public string Name { get; set; }

 

    public List<BaseCode> SubList { get; set; }

}

<src:ListBaseCodeList x:Key="MyList"/>

 

<HierarchicalDataTemplate DataType    = "{x:Type src:BaseCode}"

                        ItemsSource = "{Binding Path=SubList}">

<TextBlock Text="{Binding Path=Name}"/>

</HierarchicalDataTemplate>

=>  BaseCode 클래스를 살펴보면 BaseCode 클래스 안의 프로퍼티로 List<>형태로 자신과 똑같은 클래스를 가지고 있습니다. 그러면 그 List<>안에 있는 클래스도 마찬가지로 List<>형태의 자신의 클래스를 가지고 있는 것처럼 계층적으로 도메인 모델을 나타낼 수 있습니다. 이어서 XAML코드를 살펴보면 HierarchicalDataTemplate를 하나만 해준 것을 볼 수 있습니다. 이것은 바로 DataType 프로퍼티의 힘으로 똑같은 데이터 타입을 계층적으로 표시해줍니다. 아래 화면은 실행화면입니다.


=> 여기서 몇 가지 생각해 볼 사항들이 있습니다. 자식 노드에서 부모 노드의 값을 알고 싶거나 TreeView에 바인딩 되는 데이터의 양이 너무 많을 경우 트리를 펼칠 경우에 자식의 자식 노드(펼치는 순간 자식 노드를 펼칠 수 있는지 없는지를 알아야하기 때문에)를 가져오는 방법 등입니다.

일반적으로 TreeView에 바인딩 되는 데이터를 가공(쿼리 결과 값등)하는 시간이 그렇게 오래 걸리지 않는다면 TreeView에 모든 데이터를 바인딩하고 TreeView에 성능을 개선하는 방법으로 해결하면 될 것이라 생각됩니다.
그럼 TreeView의 성능에 대한 문제점에 대해서 살펴보도록 하겠습니다.
표시할 데이터의 양이 많아지면 프로그램에 성능에 당연히 영향을 주게됩니다. 많은 데이터를 컨트롤에 바인딩 할 경우에는 그 항목을 표시해주기 위한 레이아웃 컨테이너를 만들고 해당 레이아웃의 크기와 위치를 계산하기 때문입니다. 이러한 성능과 관련된 요소를 크게 2가지로 나타낼 수 있습니다.
1. 컨테이너의 재활용
2. UI 가상화
컨테이너의 재활용은 위에서 말했던 레이아웃 컨테이너를 만들고 사라지는 과정 대신에 이미 한번 만들어진 컨테이너를 재활용을 하는 것을 말하며 UI가상화는 항목이 실제로 표시될 때까지는 해당 항목에 대한 항목 컨테이너 생성 및 연결된 레이아웃에 대한 계산을 지연시키는 것을 말합니다.

그럼 이 두가지를 사용한 예제는 MSDN에 있습니다. 단순히 프로퍼티를 설정해줌으로써 성능상의 얼마만큼의 영향을 주는지 직접 한번 실행해보시면 조금은 놀라실 수 있을 것입니다.
http://msdn.microsoft.com/ko-kr/library/cc716882.aspx

VirtualizingStackPanel.IsVirtualizing="True"

VirtualizingStackPanel.VirtualizationMode="Recycling"

=> 예제에서는 최상위 항목을 100개 그 항목마다 하위 항목을 10개를 가지고 있었는데 그렇게 큰 차이는 느껴지지 않아 최상위 항목을 1000개로 늘려보았더니 위의 프로퍼티를 지정해줄 경우에는 이전과 차이가 없었지만 위의 프로퍼티를 지정해주지 않을 경우에는 엄청나게 느린 것을 확실히 느낄 수 있었습니다.
Posted by resisa
,

'찰스페졸드의 WPF'란 책을 보면 메모장(18장)에서 폰트의 정보를 xml파일로 관리한다. 글씨크기나 종류 등등부터 메모장의 크기정보를 xml파일로 관리한다. 이것을 이용하여 Xceed의 DataGridControl(WPF의 ListView컨트롤과 비슷한것으로 생각하면 된다.)을 튜닝을 하여 컬럼의 길이, 헤더의 표시여부, 컬럼의 순번을 저장하는 새로운 컨트롤을 만들어보자!

1. 먼저 XmlSerializer 클래스를 이용하여 xml로 로드 및 저장하는 환경설정클래스(DataGridSetting)를 만들어보자. xml 로드 및 저장하는 부분은 '찰스페졸드의 WPF'의 소스부분을 거의 그대로 사용한다.

 
    /// <summary>
    /// XML파일로 DataGrid의 환경내용을 저장한다.
    /// </summary>
    public class DataGridSetting
    {
        // Default Settings
       public bool[] HeaderVisible;
       public int[] Position;
       public string[] HeaderName;
       public double[] ColumnWidth;

        /// <summary>
        /// 디폴트 생성자
        /// </summary>
        public DataGridSetting()
        {
        }

        /// <summary>
        /// 컬럼의 갯수의 가져와 그 만큼의 배열을 생성해준다.
        /// </summary>
        /// <param name="count"></param>
        public DataGridSetting(int count)
        {
            HeaderVisible = new bool[count];
            Position = new int[count];
            HeaderName = new string[count];
            ColumnWidth = new double[count];
        }

        // Save settings to file.
        public virtual bool Save(string strAppData)
        {
            try
            {
                Directory.CreateDirectory(System.IO.Path.GetDirectoryName(strAppData));
                StreamWriter write = new StreamWriter(strAppData);
                XmlSerializer xml = new XmlSerializer(GetType());
                xml.Serialize(write, this);
                write.Close();
            }
            catch
            {
                return false;
            }
            return true;
        }

        // Load settings from file.
        public static object Load(Type type, string strAppData)
        {
            StreamReader reader;
            object settings;
            XmlSerializer xml = new XmlSerializer(type);

            try
            {
                reader = new StreamReader(strAppData);
                settings = xml.Deserialize(reader);
                reader.Close();
            }
            catch
            {
                settings = null;
            }
            return settings;
        }
    }


=> 소스를 살펴보면 XmlSerializer를 이용하면 DataGridSetting클래스의 객체를 xml파일로 저장해주고 로드해줄 수 있다는 것을 알 수 있다. DataGridSetting클래스의 멤버변수를 살펴보면 헤더를 보여줄지의 여부, 위치, 이름, 길이를 해당 타입에 맞게 배열로 선언하였고 생성자에서는 헤더의 갯수를 받아서 그 만큼의 배열을 선언해주는 것을 알 수 있다.
=> strAppData는 저장하는 위치를 나타내는 변수로 실제로 저장은 C:\abc.xml로 저장을 한다.
(※ 환경설정 파일이 많을 경우 xml파일의 배포 위치는 어플리케이션이 배포되는 위치에 사용자의 아이디로 폴더를 만들고 컨트롤의 이름으로 xml로 만들면 로그인한 사용자에 따라서 본인이 원하는 상태로 해당 컨트롤을 볼 수 있을 것이라고 생각한다.)

2. Xceed의 DataGridControl클래스를 상속받는 CITDataGrid란 클래스를 만들어보자. 이번 클래스는 부분적으로 살펴보자.

2.1 먼저 멤버변수들을 살펴보자.


    public class CITDataGrid : DataGridControl
    {
        // 상태정보가 저장되는 변수
        private string strAppData;

        public string StrAppData
        {
            get { return strAppData; }
            set { strAppData = value; }
        }

        // 세팅정보가 저장되는 클래스
        protected DataGridSetting settings;

        // 메뉴
        private System.Windows.Controls.ContextMenu menu;


=> 1번에서 만들었던 DataGridSetting클래스를 CITDataGrid클래스의 멤버변수로 선언을 했고 옵션으로 ContextMenu를 선언해줬다. ContextMenu는 오른쪽 버튼을 누르면 나오는 메뉴를 말하는데 윈도우 탐색기처럼 컬럼헤더에 오른쪽 버튼을 누르면 해당 컬럼의 보여줄지의 여부를 체크 해주기 위해서 선언하였다.
(※ WPF의 ListView클래스의 View(GridView)에는 ColumnHeaderContextMenu란 프로퍼티가 존재하고 ContextMenu를 이 프로퍼티에 설정해주면된다. Xceed의 DataGridControl에는 이런 프로퍼티가 없는 것 같아서 ContextMenu의 isOpen프로퍼티로 수동(?)으로 해당 메뉴의 내용을 보여준다.)

2.2 xml파일로 저장과 로드를 하는 메소드이다.

       
        protected virtual object LoadSettings()
        {
            return DataGridSetting.Load(typeof(DataGridSetting),
                                             strAppData);
        }

        protected virtual void SaveSettings()
        {
            if (settings == null)
                settings = new DataGridSetting(Columns.Count);

            int i = 0;
            foreach (Column col in Columns)
            {
                settings.HeaderVisible[i] = col.Visible;
                settings.HeaderName[i] = col.Title.ToString();
                settings.Position[i] = col.VisiblePosition;
                settings.ColumnWidth[i] = col.Width;
                i++;
            }

            settings.Save(strAppData);
        }


=> xml파일을 로드할 때는 DataGridSetting.Load()가 정적이라는 것과 저장할 때는 Columns라는 프로퍼티로 부터 모든 컬럼의 정보를 멤버변수로 선언 settings의 배열에 넣고 settings를 xml파일로 저장하는 것을 볼 수 있다.

2. 3 생성자 및 Loaded 이벤트, Current_Exit 이벤트


        /// <summary>
        /// 디폴트 생성자
        /// </summary>
        public CITDataGrid()
        {
            Loaded += new RoutedEventHandler(CITDataGrid_Loaded);
           
            Application.Current.Exit += new ExitEventHandler(Current_Exit);
        }

        void Current_Exit(object sender, ExitEventArgs e)
        {
            SaveSettings();
        }


        void CITDataGrid_Loaded(object sender, RoutedEventArgs e)
        {
            // 파일정보를 가져온다.(생성자에서 설정을 할 수없어서 컬럼이 추가되는 시점에서 세팅의 값을 가져온다.
            settings = (DataGridSetting)LoadSettings();

            if (settings != null)
            {
                int i = 0;
                foreach (Column col in Columns)
                {
                    col.Visible = settings.HeaderVisible[i];
                    col.VisiblePosition = settings.Position[i];
                    col.Width = (ColumnWidth)settings.ColumnWidth[i];
                    i++;
                }
            }

            // 메뉴를 생성
            ContextMenuInit();
           
            AddHandler(MouseRightButtonUpEvent, new RoutedEventHandler(CITDataGrid_MouseRightButtonUp));
        }


=> 실제로는 Unloaded이벤트에서 상태정보를 xml파일로 저장해주는 부분도 필요하다. 여기서 Application.Current.Exit 이벤트를 등록해주는 이유는 WPF에서는 컨트롤간에 부모, 자식 관계가 가능하다. 그런데 이 컨트롤을 실제로 사용하게 되면 DocPanal같은 컨트롤의 자식으로 설정이 되는데 프로그램을 X표시로 그냥 꺼버릴 경우에 Unloaded 이벤트가 발생하지 않는다. 그래서 Application.Current.Exit를 등록해주는 것이다. 여기서 Unloaded이벤트는 생략했다.
=> AddHandler()라는 이벤트를 등록해주는 메소드를 볼 수 있는데 오른쪽 버튼을 눌렀을 경우의 ContextMenu를 보여주기 위해서이다.

2.4 ContextMenu를 생성하고 MenuItem를 설정하고 체크유무에 따른 컬럼의 상태를 설정해주는 이벤트를 보자.


        private void ContextMenuInit()
        {
            // ContextMenu 생성
            menu = new ContextMenu();

            // 메뉴아이템 클릭 이벤트 등록
            menu.AddHandler(MenuItem.ClickEvent,
                            new RoutedEventHandler(MenuOnClick));

            MenuItem mi;
            foreach (Column col in Columns)
            {
                mi = new MenuItem();
                mi.IsChecked = col.Visible;
                mi.Header = col.Title;
                menu.Items.Add(mi);
            }
        }

        void MenuOnClick(object sender, RoutedEventArgs args)
        {
            MenuItem item = args.OriginalSource as MenuItem;
            item.IsChecked ^= true;

            foreach (Column col in Columns)
            {
                if (item.Header.ToString() == col.Title.ToString())
                {
                    col.Visible ^= true;
                    break;
                }
            }
        }
      
        void CITDataGrid_MouseRightButtonUp(object sender, RoutedEventArgs e)
        {
            TextBlock tb = e.OriginalSource as TextBlock;

            if (tb != null)
            {
                foreach (Column col in Columns)
                {
                    if (col.Title.ToString() == tb.Text)
                    {
                        menu.IsOpen = true;
                        break;
                    }
                }
            }
        }

=> true이면 false로 false이면 true로 해주는 문장을 한줄로 코딩해주고 있다. 기존에 if else문으로 하던걸 이렇게 하면 짧게 할 수 있는걸 왜 미처 생각하지 못했는지 모르겠다.. 다른 사람의 코딩을 볼 필요가 있다는 걸 많이 느끼게한 문장이다..
=> 오른쪽 버튼을 눌렀을 때 그 위치에 대한 Source를 가져오는 부분이다. 해당 부분이 Lable이면 Lable을 가져오고 TextBox이면 TextBox이다. 여기서는 TextBlock를 가져온다. TextBlock를 가져오면 해당 컬럼헤더의 Text를 컬럼의 Title과 비교하여 컬럼헤더의 부분에서 오른쪽 버튼을 클릭해줬음 알 수 있다.

3. 이제 새로운 프로젝트를 만들고 튜닝한 컨트롤을 사용해보자.


// xaml 참조
// xmlns:cit="clr-namespace:XceedDataGridTunning;assembly=XceedDataGridTunning"
// xmlns:xcdg="http://schemas.xceed.com/wpf/xaml/datagrid

<cit:CITDataGrid Name="grid" AutoCreateColumns="False" StrAppData="C:\abc.xml" 
                         ItemsSource="{Binding Source={StaticResource cvs_orders}}"
                         >
           
            <xcdg:DataGridControl.Columns>
                <xcdg:Column FieldName="TradeTime"
                             Title="시간" Width="200"
                             CellContentTemplate="{StaticResource customContentTemplate}"/>

                <xcdg:Column FieldName="DRCODE" Title="의사코드" Width="100" />
                <xcdg:Column FieldName="GRADE" Title="의사직급" Width="100" />
                <xcdg:Column FieldName="PRINTRANKING" Title="의사서열" Width="100" />
                <xcdg:Column FieldName="DRNAME" Title="의사성명" Width="100" />
                <xcdg:Column FieldName="SEQDATE" Title="진료순번관리일자" Width="100" />
            </xcdg:DataGridControl.Columns>                       
        </cit:CITDataGrid>


=> xml파일이 저장될 위치를 설정하고 튜닝된 CITDataGrid 클래스의 ItemSource를 설정하고 Column를 만들어주는 것을 볼 수 있다.

4. 개선사항
처음에는 Application.Current.Exit 이벤트를 몰라 Window를 종료할 때 CITDataGrid에서 이벤트를 잡을 수가 없어서(소멸자에서는 Columns프로퍼티가 InvalidException이 되어서 xml파일로 저장을 할 수가 없었다) Column를 상속받아서 Column에서 값이 바뀔때의 이벤트를 잡아 CITDataGrid클래스에서 알려주어 전체 Column를 저장했었다. 너무 자주 저장한다고 그래서 폼이 종료될 때 한번 저장하는걸로 바꿨다. 어플리케이션이 종료될 때 현재는 무조건 저장이 되지만 내용이 변했는지의 여부를 메소드로 만들어서 체크하여 설정내용이 변경되었을 때에는 저장하게 하는게 바람직한 것 같다. 실제 프로젝트에 사용이 되면 많은 수의 DataGrid를 열고 폼이 종료될 대 모두 다 같이 xml저장하면 어떻게 될지도 궁금하다. 또한 Page로 변경하여 익스플로어에서 xml파일로 저장해야 할 경우에 권한문제가 발생하는데 이것은 또 어떻게 해결해야 할지 ㅡ.ㅡ;;

처음으로 강좌 형식으로 글을 올렸다.. 아직 모르는게 많지만 배우고자 하고 마음은 누구못지 않기에 시작은 미약하지만 나중에는 내가 보았던 누구의 멋진 블로그처럼 많은 사람들이 좋은 정보를 많이 얻어가는 블로그로 만들어 가겠다!

Posted by resisa
,