1. 前言

HeaderedContentControl是WPF中就存在的控件,这个控件的功能很简单:提供Header和Content两个属性,在UI上创建两个ContentPresenter并分别绑定到Header和Content,让这两个ContentPresenter合体组成HeaderedContentControl。

2. 以前的问题

在WPF中,HeaderedContentControl是Expander、GroupBox、TabItem等诸多拥有Header属性的控件的基类,虽然很少直接用这个控件,它的存在也有一定价值。不过在WPF中它的价值也仅此而已,由开发者自己实现也极其容易,以至于后来在Silverlight中就没有提供这个控件(后来放到了Silverlight Toolkit这个扩展里)。

UWP中几乎所有的表单控件都有Header属性,如TextBox、ComboBox等,这么看起来HeaderedContentControl更加重要了,但UWP反而没有提供HeaderedContentControl这个控件。每个有Header属性的控件都既没有继承HeaderedContentControl,也没有使用HeaderedContentControl作为外层容器包装自己的内容,而是全都单独实现这个属性。其实这也可以理解,毕竟不是所有控件都是ContentControl,而且使用HeaderedContentControl作为外层容器会导致VisualTree多了一层,变得复杂而且影响性能。其实现在很少会有一个页面出现十分多表单控件的情况,这点性能损失我是不介意的。

UWP CommunityToolkit中也有一些控件包含Header属性,如HeaderedTextBlock和Expander,CommunityToolkit也没有为它们创建一个HeaderedContentControl,而且和TextBox等控件不同,UWP CommunityToolkit中的Header属性都是string类型,真是任性。

GitHub上也有过添加HeaderedContentControl的意见,其实我是很支持这件事的,毕竟HeaderedContentControl可不只是多了一个Header属性而已。可是微软一直拖到 UWPCommunityToolkit Release v2.1.0 发布才终于肯提供这个控件。

3. 现在的问题

虽然终于~终于等到了HeaderedContentControl,但让人高兴不起来,而且现在连HeaderedTextBlock和Expander都不使用这个HeaderedContentControl。微软第一次在UWP提供了HeaderedContentControl,有了一个Object类型的Header属性,两件事本应该为开发者提供更多的方便,但是,为什么会变成这样呢。

刚开始,HeaderedContentControl的Default Style是这样的:

<Style TargetType="controls:HeaderedContentControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:HeaderedContentControl">
<StackPanel>
<ContentPresenter Content="{TemplateBinding Header}" ContentTemplate="{TemplateBinding HeaderTemplate}"/>
<ContentPresenter/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

真是让人扫兴。

毕竟这是照抄WPF的,也不能说它不对,但同样地这就把WPF的遗留问题完全保留下来了:因为使用了StackPanel,所以VerticalContentAlignment无论怎么设置都是无效的,Content都是直接趴在Header下面,两个ContentPresenter总是腻在一起:

<Grid Background="#FF017DB3"
Padding="10">
<controls:HeaderedContentControl Header="Header"
Foreground="White"
Content="正确的垂直居中"
VerticalContentAlignment="Center" />
</Grid>
<Grid Grid.Column="1"
Padding="10"
Background="#FFBB310A">
<controls:HeaderedContentControl Header="Header"
Foreground="White"
Content="错误的垂直居中"
VerticalContentAlignment="Center"
Style="{StaticResource WPFStyle}" />
</Grid>

这样的合体姿势明显不对,事实上在WPF中继承HeaderedContentControl的控件(如Expander和GroupBox)都在ControlTempalte中使用了Grid或DockPanel,而不是StackPanel,HeaderedContentControl使用StackPanel本身就是个错误。好在UWP CommunityToolkit

2.1正式添加HeaderedContentControl时Default Style修改为了使用Grid,总算解决了这个历史遗留问题:

<Style TargetType="controls:HeaderedContentControl">
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="VerticalContentAlignment" Value="Top"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:HeaderedContentControl">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ContentPresenter Content="{TemplateBinding Header}" ContentTemplate="{TemplateBinding HeaderTemplate}"/>
<ContentPresenter Grid.Row="1" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

另一个问题是Header与Content之间的Margin。仔细观察就会发现TextBox等控件的Header是有一个0,0,0,8的Margin,可是HeaderedContentControl并没有这样设置,结果HeaderedContentControl就会出现高度不匹配的问题:

<StackPanel Width="200"
Margin="10,0">
<TextBox Header="TextBox" />

</StackPanel>
<StackPanel Width="200"
Margin="10,0"
Grid.Column="1">
<controls:HeaderedContentControl Header="TextBox"
HorizontalContentAlignment="Stretch">
<TextBox />
</controls:HeaderedContentControl>

</StackPanel>

不仅如此,TextBox在Disabled状态下Header会变成灰色,但HeaderedContentControl明显漏了这个VisualState,结果如下图所示,这个如果也要自己实现就很麻烦了。

以前微软迟迟不肯提供HeaderedContentControl,现在一出手就是半成品,我很怀疑微软这样做是为了考验我们这些还在坚持UWP的纯真开发者。

4. 自己实现有一个HeaderedContentControl

与其留着这个半成品祸害自己的代码,还不如干脆动手实现一个HeaderedContentControl。在以前已写过一次实现HeaderedContentControl的文章,但那篇主要是为了讲解模板化控件,没有完整的功能。这次要做得完善些。

4.1 基本外观

<Style TargetType="local:HeaderedContentControl">
<Setter Property="FontFamily"
Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize"
Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="Foreground"
Value="{ThemeResource SystemControlForegroundBaseHighBrush}" />
<Setter Property="HorizontalContentAlignment"
Value="Stretch" />
<Setter Property="VerticalContentAlignment"
Value="Stretch" />
<Setter Property="IsTabStop"
Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:HeaderedContentControl">
<Grid>



<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentPresenter x:Name="HeaderContentPresenter"
x:DeferLoadStrategy="Lazy"
Visibility="Collapsed"
Margin="0,0,0,8"
Foreground="{ThemeResource SystemControlForegroundBaseHighBrush}"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
FontWeight="Normal" />
<ContentPresenter Grid.Row="1"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Margin="{TemplateBinding Padding}"
ContentTransitions="{TemplateBinding ContentTransitions}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

包含Header和HeaderTemplate这两个属性和CommunityToolkit中的HeaderedContentControl一样,ControlTemplate中使用了Grid作为容器这点也一样,改变的主要有以下几点:

  • Margin、ContentTransitions等属性有按照标准做法好好做了绑定。
  • HorizontalContentAlignment和VerticalContentAlignment也从Left和Top改为Stretch,毕竟很多时候使用ContentPresenter 都要把这两个属性改为Stretch,还不如一开始就这样做。
  • 别忘了IsTabStop要设置为False,这点以前在UI指南里有介绍过原因,这里不再赘述。

4.2 Disabled状态

<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HeaderContentPresenter"
Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SystemControlDisabledBaseMediumLowBrush}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Normal" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
protected virtual void UpdateVisualState(bool useTransitions)
{
VisualStateManager.GoToState(this, IsEnabled ? NormalName : DisabledName, useTransitions);
}

ControlTemplate中需要包办Disabled状态,HeaderedContentControl中订阅自身的IsEnabledChanged事件,根据IsEnabled的值转换状态。

4.3 隐藏HeaderContentPresenter

private void UpdateVisibility()
{
if (_headerContentPresenter != null)
_headerContentPresenter.Visibility = _headerContentPresenter.Content == null ? Visibility.Collapsed : Visibility.Visible;
}

OnApplyTemplate()OnHeaderChanged(object oldValue, object newValue)函数中调用UpdateVisibility()以决定HeaderContentPresenter是否显示。这个功能,以及HeaderContentPresenter的Margin,HeaderedTextBlock都是有的,但偏偏就没做到隔壁的HeaderedContentControl,真是够了。

4.4 处理HeaderContentPresenter的点击事件

protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_headerContentPresenter = GetTemplateChild(HeaderContentPresenterName) as ContentPresenter;
UpdateVisibility();
UpdateVisualState(false); if (_headerContentPresenter != null)
{
_headerContentPresenter.PointerReleased += OnHeaderContentPresenterPointerReleased;
_headerContentPresenter.PointerPressed += OnHeaderContentPresenterPointerPressed1;
}
} private void OnHeaderContentPresenterPointerPressed1(object sender, PointerRoutedEventArgs e)
{
if (Content is Control control)
control.Focus(FocusState.Programmatic);
} private void OnHeaderContentPresenterPointerReleased(object sender, PointerRoutedEventArgs e)
{
e.Handled = true;
}

在TextBox上点击它的Header,输入框将会获得焦点,上述代码就是实现这个功能。

这个功能我不是十分确定,至少目前看来这个行为是正确的。

5. 结语

HeaderedContentControl 明明只是个很简单的控件,明明只是个很简单的控件,明明只是个很简单的控件。

附上完整的代码:

[TemplateVisualState(Name = NormalName, GroupName = CommonStatesName)]
[TemplateVisualState(Name = DisabledName, GroupName = CommonStatesName)]
[TemplatePart(Name = HeaderContentPresenterName, Type = typeof(ContentPresenter))]
public class HeaderedContentControl : ContentControl
{
private const string CommonStatesName = "CommonStates";
private const string NormalName = "Normal";
private const string DisabledName = "Disabled";
private const string HeaderContentPresenterName = "HeaderContentPresenter"; /// <summary>
/// 标识 Header 依赖属性。
/// </summary>
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.Register("Header", typeof(object), typeof(HeaderedContentControl), new PropertyMetadata(null, OnHeaderChanged)); /// <summary>
/// 标识 HeaderTemplate 依赖属性。
/// </summary>
public static readonly DependencyProperty HeaderTemplateProperty =
DependencyProperty.Register("HeaderTemplate", typeof(DataTemplate), typeof(HeaderedContentControl), new PropertyMetadata(null, OnHeaderTemplateChanged)); private ContentPresenter _headerContentPresenter; public HeaderedContentControl()
{
DefaultStyleKey = typeof(HeaderedContentControl);
IsEnabledChanged += OnPickerIsEnabledChanged;
} /// <summary>
/// 获取或设置Header的值
/// </summary>
public object Header
{
get => GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
} /// <summary>
/// 获取或设置HeaderTemplate的值
/// </summary>
public DataTemplate HeaderTemplate
{
get => (DataTemplate) GetValue(HeaderTemplateProperty);
set => SetValue(HeaderTemplateProperty, value);
} private static void OnHeaderChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var target = obj as HeaderedContentControl;
var oldValue = args.OldValue;
var newValue = args.NewValue;
if (oldValue != newValue)
target.OnHeaderChanged(oldValue, newValue);
} private static void OnHeaderTemplateChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var target = obj as HeaderedContentControl;
var oldValue = (DataTemplate) args.OldValue;
var newValue = (DataTemplate) args.NewValue;
if (oldValue != newValue)
target.OnHeaderTemplateChanged(oldValue, newValue);
} protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_headerContentPresenter = GetTemplateChild(HeaderContentPresenterName) as ContentPresenter;
UpdateVisibility();
UpdateVisualState(false); if (_headerContentPresenter != null)
{
_headerContentPresenter.PointerReleased += OnHeaderContentPresenterPointerReleased;
_headerContentPresenter.PointerPressed += OnHeaderContentPresenterPointerPressed1;
}
} protected virtual void OnHeaderChanged(object oldValue, object newValue)
{
UpdateVisibility();
} protected virtual void OnHeaderTemplateChanged(DataTemplate oldValue, DataTemplate newValue)
{
} protected virtual void UpdateVisualState(bool useTransitions)
{
VisualStateManager.GoToState(this, IsEnabled ? NormalName : DisabledName, useTransitions);
} private void UpdateVisibility()
{
if (_headerContentPresenter != null)
_headerContentPresenter.Visibility = _headerContentPresenter.Content == null ? Visibility.Collapsed : Visibility.Visible;
} private void OnPickerIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
UpdateVisualState(true);
} private void OnHeaderContentPresenterPointerPressed1(object sender, PointerRoutedEventArgs e)
{
if (Content is Control control)
control.Focus(FocusState.Programmatic);
} private void OnHeaderContentPresenterPointerReleased(object sender, PointerRoutedEventArgs e)
{
e.Handled = true;
}
}

6. 参考

HeaderedContentControl

HeaderedContentControl XAML Control

7. 源码

PickerTest

[UWP]合体姿势不对的HeaderedContentControl的更多相关文章

  1. 清缓存的姿势不对,真的会出生产bug哦

    最近解决了一个生产bug,bug的原因很简单,就是清理缓存的方式不对.本来没啥好说的,但是考虑到我们有时候确实会在一些小问题上栽跟头,最终决定把这个小故事拿出来跟大家分享下. 风起有一天在撸代码,突然 ...

  2. mybatis 逆向工程使用姿势不对,把表清空了,心里慌的一比,于是写了个插件。

    使用mybatis逆向工程的时候,delete方法的使用姿势不对,导致表被清空了,在生产上一刷新后发现表里没数据了,一股凉意从脚板心直冲天灵盖. 于是开发了一个拦截器,并写下这篇文章记录并分享. 这锅 ...

  3. EF批量插入太慢?那是你的姿势不对

    大概所有的程序员应该都接触过批量插入的场景,我也相信任何的程序员都能写出可正常运行的批量插入的代码.但怎样实现一个高效.快速插入的批量插入功能呢? 由于每个人的工作履历,工作年限的不同,在实现这样的一 ...

  4. 用Fiddler抓不到https的包?因为你姿势不对!往这看!

    前言 刚入行测试的小伙伴可能不知道,Fiddler默认抓http的包,如果要抓https的包,是需要装证书的!什么鬼证书?不明白的话继续往下看. Fiddler 抓取 https 数据 第一步:下载 ...

  5. 深入探讨List<>中的一个姿势。

    List<>是c#中很常见的一种集合形式,近期在阅读c#源码时,发现了一个很有意思的定义: [DebuggerTypeProxy(typeof(Mscorlib_CollectionDeb ...

  6. 使用 Java8 Optional 的正确姿势(转)

    我们知道 Java 8 增加了一些很有用的 API, 其中一个就是 Optional. 如果对它不稍假探索, 只是轻描淡写的认为它可以优雅的解决 NullPointException 的问题, 于是代 ...

  7. int转换char的正确姿势

    一:背景 在一个项目中,我需要修改一个全部由数字(0~9)组成的字符串的特定位置的特定数字,我采用的方式是先将字符串转换成字符数组,然后利用数组的位置来修改对应位置的值.代码开发完成之后,发现有乱码出 ...

  8. Pycharm上python和unittest两种姿势傻傻分不清楚

    前言 经常有人在群里反馈,明明代码一样的啊,为什么别人的能出报告,我的出不了报告:为什么别人运行结果跟我的不一样啊... 这种问题先检查代码,确定是一样的,那就是运行姿势不对了,一旦导入unittes ...

  9. 180730-Spring之RequestBody的使用姿势小结

    Spring之RequestBody的使用姿势小结 SpringMVC中处理请求参数有好几种不同的方式,如我们常见的下面几种 根据 HttpServletRequest 对象获取 根据 @PathVa ...

随机推荐

  1. Progressive Web Applications

    Progressive Web Applications take advantage of new technologies to bring the best of mobile sites an ...

  2. cs231n spring 2017 lecture7 Training Neural Networks II 听课笔记

    1. 优化: 1.1 随机梯度下降法(Stochasitc Gradient Decent, SGD)的问题: 1)对于condition number(Hessian矩阵最大和最小的奇异值的比值)很 ...

  3. Codeforces 839A Arya and Bran【暴力】

    A. Arya and Bran time limit per test:1 second memory limit per test:256 megabytes input:standard inp ...

  4. UVa 10341 - Solve It【经典二分,单调性求解】

    原题: Solve the equation:         p*e-x + q*sin(x) + r*cos(x) + s*tan(x) + t*x2 + u = 0         where  ...

  5. Codeforces Round #300(Div. 2)-538A.str.substr 538B.不会 538C.不会 。。。

    A. Cutting Banner time limit per test 2 seconds memory limit per test 256 megabytes input standard i ...

  6. Windows下MYSQL读取文件为NULL

    只记录解决问题的方法. mysql 版本: 5.7.18 问题: 在执行mysql 函数load_file时,该函数将加载指定文件的内容,存储至相应字段.如: SELECT LOAD_FILE(&qu ...

  7. PHP性能分析工具xhprof的安装使用与注意事项

    前言 xhprof由facebook开源出来的一个PHP性能监控工具,占用资源很少,甚至能够在生产环境中进行部署. 它可以结合graphviz使用,能够以图片的形式很直观的展示代码执行耗时. 下面主要 ...

  8. Switch 语句

    如果您希望有选择地执行若干代码块之一,请使用 Switch 语句. 使用 Switch 语句可以避免冗长的 if..elseif..else 代码块. 语法 工作原理: 对表达式(通常是变量)进行一次 ...

  9. ubuntu-apache下隐藏thinkphp入口文件index.php

    按照thinkphp手册中来讲,apache服务器下,隐藏thinkphp入口文件有3步: httpd.conf配置文件中加载了mod_rewrite.so模块 AllowOverride None ...

  10. 豹哥嵌入式讲堂:ARM Cortex-M开发之文件详解(8)- 镜像文件(.bin/.hex/.s19)

    大家好,我是豹哥,猎豹的豹,犀利哥的哥.今天豹哥给大家讲的是嵌入式开发里的image文件(.bin, .hex, .s19). 今天这节课是豹哥<ARM Cortex-M开发之文件详解>主 ...