拿到一個App的需求後,對於前端工程師來講,第一步要幹什麼?作Navigation規劃!第二步要幹什麼?作頁面分解!頁面分解如何作?首先要肯定UI Element的容器,其次要抽象UI Element自己,也就是要作一堆自定義控件,最終組成整個頁面。今天咱們就說說自定義控件如何實現吧。前端
在咱們的博客園UAP的Windows Phone的版本中,一個最重要的自定義控件就是PostControl,它的樣子以下圖中紅色矩形內所示。git
這個控件在無數頁面中都要用到,並且有幾種變種。上面看到的是在主頁/熱門/精華中所展現的樣子,是界面元素最全的,包括標題,做者,發佈時間,閱讀狀態(朕已閱),摘要,屬性(最下方的三組數字),還有最下方的橫線(可不要忽視它喲,它是總體頁面設計的重要組成部分)。程序員
第二個變種,是在博客列表中,以下圖所示。細心的人能夠發現這個變種中沒有顯示做者,由於這是在博主頁面,上下文中有MS-UAP的做者名稱了,因此不必再顯示了,不然會顯得很自戀。github
第三個變種,在分類博客列表裏面,最下方的屬性沒有顯示。因爲服務器端返回的數據中,推薦/閱讀/評論次數都是0,因此弄3個0在那裏感受很傻,因此能夠不顯示了,這樣會以爲本身智商有所提高。windows
第四個變種,是在全部列表中都有,就是不顯示閱讀狀態(標題下面的「朕已閱」沒有),表示這是一篇新博客,你尚未來得及看。服務器
第五個變種,是不顯示摘要和屬性,以下圖所示。由於這篇博客你已經讀過了(朕已閱),不必再把摘要顯示出來佔據有限的屏幕空間了,留着地方顯示那些沒讀過的博客。老話兒說,吃肉別吧唧嘴,讓人家沒吃到肉的人聽着難受,顯示我們有教養。前端工程師
固然,你若是想看摘要的話,點擊一下標題,摘要就自動優雅地展開了(有個小動畫);若是想看正文,就點擊一下摘要部分,進入到閱讀頁面。app
以上這些變種的邏輯,包括動畫,都是在自定義控件中來實現的,很強大吧?下面讓我看看如何實現它吧。編輯器
WinRT SDK有兩種用戶自定義控件的實現方式,一種是User Control, 另外一種是 Template Control。在WPF/ASP.NET/WindowsForm中都有這兩個概念,只不事後者可能叫作Custom Control。總之這是一個很古老的概念了。ide
如何選擇這兩種控件呢?說實話,不知道!可是咱們強烈建議你使用Template Control, 由於咱們還沒發現它有什麼缺點,可是發現User Control有缺點。
在Visual Studio 2013中,在你的Project上點擊鼠標右鍵,Add New Item:
注意幾個選擇點,下面寫PostControl.cs, 就能夠輕輕點擊Add按鈕了。請不要猛擊該按鈕,注意我們開發人員的素質。
若是一切正常的話,你的項目文件中會出現下面兩個東西:
上面那個是PostControl.cs, 我後來把它移到Controls folder下面的,爲了好管理。下面那個是Themes/Generic.xaml, 是系統幫你生成好的,別動它的位置,不然後果自負。這裏有個bug,若是你是第二次添加自定義控件,頗有可能出現了.cs文件後,在Generic.xaml中沒有新控件的style。此時你能夠用仇恨的筆寫一封email發給有關部門控訴這個bug,而後乖乖的在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,讓別人以爲你的素質不高啊(又來了)。
若是想從外面(使用時)控制某些內容,好比顯示或不顯示做者,須要自定義屬性以下:
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