最近有個專案有一個這樣的需求,在文章瀏覽頁底部有幾個AppBarButton,其中有一個是評論按鈕,需要在評論按鈕上顯示一個紅色數字,類似微信的新訊息提醒:
這種設計在iOS和Android平臺都是很常見的,但UWP上並沒有提供現成的控制元件。所以只能自己實現一個了。
做出來效果是這樣的:
分析一下實現的思路。首先這還是一個AppBarButton,只是其中增加了一個數字徽章的顯示,這個屬性應該是可以繫結到其他屬性的,如果繫結的值不為0,則顯示數字,如果為0則隱藏數字。因此我們可以通過繼承AppBarButton,並修改其模板來實現這個控制元件。
下面動手實踐一下吧。我已經安裝了最新的VS2017,不過使用VS2015的話也是差不多的。首先建立一個名為AppBarBadgeButtonSample的UWP專案:
選擇在Blend中開啟:
在頁面上新增一個AppBar:
新增後是這樣的結構:
然後需要將預設AppBarButton的模板匯出來,給我們自定義的控制元件用。在按鈕上右鍵選擇編輯模板,編輯副本:
輸入一個名字,為了簡單起見我們就在當前檔案中建立這個樣式好了,點選確定。
切換到XAML模式,可以看到頁面的XAML程式碼中新增了這樣的資源:
這個就是AppBarButton的控制元件模板了。我們需要在這個預設模板的基礎上,實現自定義的AppBarBadgeButton。
現在返回到VS2017,在當前專案中新增一個名為Controls的資料夾,再新增一個名為AppBarBadgeButton的資料夾,然後新增一個模板控制元件:
確定後,可以看到專案中新增了兩個檔案,一個是cs檔案,一個是一個xaml檔案:
這個Generic.xaml即控制元件的模板,但是如果每個控制元件的模板都寫在這裡的話,會顯得比較亂。最好是每個控制元件的cs檔案和xaml檔案都放在一塊。所以在AppBarBadgeButton目錄下新增一個名為AppBarBadgeButton.xaml的樣式檔案,這樣方便管理。同時把控制元件的名稱空間從AppBarBadgeButtonSample.Controls.AppBarBadgeButton改為AppBarBadgeButtonSample.Controls。
現在可以把之前生成的預設的AppBarButton的模板複製過來了:
注意,要把key刪掉,TargetType都要改成AppBarBadgeButton:
現在需要把這個模板匯入到Generic.xaml中,把Generic.xaml中自動生成的刪掉,改為下面這樣:
<ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="ms-appx:///Controls/AppBarBadgeButton/AppBarBadgeButton.xaml" /> </ResourceDictionary.MergedDictionaries>
這樣所有的控制元件的模板都跟自己的cs檔案放在一塊,Generic.xaml檔案中只放引用位置,專案結構就清晰多了。
現在回到AppBarBadgeButton.cs,把控制元件的基類從Control改成AppBarButton。這樣控制元件的所有行為都和預設的AppBarButton一致了。
我們需要一個數字屬性,和一個控制數字是否可見的屬性。新增以下程式碼:
public string Count
{
get { return (string)GetValue(CountProperty); }
set { SetValue(CountProperty, value); }
}
// Using a DependencyProperty as the backing store for Count. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CountProperty =
DependencyProperty.Register("Count", typeof(string), typeof(AppBarBadgeButton), new PropertyMetadata("0", null));
這是一個依賴屬性。注意為什麼數字的型別是string呢?為什麼不設定成int?
賣個關子,先做完再說。
現在可以修改控制元件的模板了。仔細分析模板就能找到需要改的地方,有一個名為ContentRoot的Grid,應該就是這裡了,在名為Content的ContentPresenter後面新增一個Border和一個TextBlock來顯示數字:
<Border CornerRadius="8" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Top" Background="Red" Grid.Row="0" MinWidth="16" Height="16"> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="10" Text="{TemplateBinding Count}" Foreground="Yellow" /> </Border>
注意,這裡要使用TemplateBinding將文字繫結到Count屬性。
現在來試試。我們設定Page的x:Name為rootPage,把剛才在主頁面新增的AppBarButton改成自定義的AppBarBadgeButton,不要忘了引入名稱空間:xmlns:localControls="using:AppBarBadgeButtonSample.Controls"
再設定Page的DataContext,並新增一個Count屬性,把AppBarBadgeButton的Count繫結到cs檔案的Count屬性上:
xaml:
<Page x:Class="AppBarBadgeButtonSample.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:AppBarBadgeButtonSample" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:localControls="using:AppBarBadgeButtonSample.Controls" mc:Ignorable="d" x:Name="rootPage"> <Page.BottomAppBar> <CommandBar> <CommandBar.Content> <Grid/> </CommandBar.Content> <localControls:AppBarBadgeButton Icon="Accept" Count="{Binding Count}" Label="AppBarButton"/> <AppBarButton Icon="Cancel" Label="AppBarButton"/> </CommandBar> </Page.BottomAppBar> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> </Grid> </Page>
cs:
public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); this.Count = 10; this.rootPage.DataContext = this; } public int Count { get; set; } }
我這裡沒有用標準的MVVM,只是簡單演示一下。現在執行下看看:
效果出來了。
現在思考剛才提到的問題,為什麼在DataContext裡的屬性可以設定為int,而控制元件本身的依賴屬性必須設定為string呢?如果有興趣可以將依賴屬性改為int試試,結果就是按鈕上什麼也顯示不出來:
這是因為,在使用TemplateBinding的時候,必須保證source屬性和target屬性的值的型別是匹配的,TextBlock的Text屬性是string,那麼用於繫結的屬性的型別也必須是string。在使用TemplateBinding時,沒有機會進行自動的型別轉換。如果型別不匹配,則無法繫結。可參考:https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/templatebinding-markup-extension
現在可以顯示數字了,但是還需要做的更完善一點。有數字的時候顯示,如果為0的時候就隱藏掉。所以再做一點小小的工作:
新增一個控制是否顯示數字的屬性:
public Visibility BadgeVisibility { get { return (Visibility)GetValue(BadgeVisibilityProperty); } set { SetValue(BadgeVisibilityProperty, value); } } // Using a DependencyProperty as the backing store for BadgeVisibility. This enables animation, styling, binding, etc... public static readonly DependencyProperty BadgeVisibilityProperty = DependencyProperty.Register("BadgeVisibility", typeof(Visibility), typeof(AppBarBadgeButton), new PropertyMetadata(Visibility.Collapsed, null));
當Count值變化時,將其設定為隱藏:
public string Count { get { return (string)GetValue(CountProperty); } set { SetValue(CountProperty, value); } } // Using a DependencyProperty as the backing store for Count. This enables animation, styling, binding, etc... public static readonly DependencyProperty CountProperty = DependencyProperty.Register("Count", typeof(string), typeof(AppBarBadgeButton), new PropertyMetadata("0", OnCountChanged)); private static void OnCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { int count = 0; int.TryParse(e.NewValue.ToString(), out count); if (count != 0) { ((AppBarBadgeButton)d).SetValue(BadgeVisibilityProperty, Visibility.Visible); } else { ((AppBarBadgeButton)d).SetValue(BadgeVisibilityProperty, Visibility.Collapsed); } }
最後將數字的Visibility屬性繫結到這個值上:
<Border CornerRadius="8" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Top" Background="Red" Visibility="{TemplateBinding BadgeVisibility}" Grid.Row="0" MinWidth="16" Height="16">