[UWP]理解及扩展Expander
##1. 前言
最近在自定义Expander的样式,顺便看了看它的源码。
Expander控件是一个ContentControl,它通过IsExpanded属性或者通过点击Header中的ToggleButton控制内容展开或隐藏。UWP SDK中没提供这个控件,而是在UWP Community Toolkit中 [提供](https://github.com/Microsoft/UWPCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/Expander) 。它是个教科书式的入门级控件,代码简单,虽然仍然不尽如人意,但很适合用于学习如何自定义模版化控件。
##2.详解
``` cs
[ContentProperty(Name = "Content")]
[TemplatePart(Name = "PART_RootGrid", Type = typeof(Grid))]
[TemplatePart(Name = "PART_ExpanderToggleButton", Type = typeof(ToggleButton))]
[TemplatePart(Name = "PART_LayoutTransformer", Type = typeof(LayoutTransformControl))]
[TemplateVisualState(Name = "Expanded", GroupName = "ExpandedStates")]
[TemplateVisualState(Name = "Collapsed", GroupName = "ExpandedStates")]
[TemplateVisualState(Name = "LeftDirection", GroupName = "ExpandDirectionStates")]
[TemplateVisualState(Name = "DownDirection", GroupName = "ExpandDirectionStates")]
[TemplateVisualState(Name = "RightDirection", GroupName = "ExpandDirectionStates")]
[TemplateVisualState(Name = "UpDirection", GroupName = "ExpandDirectionStates")]
public class Expander : ContentControl
{
public Expander();
public string Header { get; set; }
public DataTemplate HeaderTemplate { get; set; }
public bool IsExpanded { get; set; }
public ExpandDirection ExpandDirection { get; set; }
public event EventHandler Expanded;
public event EventHandler Collapsed;
public void OnExpandDirectionChanged();
protected override void OnApplyTemplate();
protected virtual void OnCollapsed(EventArgs args);
protected virtual void OnExpanded(EventArgs args);
}
```
以上是Expander的代码定义,可以看出这个控件十分简单。本文首先对代码和XAML做个详细了解。这部分完全是面向初学者的,希望初学者通过Expander的源码学会一个基本的模板化控件应该如何构造。
###2.1 Attribute
Expander定义了三种Attribute:ContentProperty、TemplatePart和TemplateVisualState。
ContentProperty表明了主要属性为Content,并且在XAML中可以将Content属性用作直接内容,即将这种代码:
``` xml
```
简化成如下形式:
``` xml
```
因为Expander本来就继承自ContentControl,我很怀疑定义这个ContentProperty的必要性。(如果各位清楚这里这么做的原因请告知,谢谢。)
TemplatePart表明ControlTemplate中应该包含名为PART_ExpanderToggleButton的ToggleButton、名为PART_RootGrid的Grid及名为PART_LayoutTransformer的LayoutTransformControl。
TemplateVisualState表明ControlTempalte中应该包含名为ExpandedStates的VisualStateGroup,其中包含名为Expanded和Collapsed的两种VisualState。另外还有名为ExpandDirectionStates的VisualStateGroup,其中包含RightDirection、LeftDirection、UpDirection和DownDirection。
即使ControlTemplate中没按TemplatePart和TemplateVisualState的要求定义,Expander也不会报错,只是会缺失部分功能。
###2.2 Header与HeaderTemplate
PART_ExpanderToggleButton的Content和ContentTemplate通过TemplateBinding绑定到Expander的Header和HeaderTemplate,通过HeaderTemplate,Expander的Header外观可以有一定的灵活性。
###2.3 IsExpanded
Expander通过IsExpanded属性控制内容是否展开。注意这是个依赖属性,即这个属性也可以通过Binding控制。在改变IsExpanded值的同时会依次调用VisualStateManager.GoToState(this, StateContentExpanded, true);、 OnExpanded(EventArgs args) 、 Expanded 和VisualStateManager.GoToState(this, StateContentCollapsed, true);、 OnCollapsed 、 Collapsed 。
OnExpanded和OnCollapsed都是protected virtual 函数,可以在派生类中修改行为。
许多人实现Expander时不使用IsExpanded属性,而是通过public void Expand()和public void Collapse()直接控制内容展开和折叠,这种做法稍微缺乏灵活性。如PART_ExpanderToggleButton通过TwoWay Binding与IsExpanded属性关联,如果只提供public void Expand()和public void Collapse()则做不到这个功能。
``` xml
```
另一个常见的做法是通过代码直接控制内容是否显示,例如这样:PART_MainContent.Visibility = Visibility.Collapsed;。这样的坏处是不能在这个过程自定义动画效果或进行其它操作。Expander通过VisualStateManager实现这个功能,做到了UI和代码分离。
###2.4 OnApplyTemplate
模板化控件在加载ControlTemplate后会调用OnApplyTemplate(),Expander的OnApplyTemplate()实现了通常应有的实现,即订阅事件、改变VisualState。
``` cs
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (IsExpanded)
{
VisualStateManager.GoToState(this, StateContentExpanded, false);
}
else
{
VisualStateManager.GoToState(this, StateContentCollapsed, false);
}
var button = (ToggleButton)GetTemplateChild(ExpanderToggleButtonPart);
if (button != null)
{
button.KeyDown -= ExpanderToggleButtonPart_KeyDown;
button.KeyDown += ExpanderToggleButtonPart_KeyDown;
}
OnExpandDirectionChanged();
}
```
控件在加载ControlTemplate时就需要确定它的状态,一般这时候都不会使用过渡动画。所以这里VisualStateManager.GoToState(this, StateContentExpanded, false)的参数useTransitions使用了false。
由于Template可能多次加载(实际很少发生),或者不能正确获取TemplatePart,所以使用TemplatePart前应该先判断是否为空;如果要订阅事件,应该先取消订阅。
###2.5 Style
``` xml
....
```
如果忽略ExpandDirectionStates,Expander的Style就如以上所示十分简短(不过HeaderToggleButtonStyle有整整300行)。注意 Setter Property="IsTabStop" Value="False" 这句,对内容控件或复合控件,约定俗成都需要将IsTabStop设置成False,这是为了防止控件本身获得焦点。对Expander来说,在前一个控件上按“Tab”键,应该首先让PART_ExpanderToggleButton获得焦点。如果IsTabStop="true",Expander会获得焦点,需要再按一次“Tab”键才能让PART_ExpanderToggleButton获得焦点。
###2.6 partial class

即使代码量不大,Expander还是将代码分别存放在几个partial class中,这样做的好处是让承载主要业务的文件(Expander.cs)结构更加清晰。尤其是依赖属性,一个完整的依赖属性定义可以有20行(属性标识符、属性包装器、PropertyChangedCallback等),而且其中一部分是静态的,另外一部分不是,在类中将一个依赖属性的所有部分放在一起,还是按静态、非静态的顺序存放,这也可能引起争论。
###2.7 其它
虽然Expander是一个教科书式的控件,但还是有几个可以改进的地方。
最让人困扰的一点是Header居然是个String。WPF中的Expander的Header是个Object,可以方便地塞进各种东西,例如一个CheckBox或一张图片。虽然通过更改ControlTemplate或HeaderTemplate也不是不可以达到这效果,但毕竟麻烦了一些。不久前MenuItem就把Header从String类型改为Object了([Menu: changed MenuItem Header to type object](https://github.com/Microsoft/UWPCommunityToolkit/pull/1294)),说不定以后Expander也有可能这样修改( [Change Expander.Header from string to object](https://github.com/Microsoft/UWPCommunityToolkit/pull/1475) )。
另外,在WPF中Expander派生自HeaderedContentControl,这就少写了Header、HeaderTemplate、OnHeaderChanged等一大堆代码。而Community Toolkit中每个有Header属性的控件都各自重复了这些代码。或许将来会有[HeaderedContentControl](https://github.com/Microsoft/UWPCommunityToolkit/issues/995)这个控件吧。
PART_ExpanderToggleButton鼠标按下时Header和Content分裂的效果还挺奇怪的,这点在上一篇文章有提过( [浅谈按钮设计](http://www.cnblogs.com/dino623/p/ButtonDesign.html))。

最后,这年头连个折叠/展开动画都没有,而且还是微软出品,真是可惜([Improve Expander control (animation, color) ](https://github.com/Microsoft/UWPCommunityToolkit/issues/924))。还好XAML扩展性确实优秀,可以自己添加这些动画。
##3. 扩展
我简单地用Behavior为Expander添加了折叠/展开动画,代码如下:
``` cs
public class PercentageToHeightBehavior : Behavior
{
///
///
public FrameworkElement ContentElement
{
get { return (FrameworkElement)GetValue(ContentElementProperty); }
set { SetValue(ContentElementProperty, value); }
}
protected virtual void OnContentElementChanged(FrameworkElement oldValue, FrameworkElement newValue)
{
if (oldValue != null)
newValue.SizeChanged -= OnContentElementSizeChanged;
if (newValue != null)
newValue.SizeChanged += OnContentElementSizeChanged;
}
private void OnContentElementSizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateTargetHeight();
}
///
///
public double Percentage
{
get { return (double)GetValue(PercentageProperty); }
set { SetValue(PercentageProperty, value); }
}
protected virtual void OnPercentageChanged(double oldValue, double newValue)
{
UpdateTargetHeight();
}
public event PropertyChangedEventHandler PropertyChanged;
private void UpdateTargetHeight()
{
double height = 0;
if (ContentElement == null || ContentElement.ActualHeight == 0 || double.IsNaN(Percentage))
height = double.NaN;
else
height = ContentElement.ActualHeight * Percentage;
if (AssociatedObject != null)
AssociatedObject.Height = height;
}
}
```
``` xml
Collapsed
Visible
```
原理是把ContentPresenter放进一个StackPanel里,通过DoubleAnimation改变这个StackPanel的高度。之所以不直接改变ContentPresenter的高度是不想改变它的内容高度。另外我也改变了PART_ExpanderToggleButton的动画效果,我有点讨厌鼠标按下时文字会变模糊这点。运行效果如下:

##4. 结语
写这篇文章拖了很多时间,正好2.0版本也发布了( [Releases · Microsoft_UWPCommunityToolkit](https://github.com/Microsoft/UWPCommunityToolkit/releases) ),所以截图及源码有一些是不同版本的,但不影响主要内容。
如前言所说,这真的是个很好的入门级控件,很适合用于学习模板化控件。
##5. 参考
[Expander Control](http://uwpcommunitytoolkit.readthedocs.io/en/master/controls/Expander/)
[Microsoft.Toolkit.Uwp.UI.Controls.Expander](https://github.com/Microsoft/UWPCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/Expander)
##6. 源码
[GitHub - ExpanderDemo](https://github.com/DinoChan/ExpanderDemo)
因为是在v1.5.0上写的,可能需要修改才能使用到v2.0.0上。
[UWP]理解及扩展Expander的更多相关文章
- [UWP]理解ControlTemplate中的VisualTransition
1. 前言 VisualTransition是控件模板中的重要组成部分,无论是自定义控件或者修改控件样式都会接触到VisualTransition.明明这么重要,博客园上好像都没多少关于VisualT ...
- poj 2115 求线性同余方程 C Looooops(好理解欧几里德扩展定理怎么应用)
C Looooops Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 29061 Accepted: 8360 Descr ...
- 从Spring容器的角度理解Dubbo扩展点的加载时机
对于Dubbo提供的扩展点,主程序执行的过程中并没有显示调用加载的过程,无论是自激活的Filter还是自适应的ThreadPool.那么这样的扩展点在程序运行的哪个节点调用的呢?跟踪之前性能监控扩展点 ...
- 理解水平扩展和垂直扩展 (转载 http://yunjiechao-163-com.iteye.com/blog/2126981)
当一个开发人员提升计算机系统负荷时,通常会考虑两种方式垂直扩展和水平扩展.选用哪种策略主要依赖于要解决的问题 以及系统资源的限制.在这篇文章中我们将讲述这两种策略并讨论每种策越的优缺点.如果你已经 ...
- git命令的理解与扩展
Git的模式如图: Workspace:工作区 Index / Stage:暂存区 Repository:仓库区(或本地仓库) Repository:仓库区(或本地仓库) 一.新建代码库 # 查看gi ...
- 从ExtensionLoader理解Dubbo扩展机制
Dubbo的扩展机制是怎么实现的?最简单的回答就是@SPI. Dubbo的插件化思路来源于Java SPI. JAVA SPI 机制 SPI的全名为Service Provider Int ...
- [UWP]创建一个进度按钮
1. 前言 最近想要一个进度按钮. 传统上UWP上处理进度可以这样实现,首先是XAML,包括一个ProgressBar和一个按钮: <StackPanel Orientation="H ...
- UWP 扩展/自定义标题栏的方法,一些概念和一些注意事项
原文 UWP 扩展/自定义标题栏的方法,一些概念和一些注意事项 在 Windows 10 的前几个版本中将页面内容扩展到标题栏上还算简单,主要是没什么坑.直到一些新控件的引入和一些外观设计趋势变化之后 ...
- 理解RESTful架构
越来越多的人开始意识到,网站即软件,而且是一种新型的软件. 这种"互联网软件"采用客户端/服务器模式,建立在分布式体系上,通过互联网通信,具有高延时(high latency).高 ...
随机推荐
- 前端数据存储方案集合(cookie localStorage等)以及详解 (二)
前端数据存储方案集合(cookie localStorage等)以及详解 (二) 在之前的文章中已经介绍到了 前端存储方案中的 cookie . 但是 cookie 的存储上限是 4KB. 如果超过了 ...
- C++ STL 双端队列deque详解
一.解释 Deque(双端队列)是一种具有队列和栈的性质的数据结构.双端队列的元素可以从两端弹出,其限定插入和删除操作在表的两端进行. 二.常用操作: 1.头文件 #include <deque ...
- Unicode 与 Unicode Transformation Format(UTF,UTF-8 / UTF-16 / UTF-32)
ASCII(American Standard Code for Information Interchange):早期它使用7 bits来表示一个字符,总共表示27 = 128个字符:后来扩展到8 ...
- OC-UICollectionView实现瀑布流
UICollectionView实现瀑布流 在iOS中可以实现瀑布流的目前已知的有2种方案: 使用UIScrollView自己封装一套,这种方案是应用于iOS6之前的,因为iOS6才出来UIColle ...
- Java过滤器处理Ajax请求,Java拦截器处理Ajax请求,拦截器Ajax请求
Java过滤器处理Ajax请求,Java拦截器处理Ajax请求,拦截器Ajax请求 >>>>>>>>>>>>>>&g ...
- [STL] day 1~2 Problem Set
Q#1 #include <cmath> #include <cstdio> #include <vector> #include <iostream> ...
- Android利用canvas画画板
首先新建一个项目工程,建立文件,如下图所示
- 报错:No identifier specified for entity: main.java.com.sy.entity.User的解决办法
自己也没怎么搭建过框架,更何况还是spring mvc的,最近在带两个实习生,正好教他们怎么搭建一个spring mvc的框架,然而我在映射表的时候,提示报错了. 实体基类: public class ...
- 开心的金明 NOIP 2006 普及组
题目描述 金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间他自己专用的很宽敞的房间.更让他高兴的是,妈妈昨天对他说:"你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过N元钱就 ...
- ASP.NET Core身份验证服务框架IdentityServer4-整体介绍
一.整体情况 现代应用程序看起来更像这个: 最常见的相互作用: 浏览器与Web应用程序的通信 Browser -> Web App Web应用程序与Web API通信 基于浏览器的应用程序与We ...