views:

285

answers:

2

Assume I have a class that looks like this:

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

I want to display a list of Sample instances where each one changes color when the current time passes Begin and then changes color again when the current time passes End.

For example, say I have a DataGrid containing a Sample sort of like this:

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

How would I get the row showing "123" to be red at 9:59, turn yellow at 10:00, and turn red at 11:00?

EDIT: One thing I'm particularly concerned about is a timer explosion. If I have 10,000 Samples, will it be a problem to have 10k (or 20k) timers? What if I have 1M Samples? I think it might be a better idea to make the timers per grid row rather than per Sample.

+3  A: 

By doing this:

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);

        }
    }
}
PL
Where does Observable.Timer come from?
Gabe
From Reactive Framework (see http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx) that comes with Silverlight Toolkit or a a separate download.
PL
Does Rx require adding a reference to a DLL? How about a `using`?
Gabe
It does require adding a reference to System.Reactive.dll. It doesn't require using though as it adds classes to System.* namespaces.
PL
+1 The use of the Rx framework it's realy nice idea, I must get round to playing with that. However I'm not sure its a good idea for the data object to expose a brush. Using a boolean property in combination of the BooltoBrushConverter in my answer would result in better separation between data and UI. There is also the assumption that Begin, End are immutable but I think that's very likely to be true. Nice one ;)
AnthonyWJones
@AnthonyWJones: Well, I was considering Sample class as a ViewModel class that in real application would serve as a layer between a real model and the view. And RX - I agree with you, I find myself using it more and more lately as it allows to write a very succinct code so much easier.
PL
+1  A: 

There are probably a number of ways to do this and other factors in your real app may affect whether the approach I outline below suits your app.

Indicating change of state

First you will need some way to alert the UI of change in status of a Sample, it will be in range for a period of time and then it will go out of range. You could have this state exposed as a property in the Sample type. You would notify the UI by implementing the INotifyPropertyChanged interface. Here is what your class looks like with INotifyPropertyChanged implemented:-

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;

}

Some code internal to the TimeSample would make the value of NowInRange property true when the current time is within its Begin and End range. (I'll come back to that).

Converting a boolean to a brush

The next issue is that you want to change the color of an item. Hence we'd want to bind say the Foreground property of a TextBlock to the NowInRange property of a TimedSample. So we need an 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");
    }
}

Some XAML to put this together

Now we just need to place this converter in a resource dictionary and we can wire it all up. The Xaml below assumes a list of TimedSample objects is assigned to the Usercontrol's DataContext property.

<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>

Making it tick

Now what is needed is some mechanism to cause the NowInRange property to flip its value at the appropriate point in time. Again there are probably several ways to do this. I'll use a very general solution based on the DispatcherTimer. In this case we add a statically held DispatcherTimer instance to the TimedSample class. It could look like this:-

    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;
    }

That'll work fine but there is a problem. It will leak memory, once a TimedSample is instanced it will never be freed and collected by the GC. It will forever be referenced by the Tick event of the timer, worse yet will continue to execute code in timer_Tick despite not being used anywhere else.

The Silverlight Toolkit has neat solution to this in the form of the WeakEventListener class. Beat Kiener blogs about it and includes the code for it in Simple Weak Event Listener for Silverlight. With that in place the TimedSample constructor can look like this:-

    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;
    }

When a TimedSample is no longer referenced by the UI or anywhere else the GC can collect it. When the next Tick event fires the WeakEventListener detects that the object is gone and calls OnDetachAction making the instance fo WeakEventListener itself also available for garbage collection.

I've started so I'll finish

This answer has ended up being quite long, sorry about that, but seeing as it is I may as well give you the test code-behind I was using for the Xaml listed above:-

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;
        }
    }
}
AnthonyWJones
Although I like the idea of only using a single timer, I don't like how it calls every Sample's handler every second. If I have 1M Samples, 10k of which are in the DataGrid, only 100 of which are displayed, it will still invoke the handler function 1M times per second!
Gabe
@gabe: As stated in my answer the solution may not fit your real apps requirements. Of course if you do have 1M instances and 10K of them present in a Grid this solution perhaps doesn't fit too well ;) Thats the problem with simple highly general solutions, they tend not to scale very well.
AnthonyWJones