65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 短 TimeSpan 自定义控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (5投票s)

2012年1月19日

CPOL

4分钟阅读

viewsIcon

41510

downloadIcon

1403

使用一个旋转框自定义控件的WPF短时间间隔自定义控件。

timespan control

引言

在我之前的两篇文章中,我首先描述了 一个简单的短时间间隔用户控件,它使用滑块来选择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

要求

我们希望该控件看起来像这样

timespan control with generic theme

我们有三个旋转框控件,分别表示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中,我们创建以下具有双向绑定的依赖属性:HoursMinutesSeconds。然后,通过简单地使用XAML数据绑定,我们可以将三个SpinnerControl绑定到它们各自代表的属性,框架会为我们处理绑定和更新通知。

然而,这意味着我们不仅有一个表示控件底层值的TimeSpan,还有一个小时、分钟和秒的副本,这有点尴尬,因为我们存储的TimeSpan值没有单一事实来源

然而,只要我们保持所有数据同步,我们就不会有问题。为了做到这一点,我们只需要检查一下,如果我们设置了任何属性为新值,它是否与现有值不匹配,如果匹配,则不调用其他属性的setter。

例如:当Value改变时,我们需要更新HoursMinutesSeconds属性

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无关,而是与我们解决问题的方式有关。在我的例子中,解决方案是在引起问题的OnDecreaseOnIncrease命令中也限制边界

protected void OnDecrease()
{
    Value = LimitValueByBounds(Value - Change, this);
}

通过添加这个简单的修复,我们防止Value低于我们的最小值。

事件

由于我们有四个属性,并且每个属性都可以更改,因此创建四个相应的事件是合理的

timespan control

这些只是典型的样板自定义控件事件实现。

结论

关于这个控件没有太多要说的了。这基本上就是创建四个DependencyProperty字段和四个相应的事件。所有的验证工作都委托给了底层的SpinnerControl。我们唯一需要做的额外工作是,由于我们在两个地方存储了TimeSpanValue以及HoursMinutesSeconds),我们确保数据在所有属性中都得到同等更新。

正如开头提到的,这个控件只用于表示一个简短且定义明确的TimeSpan,范围从00:00:00到23:59:59。当然,这个控件可以扩展以表示一个完整的TimeSpan

请在下方留下您的评论或建议,如果觉得这篇文章有帮助,请不要忘记投票。谢谢!

© . All rights reserved.