[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).高 ...
随机推荐
- Python网络数据采集3-数据存到CSV以及MySql
Python网络数据采集3-数据存到CSV以及MySql 先热热身,下载某个页面的所有图片. import requests from bs4 import BeautifulSoup headers ...
- 52. leetcode 96. Unique Binary Search Trees
96. Unique Binary Search Trees Given n, how many structurally unique BST's (binary search trees) tha ...
- 关于戴尔没有活动分区,遇到了“Windows安装程序无法将windows配置为在此计算机的硬件上运行”提示等
虽然只有几句话,但是还是超级好用的 装过很多戴尔的笔记本,发现很多都是这里的问题导致系统装不进去. 1.硬盘设置有问题.进BIOS ,到SATA 设置看看硬盘是不是设置为AHCI了.这个ghost系统 ...
- Hadoop 新生报道(三) hadoop基础概念
一.NameNode,SeconderyNamenode,DataNode NameNode,DataNode,SeconderyNamenode都是进程,运行在节点上. 1.NameNode:had ...
- C#去掉JSON字符串中的最后一个数字
这个问题总结起来就是去掉字符串中的最后一个"," 字符串:string s = "1,2,3,4,5," 目标:删除最后一个 "," 方法: ...
- NYOJ--1236--挑战密室(第八届河南省程序设计大赛)
挑战密室 时间限制:1000 ms | 内存限制:65535 KB 难度:4 描述 R组织的特工Dr. Kong 为了寻找丢失的超体元素,不幸陷入WTO密室.Dr. Kong必须尽快找到解锁密 ...
- Vue模板逻辑
前面的话 上一篇介绍了Vue的模板内容,而对于一般的模板引擎来说,除了模板内容,还包括模板逻辑.常用的模板逻辑包括条件和循环.本文将详细介绍Vue模板逻辑 条件渲染 在Vue中,实现条件逻辑依靠条件指 ...
- 【我的漫漫跨考路】有生之年·调完了BUG--冒泡排序C++版本
正文之前 今天去牛客网试了试一些实战编程题,感觉贼有意思,但是也很难,挑了个成绩排序的算法题我就开始怼! 对我一个编程经验并不是很丰富的人来说,确实算是个挑战了. 所以我满满当当的搞了四个小时多,才算 ...
- Spring Boot中使用 Spring Security 构建权限系统
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中配置的Bean,为应用系统提供声明式的安全 ...
- python中的赋值和深浅拷贝
python中,A object = B object 是一种赋值操作,赋的值不是一个对象在内存中的空间,而只是这个对象在内存中的位置 . 此时当B对象里面的内容发生更改的时候,A对象也自然而然的 ...