【问题标题】:Change property based on timer in Silverlight根据 Silverlight 中的计时器更改属性
【发布时间】:2010-03-03 02:45:50
【问题描述】:

假设我有一个如下所示的类:

class Sample
{
    public string Value { get; set; }
    public DateTime Begin { get; set; }
    public DateTime End { get; set; }
}

我想显示Sample 实例的列表,其中每个实例在当前时间经过Begin 时改变颜色,然后在当前时间经过End 时再次改变颜色。

例如,假设我有一个包含 Sample 的 DataGrid,如下所示:

dataGrid1.ItemsSource = new List<Sample> {
    { Value="123",
      Begin=DateTime.Parse("10:00"),
      End=DateTime.Parse("11:00") } };

如何让显示“123”的行在 9:59 变为红色,在 10:00 变为黄色,在 11:00 变为红色?

编辑:我特别担心的一件事是计时器爆炸。如果我有 10,000 个样本,那么拥有 10k(或 20k)个计时器会不会有问题?如果我有 1M 样本怎么办?我认为将计时器设置为每个网格行而不是每个样本可能是一个更好的主意。

【问题讨论】:

    标签: c# silverlight timer


    【解决方案1】:

    通过这样做:

    MainPage.xaml

    <UserControl xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"  x:Class="ColorGridRow.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:ColorGridRow" mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
    <Grid x:Name="LayoutRoot">
        <data:DataGrid ItemsSource="{Binding}" AutoGenerateColumns="False">
            <data:DataGrid.Columns>
                <data:DataGridTemplateColumn>
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Grid Background="{Binding RowBackground}">
                                <TextBlock Text="{Binding Value}"/>
                            </Grid>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                </data:DataGridTemplateColumn>
                <data:DataGridTemplateColumn>
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Grid Background="{Binding RowBackground}">
                                <TextBlock Text="{Binding Begin}"/>
                            </Grid>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                </data:DataGridTemplateColumn>
                <data:DataGridTemplateColumn>
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Grid Background="{Binding RowBackground}">
                                <TextBlock Text="{Binding End}"/>
                            </Grid>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                </data:DataGridTemplateColumn>
            </data:DataGrid.Columns>
        </data:DataGrid>
    </Grid>
    

    MainPage.xaml.cs

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Windows.Controls;
    using System.Windows.Media;
    
    namespace ColorGridRow
    {
        public partial class MainPage : UserControl
        {
            public MainPage()
            {
                InitializeComponent();
                DataContext = new List<Sample>
                    {
                        new Sample("1", DateTime.Now + TimeSpan.FromSeconds(1), DateTime.Now + TimeSpan.FromSeconds(3)),
                        new Sample("2", DateTime.Now + TimeSpan.FromSeconds(2), DateTime.Now + TimeSpan.FromSeconds(4)),
                        new Sample("3", DateTime.Now + TimeSpan.FromSeconds(3), DateTime.Now + TimeSpan.FromSeconds(5)),
                    };
            }
        }
    
        public class Sample : INotifyPropertyChanged
        {
            private SolidColorBrush _savedRowBackground;
            private SolidColorBrush _rowBackground;
    
            public string Value { get; private set; }
            public DateTime Begin { get; private set; }
            public DateTime End { get; private set; }
    
            public SolidColorBrush RowBackground
            {
                get { return _rowBackground; }    
                set
                {
                    _rowBackground = value;
                    NotifyPropertyChanged("RowBackground");
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged = delegate { };
    
            private void NotifyPropertyChanged(string propertyName)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
    
            public Sample(string value, DateTime begin, DateTime end)
            {
                Value = value;
                Begin = begin;
                End = end;
                RowBackground = new SolidColorBrush(Colors.Red);
    
                Observable.Timer(new DateTimeOffset(begin)).Subscribe(_ =>
                {
                    _savedRowBackground = _rowBackground;
                    RowBackground = new SolidColorBrush(Colors.Yellow);
                });
    
                Observable.Timer(new DateTimeOffset(end)).Subscribe(_ => RowBackground = _savedRowBackground);
    
            }
        }
    }
    

    【讨论】:

    • Observable.Timer 从何而来?
    • 来自 Silverlight Toolkit 附带的 Reactive Framework(请参阅 msdn.microsoft.com/en-us/devlabs/ee794896.aspx)或单独下载。
    • Rx 是否需要添加对 DLL 的引用? using 怎么样?
    • 它确实需要添加对 System.Reactive.dll 的引用。它不需要 using,因为它将类添加到 System.* 命名空间。
    • +1 使用 Rx 框架是一个非常好的主意,我必须要考虑使用它。但是,我不确定数据对象暴露画笔是否是个好主意。在我的回答中结合 BooltoBrushConverter 使用布尔属性将导致数据和 UI 之间更好的分离。还有一个假设是 Begin, End 是不可变的,但我认为这很可能是真的。不错的一个;)
    【解决方案2】:

    可能有多种方法可以做到这一点,您的实际应用中的其他因素可能会影响我在下面概述的方法是否适合您的应用。

    指示状态变化

    首先,您需要通过某种方式来提醒 UI Sample 的状态变化,它会在范围内一段时间,然后会超出范围。您可以将此状态作为Sample 类型中的属性公开。您将通过实现INotifyPropertyChanged 接口来通知 UI。这是您的班级在实现 INotifyPropertyChanged 后的样子:-

    public class TimedSample : INotifyPropertyChanged
    {
    
        private string _Value;
        public string Value
        {
            get { return _Value; }
            set
            {
                _Value = value;
                NotifyPropertyChanged("Value");
            }
        }
    
        private DateTime _Begin;
        public DateTime Begin
        {
            get { return _Begin; }
            set
            {
                _Begin = value;
                NotifyPropertyChanged("Begin");
            }
        }
    
        private DateTime _End;
        public DateTime End
        {
            get { return _End; }
            set
            {
                _End = value;
                NotifyPropertyChanged("End");
            }
        }
    
        private bool _NowInRange;
        public bool NowInRange
        {
            get { return _NowInRange; }
            private set
            {
                _NowInRange = value;
                NotifyPropertyChanged("NowInRange");
            }
        }
    
        private void NotifyPropertyChanged(string name)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
    
    }
    

    当当前时间在BeginEnd 范围内时,TimeSample 内部的某些代码会使NowInRange 属性的值变为真。 (我会回到那个)。

    将布尔值转换为画笔

    下一个问题是您要更改项目的颜色。因此,我们想将TextBlockForeground 属性绑定到TimedSampleNowInRange 属性。所以我们需要一个IValueConverter:-

    public class BoolToBrushConverter : IValueConverter
    {
        public Brush FalseBrush { get; set; }
        public Brush TrueBrush { get; set; }
    
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value == null)
                return FalseBrush;
            else
                return (bool)value ? TrueBrush : FalseBrush;
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException("This converter only works for one way binding");
        }
    }
    

    一些 XAML 组合起来

    现在我们只需要将这个转换器放在一个资源字典中,我们就可以将它全部连接起来。 下面的 Xaml 假定将 TimedSample 对象列表分配给 Usercontrol 的 DataContext 属性。

    <UserControl x:Class="SilverlightApplication1.ListBoxStuff"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          xmlns:local="clr-namespace:SilverlightApplication1"
    >
        <Grid x:Name="LayoutRoot" Background="White">
            <Grid.Resources>
                <local:BoolToBrushConverter x:Key="BoolToYellowAndRed" TrueBrush="Yellow" FalseBrush="Red" />
            </Grid.Resources>
            <ListBox ItemsSource="{Binding}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Value}"
                            Foreground="{Binding NowInRange, Converter={StaticResource BoolToYellowAndRed}}" />
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </Grid>
    </UserControl>
    

    让它打勾

    现在需要一些机制来使NowInRange 属性在适当的时间点翻转其值。同样,可能有几种方法可以做到这一点。我将使用基于DispatcherTimer 的非常通用的解决方案。在这种情况下,我们将静态保存的DispatcherTimer 实例添加到TimedSample 类。它可能看起来像这样:-

        static readonly DispatcherTimer timer; 
    
        static TimedSample()
        {
            timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) };
            timer.Start();
        }
    
        public TimedSample()
        {
                   // Do not actually do this!
                   timer.Tick += timer_Tick;
        }
    
        private void timer_Tick(object sender, EventArgs e)
        {
            DateTime now = DateTime.Now;
            if (NowInRange != (Begin < now && now < End))
                NowInRange = !NowInRange;
        }
    

    这可以正常工作,但有一个问题。它会泄漏内存,一旦 TimedSample 被实例化,它将永远不会被 GC 释放和收集。它将永远被计时器的 Tick 事件引用,更糟糕的是,它会继续执行 timer_Tick 中的代码,尽管没有在其他任何地方使用。

    Silverlight 工具包以WeakEventListener 类的形式提供了巧妙的解决方案。 Beat Kiener 关于它的博客,并在 Simple Weak Event Listener for Silverlight 中包含它的代码。有了它,TimedSample 构造函数看起来像这样:-

        public TimedSample()
        {
            var weakListener = new WeakEventListener<TimedSample, DispatcherTimer, EventArgs>(this, timer);
            timer.Tick += weakListener.OnEvent;
            weakListener.OnEventAction = (instance, source, e) => instance.timer_Tick(source, e);       
            weakListener.OnDetachAction = (listener, source) => timer.Tick -= listener.OnEvent;
        }
    

    当 UI 或任何其他地方不再引用 TimedSample 时,GC 可以收集它。当下一个 Tick 事件触发时,WeakEventListener 检测到对象已消失并调用 OnDetachAction 使 WeakEventListener 的实例本身也可用于垃圾回收。

    我已经开始了,所以我会完成

    这个答案已经很长了,对此感到抱歉,但既然如此,我不妨给你我用于上面列出的 Xaml 的测试代码隐藏:-

    public partial class ListBoxStuff : UserControl
    {
        public ListBoxStuff()
        {
            InitializeComponent();
            DataContext = GetTimedSamples(10, TimeSpan.FromSeconds(5));
        }
    
        IEnumerable<TimedSample> GetTimedSamples(int count, TimeSpan interval)
        {
            TimedSample sample = null;
            for (int i = 0; i < count; i++)
            {
                sample = new TimedSample()
                {
                    Value = String.Format("Item{0}", i),
                    Begin = sample != null ? sample.End : DateTime.Now,
                    End = (sample != null ? sample.End : DateTime.Now) + interval
                };
                yield return sample;
            }
        }
    }
    

    【讨论】:

    • 虽然我喜欢只使用一个计时器的想法,但我不喜欢它每秒调用每个 Sample 的处理程序的方式。如果我有 1M 个样本,其中 10k 个在 DataGrid 中,只显示其中的 100 个,它仍然会每秒调用处理函数 1M 次!
    • @gabe:正如我在回答中所述,该解决方案可能不符合您的实际应用要求。当然,如果您确实有 1M 实例并且其中 10K 存在于 Grid 中,则此解决方案可能不太适合;)这就是简单的高度通用解决方案的问题,它们往往无法很好地扩展。
    猜你喜欢
    • 1970-01-01
    • 2014-12-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多