##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)ExpandedVisualStateManager.GoToState(this, StateContentCollapsed, true);OnCollapsedCollapsed
OnExpandedOnCollapsed都是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
![](http://images2017.cnblogs.com/blog/38937/201709/38937-20170905162416601-1518982570.png)

即使代码量不大,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))。
![](http://images2017.cnblogs.com/blog/38937/201709/38937-20170905162431476-343920582.gif)

最后,这年头连个折叠/展开动画都没有,而且还是微软出品,真是可惜([Improve Expander control (animation, color) ](https://github.com/Microsoft/UWPCommunityToolkit/issues/924))。还好XAML扩展性确实优秀,可以自己添加这些动画。

##3. 扩展
我简单地用Behavior为Expander添加了折叠/展开动画,代码如下:
``` cs
public class PercentageToHeightBehavior : Behavior
{
///

/// 获取或设置ContentElement的值
///

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();
}

///

/// 获取或设置Percentage的值
///

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的动画效果,我有点讨厌鼠标按下时文字会变模糊这点。运行效果如下:
![](http://images2017.cnblogs.com/blog/38937/201709/38937-20170905162507882-1146498867.gif)

##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的更多相关文章

  1. [UWP]理解ControlTemplate中的VisualTransition

    1. 前言 VisualTransition是控件模板中的重要组成部分,无论是自定义控件或者修改控件样式都会接触到VisualTransition.明明这么重要,博客园上好像都没多少关于VisualT ...

  2. poj 2115 求线性同余方程 C Looooops(好理解欧几里德扩展定理怎么应用)

    C Looooops Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 29061   Accepted: 8360 Descr ...

  3. 从Spring容器的角度理解Dubbo扩展点的加载时机

    对于Dubbo提供的扩展点,主程序执行的过程中并没有显示调用加载的过程,无论是自激活的Filter还是自适应的ThreadPool.那么这样的扩展点在程序运行的哪个节点调用的呢?跟踪之前性能监控扩展点 ...

  4. 理解水平扩展和垂直扩展 (转载 http://yunjiechao-163-com.iteye.com/blog/2126981)

      当一个开发人员提升计算机系统负荷时,通常会考虑两种方式垂直扩展和水平扩展.选用哪种策略主要依赖于要解决的问题 以及系统资源的限制.在这篇文章中我们将讲述这两种策略并讨论每种策越的优缺点.如果你已经 ...

  5. git命令的理解与扩展

    Git的模式如图: Workspace:工作区 Index / Stage:暂存区 Repository:仓库区(或本地仓库) Repository:仓库区(或本地仓库) 一.新建代码库 # 查看gi ...

  6. 从ExtensionLoader理解Dubbo扩展机制

    Dubbo的扩展机制是怎么实现的?最简单的回答就是@SPI. Dubbo的插件化思路来源于Java SPI.   JAVA SPI 机制     SPI的全名为Service Provider Int ...

  7. [UWP]创建一个进度按钮

    1. 前言 最近想要一个进度按钮. 传统上UWP上处理进度可以这样实现,首先是XAML,包括一个ProgressBar和一个按钮: <StackPanel Orientation="H ...

  8. UWP 扩展/自定义标题栏的方法,一些概念和一些注意事项

    原文 UWP 扩展/自定义标题栏的方法,一些概念和一些注意事项 在 Windows 10 的前几个版本中将页面内容扩展到标题栏上还算简单,主要是没什么坑.直到一些新控件的引入和一些外观设计趋势变化之后 ...

  9. 理解RESTful架构

    越来越多的人开始意识到,网站即软件,而且是一种新型的软件. 这种"互联网软件"采用客户端/服务器模式,建立在分布式体系上,通过互联网通信,具有高延时(high latency).高 ...

随机推荐

  1. jquery实现多条件筛选特效代码分享

    本文实例讲述了jquery实现多条件筛选特效.分享给大家供大家参考.具体如下:jquery实现的多条件搜索表单带日期选择表格表单效果源码,是一段实现了多个条件筛选搜索的特效代码,多条件拥有时间.地点. ...

  2. jquery.imagezoom.js制作鼠标悬停图片放大镜特效、参数和最简教程

    一.插件介绍 今天在用到放大镜效果的时候,突然发现网站里没有放大镜的插件.于是总结了一下,放到这里.为自己,也为他人提供方便.jquery.imagezoom.js这款插件用途很简单,就是鼠标移过去, ...

  3. 再谈CVE-2017-7047 Triple_Fetch和iOS 10.3.2沙盒逃逸

    作者:蒸米 ----------------- 0x00 序 Ian Beer@google发布了CVE-2017-7047Triple_Fetch的exp和writeup[1],chenliang@ ...

  4. 如何两周达到150行Java程序的能力--part 2

    第一次课训练营课程打通了有C语言编程通往面向对象编程的道路,然而道路依然会曲折.下图是第二次训练营课程的训练大纲,从第二次开始,每次课首先进行测试. 针对作业1的训练要求,明确定义了13个具体的测试点 ...

  5. HTML 贝塞尔曲线

    1.二次贝塞尔曲线 <canvas id="myCanvas" width="300" height="150" style=&quo ...

  6. python基础===Python性能优化的20条建议

    优化算法时间复杂度 算法的时间复杂度对程序的执行效率影响最大,在Python中可以通过选择合适的数据结构来优化时间复杂度,如list和set查找某一个元素的时间复杂度分别是O(n)和O(1).不同的场 ...

  7. 甲方VS乙方

    甲方与乙方,在很多人都存在有误解,不知道究竟如何辨别.这里我提一些简单的辨别方式:甲方一般是指提出目标的一方,在合同拟订过程中主要是提出要实现什么目标,乙方一般是指完成目标,在合同中主要是提出如何保证 ...

  8. Cache替换算法:LRU与LFU的区别

    LFU(Least Frequently Used)最近最少使用算法.它是基于“如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小”的思路.LRU(Least Rece ...

  9. html5/css3布局(一)

    响应式布局 1.响应式布局介绍 响应式布局可以为不同终端的用户提供更加舒适的界面和更好的用户体验,就是一个网页可以在不同设备上显示,比如:电脑.平板.手机等,不同设备都可以兼容显示.这样就不必为每一种 ...

  10. 中点Bresenham画圆

    这里不仔细讲原理,只是把我写的算法发出来,跟大家分享下,如果有错误的话,还请大家告诉我,如果写的不好,也请指出来,一起讨论进步. 算法步骤: (1) 输入圆的半径R. (2) 计算初始值d = 1 - ...