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