WPF 短 TimeSpan 自定义控件






4.75/5 (5投票s)
使用一个旋转框自定义控件的WPF短时间间隔自定义控件。
引言
在我之前的两篇文章中,我首先描述了 一个简单的短时间间隔用户控件,它使用滑块来选择TimeSpan
的小时、分钟和秒。在我的第二篇文章中,我描述了 一个自定义旋转框控件,可以用来替换滑块。
在这第三篇文章中,我将更新TimeSpan
控件以使用SpinnerControl
,并将TimeSpan
控件从UserControl
改为自定义控件(特别是为了能够应用自定义主题)。我称之为ShortTimeSpanControl
,因为它只表示从00:00:00到23:59:59的正面TimeSpan
。完整的TimeSpan
在此处描述。
TimeSpan
值可以表示为[-]d.hh:mm:ss.ff,其中可选的负号表示负时间间隔,d分量是天,hh是24小时制的小时,mm是分钟,ss是秒,ff是秒的小数部分。也就是说,时间间隔由正数或负数天组成,不带时间,或者由天数和时间组成,或者只有时间。
使用提供的通用主题,这是一个选择控件,用户可以选择一个TimeSpan
。由于它是一个自定义控件,您可以应用任何您喜欢的自定义主题,并且,也许,可以将其设置为一个只读控件(即交互式的),通过数据绑定更新其Value
。
要求
我们希望该控件看起来像这样
我们有三个旋转框控件,分别表示0..23小时、0..59分钟和0..59秒。
该控件应该
- 返回并接受一个
TimeSpan
Value
。 - 保持
TimeSpan
在边界内。 - 当
Value
改变时引发一个事件。
数据绑定和不要重复自己
我们需要考虑如何从SpinnerControl
获取值以更新ShortTimeSpanControl
中的值,反之亦然。在上一篇文章的旋转框控件中,我们设置了
private static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(decimal), typeof(SpinnerControl),
new FrameworkPropertyMetadata(DefaultValue,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnValueChanged,
CoerceValue
));
这样Value
属性默认是双向绑定的。
在我们的ShortTimeSpanControl
中,我们创建以下具有双向绑定的依赖属性:Hours
、Minutes
和Seconds
。然后,通过简单地使用XAML数据绑定,我们可以将三个SpinnerControl
绑定到它们各自代表的属性,框架会为我们处理绑定和更新通知。
然而,这意味着我们不仅有一个表示控件底层值的TimeSpan
,还有一个小时、分钟和秒的副本,这有点尴尬,因为我们存储的TimeSpan
值没有单一事实来源。
然而,只要我们保持所有数据同步,我们就不会有问题。为了做到这一点,我们只需要检查一下,如果我们设置了任何属性为新值,它是否与现有值不匹配,如果匹配,则不调用其他属性的setter。
例如:当Value
改变时,我们需要更新Hours
、Minutes
和Seconds
属性
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
var control = d as ShortTimeSpanControl;
if (d == null)
return;
var oldValue = (TimeSpan)args.OldValue;
var newValue = (TimeSpan)args.NewValue;
// ensure we don't get into a loop with the 4 properties changing
// by only changing the value if it has changed.
if (oldValue != newValue)
{
control.SetValue(HoursProperty, (object)newValue.Hours);
control.SetValue(MinutesProperty, (object)newValue.Minutes);
control.SetValue(SecondsProperty, (object)newValue.Seconds);
}
var e = new RoutedPropertyChangedEventArgs<TimeSpan>(oldValue, newValue, ValueChangedEvent);
control.OnValueChanged(e);
}
数据绑定和旋转框控件的CoerceValueCallback
在我将旋转框控件添加到时间间隔控件通用主题时,我发现了一些奇怪的行为:问题是我的时间间隔控件中的Value
依赖属性超出了它的边界,但这只在XAML数据绑定到旋转框控件时发生。我的旋转框控件中的递减命令是
protected void OnDecrease()
{
Value -= Change;
}
这导致了超/欠限,而本应阻止这种情况的CoerceValueCallback
是
private static decimal LimitValueByBounds(decimal newValue, SpinnerControl control)
{
newValue = Math.Max(control.Minimum, Math.Min(control.Maximum, newValue));
// then ensure the number of decimal places is correct.
newValue = Decimal.Round(newValue, control.DecimalPlaces);
return newValue;
}
private static object CoerceValue(DependencyObject obj, object value)
{
decimal newValue = (decimal)value;
SpinnerControl control = obj as SpinnerControl;
if (control != null)
{
// ensure that the value stays within the bounds of the minimum and
// maximum values that we define.
newValue = LimitValueByBounds(newValue, control);
}
return newValue;
}
似乎数据绑定更新首先发生,由WPF框架执行(当它‘隐式’调用依赖属性的SetValue
时),允许欠限,然后在数据绑定更新之后调用CoerceValueCallback
。事实证明,这在网上已经有很多地方讨论过了,例如这里,但MS Connect网站上的这个项目描述了这个问题以及行为背后的原因。
我尝试用Slider
替换SpinnerControl
,发现Slider
表现正常,这表明问题实际上与WPF无关,而是与我们解决问题的方式有关。在我的例子中,解决方案是在引起问题的OnDecrease
和OnIncrease
命令中也限制边界
protected void OnDecrease()
{
Value = LimitValueByBounds(Value - Change, this);
}
通过添加这个简单的修复,我们防止Value
低于我们的最小值。
事件
由于我们有四个属性,并且每个属性都可以更改,因此创建四个相应的事件是合理的
这些只是典型的样板自定义控件事件实现。
结论
关于这个控件没有太多要说的了。这基本上就是创建四个DependencyProperty
字段和四个相应的事件。所有的验证工作都委托给了底层的SpinnerControl
。我们唯一需要做的额外工作是,由于我们在两个地方存储了TimeSpan
(Value
以及Hours
、Minutes
、Seconds
),我们确保数据在所有属性中都得到同等更新。
正如开头提到的,这个控件只用于表示一个简短且定义明确的TimeSpan
,范围从00:00:00到23:59:59。当然,这个控件可以扩展以表示一个完整的TimeSpan
。
请在下方留下您的评论或建议,如果觉得这篇文章有帮助,请不要忘记投票。谢谢!