博客園客戶端(Universal App)開發隨筆 -- App的精靈:自定義控件

前言

拿到一個App的需求後,對於前端工程師來講,第一步要幹什麼?作Navigation規劃!第二步要幹什麼?作頁面分解!頁面分解如何作?首先要肯定UI Element的容器,其次要抽象UI Element自己,也就是要作一堆自定義控件,最終組成整個頁面。今天咱們就說說自定義控件如何實現吧。前端

感性認識

在咱們的博客園UAP的Windows Phone的版本中,一個最重要的自定義控件就是PostControl,它的樣子以下圖中紅色矩形內所示。git

image

這個控件在無數頁面中都要用到,並且有幾種變種。上面看到的是在主頁/熱門/精華中所展現的樣子,是界面元素最全的,包括標題,做者,發佈時間,閱讀狀態(朕已閱),摘要,屬性(最下方的三組數字),還有最下方的橫線(可不要忽視它喲,它是總體頁面設計的重要組成部分)。程序員

 

第二個變種,是在博客列表中,以下圖所示。細心的人能夠發現這個變種中沒有顯示做者,由於這是在博主頁面,上下文中有MS-UAP的做者名稱了,因此不必再顯示了,不然會顯得很自戀。github

image

 

第三個變種,在分類博客列表裏面,最下方的屬性沒有顯示。因爲服務器端返回的數據中,推薦/閱讀/評論次數都是0,因此弄3個0在那裏感受很傻,因此能夠不顯示了,這樣會以爲本身智商有所提高。windows

image

 

第四個變種,是在全部列表中都有,就是不顯示閱讀狀態(標題下面的「朕已閱」沒有),表示這是一篇新博客,你尚未來得及看。服務器

image

 

第五個變種,是不顯示摘要和屬性,以下圖所示。由於這篇博客你已經讀過了(朕已閱),不必再把摘要顯示出來佔據有限的屏幕空間了,留着地方顯示那些沒讀過的博客。老話兒說,吃肉別吧唧嘴,讓人家沒吃到肉的人聽着難受,顯示我們有教養。前端工程師

固然,你若是想看摘要的話,點擊一下標題,摘要就自動優雅地展開了(有個小動畫);若是想看正文,就點擊一下摘要部分,進入到閱讀頁面。app

image

 

以上這些變種的邏輯,包括動畫,都是在自定義控件中來實現的,很強大吧?下面讓我看看如何實現它吧。編輯器

 

兩種自定義控件的選擇

WinRT SDK有兩種用戶自定義控件的實現方式,一種是User Control, 另外一種是 Template Control。在WPF/ASP.NET/WindowsForm中都有這兩個概念,只不事後者可能叫作Custom Control。總之這是一個很古老的概念了。ide

如何選擇這兩種控件呢?說實話,不知道!可是咱們強烈建議你使用Template Control, 由於咱們還沒發現它有什麼缺點,可是發現User Control有缺點。

 

在Visual Studio 2013中,在你的Project上點擊鼠標右鍵,Add New Item:

image

注意幾個選擇點,下面寫PostControl.cs, 就能夠輕輕點擊Add按鈕了。請不要猛擊該按鈕,注意我們開發人員的素質。

若是一切正常的話,你的項目文件中會出現下面兩個東西:

image

上面那個是PostControl.cs, 我後來把它移到Controls folder下面的,爲了好管理。下面那個是Themes/Generic.xaml, 是系統幫你生成好的,別動它的位置,不然後果自負。這裏有個bug,若是你是第二次添加自定義控件,頗有可能出現了.cs文件後,在Generic.xaml中沒有新控件的style。此時你能夠用仇恨的筆寫一封email發給有關部門控訴這個bug,而後乖乖的在Generic.xaml中本身添加。添加什麼東西呢?後面會說到。

 

Generic.xaml

首先咱們看這個文件中的模樣,一堆xaml語法而已:

<Style TargetType="local:PostControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:PostControl">
                    <Border>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

最開始時是上面那個樣子,空白的模板,你要把它寫成你本身想要的樣子。此時你的頭腦中要有PostControl控件的具體樣式,由於這個編輯器不能所見即所得。我最後把他改爲了如下樣子:

<Style TargetType="local:PostControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:PostControl">
                    <Border BorderThickness="0,0,0,1" BorderBrush="{ThemeResource CNBlogsLineColor}">
                        <Grid Margin="15">
                            <Grid.RowDefinitions>
                                <RowDefinition/>
                                <RowDefinition/>
                                <RowDefinition/>
                                <RowDefinition/>
                            </Grid.RowDefinitions>
                            <TextBlock x:Name="tb_Title" Grid.Row="0" Text="{Binding Title}" Style="{StaticResource PostTitleFont}"/>
                            <Grid Grid.Row="1" Margin="0,5,0,0">
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto"/>
                                    <ColumnDefinition Width="Auto"/>
                                    <ColumnDefinition Width="*"/>
                                </Grid.ColumnDefinitions>
                                <local:AuthorControl Grid.Column ="0" Visibility="{TemplateBinding AuthorVisible}" NameFontSize="20" NameColor="{ThemeResource CNBlogsAttributionColor}" AvatarHeight="25" Margin="0,0,10,0" />
                                <TextBlock Grid.Column="1" Text="{Binding PublishTime, Converter={StaticResource TimeCountDownConverter}}" Style="{StaticResource PublishTimeFont}" VerticalAlignment="Center"/>
                                <TextBlock x:Name="tb_Status" Grid.Column="2" Text="{Binding Status, Converter={StaticResource PostStatusConverter}}" FontFamily="Segoe UI Symbol" FontSize="14" HorizontalAlignment="Right" VerticalAlignment="Center"/>
                            </Grid>

                            <!-- used for tapped anywhere on title and attribution -->
                            <Rectangle x:Name="rect_Header" Grid.RowSpan="2" Fill="Transparent"/>

                            <TextBlock x:Name="tb_Summary" Grid.Row="2" Margin="0,5" TextTrimming="CharacterEllipsis" MaxLines="4" FontSize="20" FontFamily="Segoe WP" Foreground="{ThemeResource CNBlogsSummaryColor}" TextWrapping="Wrap" Visibility="Collapsed">
                                <Run Text="{Binding Summary}"/>
                                <Run Text="..."/>
                                <TextBlock.Resources>
                                    <Storyboard x:Name="sb_Summary">
                                        <DoubleAnimation Storyboard.TargetName="tb_Summary" Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:1"/>
                                    </Storyboard>
                                </TextBlock.Resources>
                            </TextBlock>
                            <local:AttributionControl x:Name="control_Attribution" Grid.Row="3" HorizontalAlignment="Right" Visibility="{TemplateBinding AttributionVisible}" FontFamily="Global User Interface"/>
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

最外面有個Border,它的好處是把邊界設置成{0,0,0,1}就會在最下面顯示分割線。若是你不想在最下面顯示一條橫線來分割兩個博客,能夠把這層border去掉。

裏面是個Grid,定義了四行:第一行是標題;第二行是做者/發佈時間/閱讀狀態。這裏做者又是另外一個自定義控件,因此寫成了local:AuthorControl;第三行是摘要;第四行是屬性,也是一個自定義控件。

說明幾個刁(雕)民(蟲)小技:

1)控件能夠套控件,好比AuthorControl和AttributionControl在PostControl裏面。

2)閱讀狀態的HorizontalAlignment=Right,就是右側對齊,這個必須在Grid裏纔有用,在StackPanel裏好像作不到。也許我不夠刁,反正在Windows Phone上沒試出來。

3)必需要知道屏幕的寬度,不然若是顯示內容不夠一屏寬,右對齊就不是真正的右對齊了,這個你本身試試就知道了。如何知道屏幕寬度呢?在PostControl.cs裏:

public PostControl()
        {
            this.DefaultStyleKey = typeof(PostControl);
            this.DataContextChanged += PostControl_DataContextChanged;
            this.Width = CNBlogs.DataHelper.Helper.Functions.GetWindowsWidth();
        }

上面那個this.Width,就是根據屏幕寬度指定控件自己的寬度。若是你要兩邊留白,能夠想別的辦法獲得該控件所屬的容器的寬度,好比listview的寬度。

4)不要在最外層設置Margin,就是Border那一層,好比Margin=20. 若是設置了,你麻煩大了!別緊張,你惟一的麻煩是在調整個頁面的樣子時,遇到一些奇怪的空白,找了一圈才知道是控件本身內部有空白。若是想設置空白,在使用這個控件的XAML裏設置,好比ListView的ItemsPanel裏。

5)你們可能看到這個:<Rectangle x:Name="rect_Header" Grid.RowSpan="2" Fill="Transparent"/>。挺奇怪的,但做用很大。由於咱們設計在點擊上面兩行(標題和做者)時隱藏或顯示摘要,可是這兩行並不會充滿了字,確定有不少空白。若是你纖細的手指點擊到了空白處,不會觸發任何內部點擊事件。若是加了一層透明的Rectangle,能夠點擊任何位置了。

6)裏面能夠發現有用到TemplateBinding語法的,這個對應的屬性要在PostControl.cs裏註冊(下面有說)。

7)寫完全部style後,仔細看一遍,不要有多餘的Border, Grid, StackPanel等容器。尤爲是在修改了style後,這種狀況頗有可能發生。其壞處就是讓別人以爲你的程序員素質不高啊微笑

8)能夠在style裏面定義動畫,就像summary裏的Storyboard那樣,而後在PostControl.cs裏調用。

9)TextBlock是個好東東,要妥善使用。好比<Run Text/>語法,能夠用來拼接字符串,還能夠指定不一樣的字體字號字色,但不建議這樣作,會毀壞的你的UI,讓別人以爲你的素質不高啊(又來了)。

 

PostControl.cs

 

自定義屬性

若是想從外面(使用時)控制某些內容,好比顯示或不顯示做者,須要自定義屬性以下:

public Visibility AuthorVisible
        {
            get { return (Visibility)GetValue(AuthorVisibleProperty); }
            set { SetValue(AuthorVisibleProperty, value); }
        }

        // Using a DependencyProperty as the backing store for AuthorVisiable.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AuthorVisibleProperty =
            DependencyProperty.Register("AuthorVisible", typeof(Visibility), typeof(PostControl), new PropertyMetadata(Visibility.Visible));

這個語法太難記住了,你能夠每次copy/paste/modify,有一個簡單的方式是在空白處鍵入propdp,而後按Tab鍵,會自動生成這些東東,而後再手工改一些關鍵的西西,就是你想要的東西了。有了這個屬性後,在Style裏面(Generic.xaml),能夠這樣寫:

<local:AuthorControl Grid.Column ="0" Visibility="{TemplateBinding AuthorVisible}" />

代表這個AuthorControl的顯示與否能夠在使用時控制。

而後你在使用這個PostControl的page.xaml中這樣寫:

<ListView x:Name="lv_AuthorPosts" Grid.Row="1" Background="{ThemeResource CNBlogsBackColor}" Loaded="lv_AuthorPosts_Loaded">
            <ListView.Header>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <local:PostControl AuthorVisible="Collapsed" Tapped="PostControl_Tapped"/>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

PostControl的AuthorVisible=Collapsed,因而乎做者不顯示了,顯示的是咱們開發者的情懷(扯遠了)。

 

事件註冊

在構造函數中,能夠註冊你想要的事件,如:

public PostControl()
        {
            this.DefaultStyleKey = typeof(PostControl);
            this.DataContextChanged += PostControl_DataContextChanged;
            this.Width = CNBlogs.DataHelper.Helper.Functions.GetWindowsWidth();
        }

此處註冊了DataContextChanged事件,並在後面要響應這個事件。

還有些事件是不需註冊的,好比OnApplyTemplate(), 表現爲一個方法,直接override便可。

 

顯示控制

若是在一個ListView中顯示一串博文,並且這些博文的狀態可能不同,好比有的是」朕已閱「不顯示摘要,有的是要顯示摘要,咱們須要在OnApplyTemplate()中控制這個顯示行爲。

protected override void OnApplyTemplate()
        {
            this.UpdateUI(false);
        }

注意啦!這裏有個問題,你實際運行就知道了,因爲ListView有一些」智能「顯示控制,它只會對前幾個博文執行OnApplyTemplate()方法,具體幾個呢?依賴於你的屏幕的高度能顯示幾個博文。對於後面全部的博文,都會無視這個方法,這樣當你卷滾ListView至下方時,悲劇發生了,沒有按照你的意思控制顯示(該隱藏的摘要沒有隱藏)。

怎麼辦?再次拿起仇恨的筆寫一封控告信,而後默默地燒掉它。幸虧咱們註冊了DataContextChanged事件,因而輕鬆地寫下以下代碼:

void PostControl_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
        this.UpdateUI(false);
}

搞定啦!後面的博文也能按照你的邏輯顯示了。具體緣由不講了,你本身領會吧。

 

事件響應

OnTapped事件是咱們必需要用到的,處理當用戶點擊此控件的任何一個部分時,你要響應的邏輯。

/// <summary>
        /// if user click title, control will collapse summary, set status = Skip, next time: show title only
        /// if user click summary, goto reading page, set status = Read, next time: show title only
        /// if user click favorite in reading page, set status = Favorite, next time: show title only
        /// </summary>
        /// <param name="e"></param>
        protected override void OnTapped(TappedRoutedEventArgs e)
        {
            CNBlogs.DataHelper.DataModel.Post post = this.DataContext as CNBlogs.DataHelper.DataModel.Post;
            if (post == null)
            {
                return;
            }

            // click on the title
            if (e.OriginalSource is Windows.UI.Xaml.Shapes.Rectangle)
            {
                this.GotoReadingPage = false;
                if (this.showSummary) // show summary
                {
                    this.HideSummary();
                    this.SetNewStatus(post, DataHelper.DataModel.PostStatus.Skip);
                }
                else // hide summary
                {
                    this.ShowSummary(true);
                }
            }
            else // click on the summary
            {
                TextBlock tbSource = e.OriginalSource as TextBlock;
                if (tbSource.Name == "tb_Summary")
                {
                    // don't navigate to target page here(in control), need do that in page's viewmodel (.cs)
                    this.GotoReadingPage = true;
                    this.SetNewStatus(post, DataHelper.DataModel.PostStatus.Read);
                }
            }

            base.OnTapped(e);
        }

我把最上端的註釋也保留了,for your eyes only.

這裏特別強調一點,很重要:其實這個點擊事件能夠在三個地方響應:

1)在這裏的code中響應

2)在ListView的Control中響應(PostControl_Tapped)

<ListView x:Name="lv_BestPosts" Grid.Row="1" Background="{ThemeResource CNBlogsBackColor}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <local:PostControl Tapped="PostControl_Tapped"/>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

3)在ListView的ItemClicked事件中響應。

有何區別呢?建議以下:

1)在PostControl.cs中響應事件時,只關注控件自己的樣式變化,好比隱藏摘要,不要作別的事情,不然就會超出你的職責範圍了,讓上層事件沒法處理。

2)在控件的PostControl_Tapped中,你能夠作上層邏輯了,好比直接顯示博文閱讀頁面。可是PostControl實例是從sender裏獲得的:

private void PostControl_Tapped(object sender, TappedRoutedEventArgs e)
        {
            PostControl postControl = sender as PostControl;
            if (postControl.GotoReadingPage)
            {
                Post post = postControl.DataContext as Post;
                this.Frame.Navigate(typeof(PostReadingPage), post);
            }
            else
            {

            }

3)在ListView的ItemClicked事件中(若是有的話),也一樣能夠作上層邏輯。記得上面那個透明的Rectangle吧,若是沒有它,ItemClicked事件也會響應,可是底層那兩個事件不必定會響應(若是手指頭太細點在了空白處)。並且對應的類實例是從click事件中獲得的,而不是sender:

private void lv_Category_ItemClick(object sender, ItemClickEventArgs e)
        {
            Category category = e.ClickedItem as Category;
            this.Frame.Navigate(typeof(SubCategoriesPage), category);
        }

如上面代碼中的Category實例,是從e.ClickedItem中獲得的。

 

小結

寫累了,休息一下,咱們已經完成了一個自定義控制的樣式定義和邏輯定義,用幾回就熟悉了。某些粗糙的App, 直接用一個TextBlock顯示內容,不加任何修飾,體現不出我程序員們的素質,建議稍微講究一些,用個template control。就拿這個PostControl來講,你盡能夠把它拿去稍微修改一下,就能夠適應全部閱讀類的需求了。真的,不信你試試,我反正已經用這個Control作了三個App了。

在Windows 8.1上,一樣的道理,可使用自定義control。可是以博客園爲例,因爲UI design相差太大,沒法複用樣式,但能夠部分複用邏輯,因此建議不要把這些control放在Shared裏面,而是放在各自的Project內部。

比較Windows 8.1和Windows Phone 8.1的自定義控件,在Windows上,因爲顯示面積大,控件要設計得大氣,別扣扣嗦嗦的,能夠色彩鮮明些,顯示充分些;可是在Windows Phone上,顯示面積小,要講究精巧,好比隱藏摘要這件事,很適合Windows Phone,可是不適合Windows。

 

分享代碼,改變世界!

 

Windows Phone Store App link:

http://www.windowsphone.com/zh-cn/store/app/博客園-uap/500f08f0-5be8-4723-aff9-a397beee52fc (明天會有一個更新)

Windows Store App link:

http://apps.microsoft.com/windows/zh-cn/app/c76b99a0-9abd-4a4e-86f0-b29bfcc51059

(明天會有一個更新)

GitHub open source link:

https://github.com/MS-UAP/cnblogs-UAP

MSDN Sample Code:

https://code.msdn.microsoft.com/CNBlogs-Client-Universal-477943ab

相關文章
相關標籤/搜索