티스토리 뷰

반응형

앱 개발 템플릿을 보면 기본적인 모양의 타일들로만 구성이 되어 있다. 하지만, 기본으로 제공하는 앱들 중 여행 앱을 보면 메인 화면에 다양한 형태의 타일들이 붙어 있는 것을 볼 수 있다. 이런 다양한 모양의 타일을 가지는 앱을 만들기 위한 방법을 살펴 보기로 하자.

 

 

1. HubApp-Beta1 online template

Visual Studio 2012에 File -> New -> Project -> Online -> Template -> Visual C# -> 2page -> HubAppExtension 선택 -> OK

 

 

온라인 템플릿을 선택해서 프로젝트를 생성 한 후 실행을 해보자

 

 

다양한 모양의 타일을 가진 템플릿을 볼 수 있다.

여행 앱 처럼 더 다양하지는 않지만..그래도 기본 템플릿에 비하면 엄청난 발전이 느껴지는 템플릿이다. 이제 어떻게 구성된 템플릿인지 좀더 자세하게 살펴 보아야 할 것 같다.

 

2. GridViewVariableWrapPanel

MainHubPage.xaml 디자인 타임 화면을 열고 그리드 뷰를 찍어보았다.

 

        <!-- Horizontal scrolling grid used in most view states -->
        <ctrl:GridViewVariableWrapPanel x:Name="itemGridView"
            AutomationProperties.AutomationId="ItemGridView"
            AutomationProperties.Name="Grouped Items"
            Grid.RowSpan="2"
            Padding="116,137,40,46"
            ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}"
            SelectionMode="None"
            IsSwipeEnabled="false"
            IsItemClickEnabled="True"
            ItemClick="ItemView_ItemClick"           
            GroupStyleSelector="{StaticResource HubPageGroupStyleSelector}"
            ItemTemplateSelector="{StaticResource HubPageItemTemplateSelector}"
            ItemsPanel="{StaticResource HorizontalHubItemPanel}" />

 

이게 전부다.

우선 GridViewVariableWrapPanel이라는 녀석의 정체부터 파악해보자

Controls -> GridViewVariableWrapPanel.cs 파일에 있는 컨트롤로 GridView 컨트롤을 상속 받았다

 

    public class GridViewVariableWrapPanel : GridView
    {
        [DebuggerNonUserCode]

        protected override void PrepareContainerForItemOverride(Windows.UI.Xaml.DependencyObject element, object item)
        {
            try
            {
                if (item is IVariableGridSize)
                {
                    dynamic _Item = item;
                    element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.ColumnSpanProperty, _Item.ColumnSpan);
                    element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.RowSpanProperty, _Item.RowSpan);
                }
            }
            catch // Ignoring Exceptions here is by design
            {
                element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.ColumnSpanProperty, 1);
                element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.RowSpanProperty, 1);
            }
            finally
            {
                base.PrepareContainerForItemOverride(element, item);
            }
        }
    }

 

아이뎀을 그려줄 때 그 아이템이 IVariableGridSize라는 인터페이스를 상속받은 모델인지 확인해서, 상속 받은 모델이라면 타일 엘리먼트의 ColumnSpan, RowSpan을 설정해주는 작업을 한다. 큰 타일, 작은 타일을 동적으로 구성하는데 도움을 준다.

 

3. ItemsSource

ItemsSource에 바인딩된 데이터를 한번 살펴 보도록 하자

 

ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}"

 

뷰 상단에 리소스로 등록되어있는 CollectionViewSource를 찾고

        <CollectionViewSource
            x:Name="groupedItemsViewSource"
            Source="{Binding Groups}"
            IsSourceGrouped="true"
            d:Source="{Binding SampleItems, Source={d:DesignInstance Type=data:SampleDataSource, IsDesignTimeCreatable=True}}"/>

Source 프로퍼티에 바인딩 되어있는 것이 Groups라는 것을 찾았다.

 

        protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
        {
            // TODO: Create an appropriate data model for your problem domain to replace the sample data
            SampleDataSource data = new SampleDataSource();
            this.DefaultViewModel["Groups"] = data.SampleItems;
        }

 

Groups는 뷰의 cs에서 data.SampleItems를 넣어주는 것이고,

 

DataModel -> SampleDataSource.cs 에서 SampleDataSource를 찾아서 보니

 

        private ObservableCollection<GroupInfoList<object>> _sampleItems = new ObservableCollection<GroupInfoList<object>>();
        public ObservableCollection<GroupInfoList<object>> SampleItems
        {
            get { return this._sampleItems; }
        }

 

GroupInfoList<object>형을 사용하는 것을 알 수 있다.

사실 여기까지는 크게 새로운 것이 없었는데..

 

4. GroupStyleSelector="{StaticResource HubPageGroupStyleSelector}" ItemTemplateSelector="{StaticResource HubPageItemTemplateSelector}"

 

이 부분이 바로 문제의 핵심으로, 먼저 GroupStyleSelector를 보면, HubPageGroupStyleSelector로, HubAppStyles.xaml에 존재한다.

* GroupStyleSelector는 그룹의 스타일을 조건에 따라 변경할 수 있도록 만들어진 프로퍼티이다.

 

<TS:HubPageGroupStyleSelector x:Key="HubPageGroupStyleSelector" DefaultGroupStyle="{StaticResource HubPageGroupStyle}" />

 

* HubPageGroupStyleSelector는 비교적 간단한 셀렉터로 DefaultGroupStyle이라는 프로퍼티를 반환한다. 즉 이넘은 여러개의 템플릿을 교환하는 용도가 아니라, 다음에 이야기할 셀렉터의 중간 다리 역할을 할 뿐이다.

    public class HubPageGroupStyleSelector : GroupStyleSelector
    {
        public GroupStyle DefaultGroupStyle { get; set; }

        protected override GroupStyle SelectGroupStyleCore(object group, uint level)
        {
            return DefaultGroupStyle;
        }
    }

 

DefaultGroupStyle에 연결되어 있는 HubPageGroupStyle의 내용을 살펴보자

 

    <GroupStyle x:Key="HubPageGroupStyle"
                ContainerStyleSelector="{StaticResource HubPageContainerStyleSelector}"
                HeaderTemplateSelector="{StaticResource HubPageGroupTemplateSelector}"
                Panel="{StaticResource NormalGroupItemsPanel}" />

 

 

* 그룹 스타일을 보니 ContainerStyleSelector, HeaderTemplateSelector, Panel 이렇게 3개의 프로퍼티를 가지고 있는 걸 알 수 있다. 여기서 다시 한단계 더 들어가보면 HubPageContainerStyleSelector 가 있다.

 

-> 이것도 바로 위에 존재 한다.

 

    <TS:HubPageContainerStyleSelector x:Key="HubPageContainerStyleSelector"
                                      HubGroupContainerStyleDefault="{StaticResource DefaultContainerStyle}" />

 

* HubPageContainerStyleSelector의 소스를 보면 기본 스타일 셀렉터를 상속 받았고, 내부에 HubGroupContainerStyleDefault라는 프로퍼티를 가지고 있다. 스타일 셀렉터에서는 스타일을 변경할 때 SelectStyleCore 메소드를 override 받아서 사용하는데, GroupContainerDataTemplate라는 곳에 템플릿을 반환하는 XamlResourceHelper의 기능이 좀 특이하다.

 

    public class HubPageContainerStyleSelector : StyleSelector
    {
        public Style HubGroupContainerStyleDefault { get; set; }

 

        protected override Style SelectStyleCore(object item, DependencyObject container)
        {
            try
            {
                var GroupContainerDataTemplate = XamlResourceHelper.GetGroupContainerStyleFromPage(item, container);

                if (GroupContainerDataTemplate != null)
                {
                    return GroupContainerDataTemplate;
                }
                else
                {
                    return HubGroupContainerStyleDefault;
                }
            }
            catch (System.Exception ex)
            {
                if (Debugger.IsAttached)
                {
                    Debug.WriteLine("Error in HubPageContainerStyleSelector.SelectStyleCore, details: " + ex);
                    Debugger.Break();
                }

                return base.SelectStyleCore(item, container);
            }
        }
    }

 

5. XamlResourceHelper.cs

이 프로젝트의 핵심 기능이 들어 있다고 생각한다.

 

        public static Style GetGroupContainerStyleFromPage(object item, DependencyObject container)
        {
            Style output = null;

            string GroupKey = string.Empty;

            //그룹의 Key값을 찾음

            if (item != null && item is GroupInfoList<object>)
            {
                GroupKey = ((GroupInfoList<object>)item).Key;
            }
            else if (item != null && item is ICollectionViewGroup)
            {
                var groupView = item as ICollectionViewGroup;
                var groupCollection = groupView.Group as GroupInfoList<object>;

                GroupKey = groupCollection.Key;
            }
            else
            {
                Debug.WriteLine("MainPageGroupTemplateSelector cannot determine the item type to retrieve the key");
                return null;
            }

            // Get the datatemplate from resource dictionary within the host page, Key값 + "Container"라는 이름의 스타일을 찾음
            output = XamlResourceHelper.GetStyleFromPage(container, GroupKey + "Container");

            return output;
        }

 

...

 

        private static object GetDataTemplateFromPage(DependencyObject container, string TemplateName, string TemplateSuffix)
        {
            object output = null;

            Page ParentPage = container.FindParentPage() as Page; // Note: Have to use the FindParentPage since App.Current.Resources does not contain items from page, 확장메소드로 컨테이너가 속한 페이지를 반환

            // Create proper template name, 템플릿 이름 구성하고
            string NameOfResource = TemplateName + TemplateSuffix;
            Debug.WriteLine("GetDataTemplateFromPage is looking for a template with name " + NameOfResource);

            // 페이지에서 해당 이름의 데이터를 찾고

            if (ParentPage != null && ParentPage.Resources.Count > 0)
            {
                // See if it exists based on key
                if (ParentPage.Resources.ContainsKey(NameOfResource))
                {
                    // If it does, get it and return it
                    var ResourceObject = ParentPage.Resources[NameOfResource] as object;

                    output = ResourceObject;
                }
            }

            if (output == null)
            {
                // Finaly attempt, try looking in global resource dictionary, 페이지에서 찾지 못하면 전역 리소스에서 찾고
                try
                {
                    Debug.WriteLine("Looking for style in global resource dictionary, key: " + NameOfResource);

                    output = Application.Current.Resources[NameOfResource] as object;
                }
                catch (System.Exception)
                {
                    // Empty by design
                }
            }

            return output;
        }

 

...

 

그룹스타일 셀렉터, 아이템 템플릿 셀렉터.. 각각의 셀렉터들이 모두 비슷한 동작을 한다. 즉, 클래스 이름을 기반으로 스타일이나, 템플릿을 찾아서 사용하도록 구성되어 있는 것이다.

 

6. 그렇다면, 클래스의 이름 기반 템플릿이 아닌 다른 이름의 템플릿을 사용해야 하는 경우는 어떻게 처리하나?

 

SampleDataSource.cs

원래대로라면 SampleDataItem3라는 이름의 템플릿을 찾아서 사용해야 하겠지만, 이 템플릿은 SampleDataItemThree라는 이름의 템플릿을 사용하도록 지정되어있다. TemplateItemName이란 속성은 사용자가 추가한 속성이다. 그래서, 템플릿의 이름을 찾을 때 확장 메소드  GetTemplateName을 사용해서 이름을 반환 받아야 한다.

 

    [TemplateItemName("SampleDataItemThree")] // If you dont want to use class name for template selection, use this attribute to override
    public class SampleDataItem3 : SampleDataCommon, ISampleDataItem, IVariableGridSize
    {
        public SampleDataItem3(String uniqueId, String title, String subtitle, String imagePath, String description, String content, GroupInfoList<object> group)
            : base(uniqueId, title, subtitle, imagePath, description)
        {
            this._content = content;
            this._group = group;
        }

        private string _content = string.Empty;
        public string Content
        {
            get { return this._content; }
            set { this.SetProperty(ref this._content, value); }
        }

        private GroupInfoList<object> _group;
        public GroupInfoList<object> Group
        {
            get { return this._group; }
            set { this.SetProperty(ref this._group, value); }
        }

        public int RowSpan { get; set; }
        public int ColumnSpan { get; set; }
    }

 

7. Extension Methods 확장 메소드

 

이 프로젝트에서 사용되는 확장 메소드는 2가지가 있다.

        //부모 페이지를 반환한다.

        public static DependencyObject FindParentPage(this DependencyObject child)
        {
            var parent = VisualTreeHelper.GetParent(child);

            if (parent == null)
            {
                return null;
            }

            if (parent is Page)
            {
                return parent;
            }
            else
            {
                return parent.FindParentPage();
            }
        }

 

        //템플릿 이름을 반환한다.

        public static string GetTemplateName(this object item)
        {

            string output = string.Empty;

            // Try to get name from attribute
            System.Reflection.MemberInfo info = item.GetType().GetTypeInfo();
            foreach (object attrib in info.GetCustomAttributes(true))
            {
                if (attrib is TemplateItemName)
                {
                    var TemplateNameAttribute = attrib as TemplateItemName;

                    output = TemplateNameAttribute.Name;
                }
            }

            // If not found in attribute, try to get name from Type (class name)
            if (output == string.Empty)
            {
                output = item.GetType().GetTypeInfo().Name;
            }

            if (item is IVariableGridSize)
            {
                var itemWithSize = item as IVariableGridSize;

                if (itemWithSize.RowSpan > 1 || itemWithSize.ColumnSpan > 1)
                {
                    output = output + String.Format("{0}by{1}", itemWithSize.RowSpan, itemWithSize.ColumnSpan);
                }
            }

            return output;
        }

 

8. 템플릿 활용하기.

아주 멋지게 만들어진 프로젝트 템플릿이라는 생각이 든다. 사용자가 약간만 자신의 취향에 맞게 수정해서 사용하면 멋진 앱을 만들 수 있을 것이다.

 

반응형

'Previous Platforms > Samples' 카테고리의 다른 글

Using Task, Async, Await  (0) 2013.07.28
Custom Behavior Sample  (0) 2012.12.25
Text file encoding sample  (0) 2012.11.28
WinRT File Based Database sample  (0) 2012.11.12
Tile + BackgroundTask = LiveTile APP!  (0) 2012.10.30
댓글