博客园客户端UAP开发随笔--自定义控件的左膀右臂
前言
我们上一次说到了App的精灵:自定义控件。这一次,我们接着这一话题,说说自定义控件的两个得力助手:
- 选择器 - TemplateSelector
- 转换器 – Converter
这两个东西能帮助自定义控件更为简单方便地被使用,所以必须掌握。
数值转换器 Converter
这个大家可能不陌生,因为在MSDN里,介绍到Data Binding时,总会顺带着介绍一下数据转换,比如这个网页:
https://msdn.microsoft.com/library/windows/apps/xaml/hh464965.aspx/
最后的那个StringFormatter就是。
在博客园UAP中,我们使用了好几个Converter,每个Converter实际上是一段cs类,派生自IValueConverter接口。它们都放在CNBlogs.Shared项目的ControlHelper目录中。我们用其中的两个做例子来说明一下。
Bool/Visibility的转换
我们看这段代码:
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return (bool)value ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
功能很直接,就是把bool值转换成Visibility的枚举值,然后绑定到一个控件的某个元素上,比如,在XAML里:
<Style TargetType="local:FavoriteAuthorControl">
<Setter Property="Template">
<Setter.Value>
……
<TextBlock Grid.Column="0" Text="" FontFamily="Segoe UI Symbol" Foreground="Red" Visibility="{Binding Converter={StaticResource BoolToVisiblityConverter}, Path=HasNew}"/>
……
</Setter.Value>
</Setter>
</Style>
上面这段XAML是一个自定义控件的style描述,控件本身是“收藏”页中的“关注的博主”控件,其目的是:当HasNew字段值是true时,在博主的头像左侧显示一个红色的小星星,表示该博主有新的博客发表了,你如果关注了该博主,可以点进去看他的新博客。当HasNew是false时,不显示小星星,你就不需要进去看一圈,发现没有新博客然后失望地离开了。
有的人也许会说:我在FavoriteAuthorControl.cs的code里,直接取HasNew值,看其是true还是false,然后直接用code写Visibility = Visiable or Collapsed即可。
这样做当然也可以啦,但是有几个不方便的地方:
1)你需要写code
2)读你的程序的人,如果没读到你的code,就不知道这个逻辑
3)你需要命名那个TextBlock (x:Name=…)然后在code里用GetChildTemplate(“…")来得到这个控件,再指定其Visibility值。
4)在其它控件中,你依然还要写code做类似的事
而用我们推荐的方法,其好处显而易见:
1)在XAML中绑定即可,no code
2)这个转换器可以重用
3)无需更多定义,在binding阶段一次性解决(这也是WPF的优点之一),性能无需考虑
4)可读性极好
bool/string转换器
再举一个例子巩固一下,上code:
class NightModeLabelConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
bool bValue = (bool)value;
if (bValue)
{
return "日间模式";
}
else
{
return "夜间模式";
}
}
public object ConvertBack(object value, Type targetType, object parameter, string culture)
{
throw new NotImplementedException();
}
}
在XAML中这样使用:
<AppBarButton x:Name=”btn_Mode”Label=”{Binding Converter={StaticResource NightModeLabelConverter}, Path=Settings.NightMode”/>
这个绑定过程会先检查Setting中NightMode的值,如果是True,会在按钮下方显示“日间模式”;如果是false,会显示“夜间模式”。
模板选择器 TemplateSelector
模板选择器很少有人用到,因为介绍它的文档很少,到目前为止我们没发现,也是一个偶然的机会知道有这么个东西的,很好用!记得有几次面试,我们都问到了这个问题:如果一条新闻有图片,另一条新闻没图片,你准备如何显示这两个Control呢?如下图所示,上面那个有图片(特意用红色矩形标记了一下),下面那个没图片。
我们得到两种回答:
1)做成一个通用的Control,然后根据有无图片的URL自动显示右侧的图片
2)用code在自定义空间里控制右侧图片是否显示
当然可以!这些都能实现这个目的。但要注意图片左侧还有一条灰色竖线哟。
我们今天推荐一个更为标准通用的方法:TemplateSelector。
先上code:
namespace CNBlogs.ControlHelper
{
class NewsControlTemplateSelector : DataTemplateSelector
{
public DataTemplate dtHasImage { get; set; }
public DataTemplate dtNoImage { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
CNBlogs.DataHelper.DataModel.News news = item as CNBlogs.DataHelper.DataModel.News;
if (string.IsNullOrEmpty(news.TopicIcon))
{
return dtNoImage;
}
return dtHasImage;
}
}
}
在Shared project中,ControlHelper目录下,有个NewsControlTemplateSelector.cs。它从DataTemplaterSelector父类派生出来,先定义了两个DataTemplate: dtHasImage和dtNoImage,分别对应有无图片的两种情况。然后在SelectTemplateCore重写方法中,先获得news实例,判断TopicIcon是否存在,如果存在,返回dtHasImage;否则返回dtNoImage。
在MainPage.xaml中,这样使用:
<Page.Resources>
......
<DataTemplate x:Key="NewsNoImageTemplate">
<local:NewsTitleTextControl/>
</DataTemplate>
<DataTemplate x:Key="NewsHasImageTemplate">
<local:NewsTitleTextImageControl/>
</DataTemplate>
......
</Page.Resources>
首先在Page.Resource中定义两个Template,给出两个不一样的Key值,一个叫做“NewsNoImageTemplate”,另一个叫做“NewsHasImageTemplate”,分别对应两个自定义control:NewsTitleTextControl和NewsTitleTextImageControl (在Theme\Generic.xaml中定义的,一个只有标题文本,另一个有标题文本和右侧图片)。
然后在News PivotItem的ListView中这样写:
<PivotItem Margin="0" Tag="news">
......
<ListView.ItemTemplateSelector>
<ControlHelper:NewsControlTemplateSelector dtHasImage="{StaticResource NewsHasImageTemplate}" dtNoImage="{StaticResource NewsNoImageTemplate}"/>
</ListView.ItemTemplateSelector>
</ListView>
</PivotItem>
ListView.ItemTemplateSelector用于定义当前的新闻用什么样的模板来显示。注意下面的语法:
1)ControlHelper:NewsControlTemplateSelector对应第一段cs代码的类名
2)dtHasImage/dtNoImage对应cs代码中的两个返回值
3)StaticResource NewsHasImageTemplate对应Page.Resources中的x:Key=”NewsHasImageTemplate”,间接对应到了有图片的自定义控件模板。
即:控件<—>资源文件<—>选择器。这三者的关系容易搞混,尤其是名字,特别容易起错,以至于最后自己都不记得什么对应什么。建议是控件用Control做结尾,如NewTitleTextControl;资源文件的Key用Template做结尾,如NewsNoImageTemplate;选择器用dt做开始,如dtNoImage。这样的话,如果有图片,Selector就会返回dtHasImage,对应到了本页资源中的StaticResource NewHasImageTemplate,从而使用NewsTitleTextImage自定义控件来显示该条新闻。
小结
好了,掌握了第一个转换器,我们不用再每个control里都写code了,直接在XAML中绑定即可。掌握了第二个选择器,我们可以充分分解页面逻辑,把它们颗粒化到一个合理的程度,不需要再考虑如何用一个控件显示两种或多种样式。
但是要注意,在以前的“App精灵”随笔中,提到的PostControl控件,有时要显示作者头像,有时要显示“朕已阅”等等,如果你要做成5个Control,再用TemplateSelector来选择就是不对的了。基本原则是:在一个ListView中,如果有两种以上的样式选择,用TemplateSelector;在两个页面中,用TemplateBinding外部配置来指定要不要显示某个元素,比如AuthorAvatar.Visibility = {TemplateBinding ShowAvatar},使用它时在两个页面中分别直接指定Visable或Collapsed即可,从而允许使用一个控件搞定5种情况。
在Windows Phone 8.1和Windows 8.1中,一点儿区别都没有,所以我们把这些cs都放在了shared project中,以便能够在两个项目中共享。
分享代码,改变世界!
Windows Phone Store App link:
https://www.windowsphone.com/zh-cn/store/app/博客园-uap/500f08f0-5be8-4723-aff9-a397beee52fc
Windows Store App link:
https://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