Some poor souls analysts spend their entire days examining numbers in spreadsheets. Row by row, column by column. The only job is to spot the number that’s out of place; a professional game of “Where’s Waldo”. Doesn’t sound that bad, yet.. Let’s raise the stakes and make the data dynamic. That’s right, we’re looking for Waldo, but now he’s allowed to move around, and show up in multiple places at the same time. With a little UI/UX magic, we can turn this task from impossible to someone’s day job.
Conditional formatting is a concept where an item is formatted or styled specially based on a condition, much as the term implies. In a grid, this is usually done based on a the value of the cell, but can many times be more complex than that. In a typical scenario the background color of a cell is changed based on a certain value. A value dips below 10, the cell turns red; the value exceeds 100, the cell turns blue.
Figure 1: A grid of stock quotes with conditional formatting applied.
CellControlAttached Event
The XamWebGrid allows you to perform conditional formatting through it’s CellControlAttached event, which has some special characteristics. The CellControlAttached event only fires for cells that are currently in view as a result of the XamWebGrid’s virtualization engine. The virtualization engine ensures that the grid doesn’t waste CPU cycles and memory on objects that aren’t in view. The result is a grid that performs just as fast when bound to 10 records or 10,000.
The CellControlAttached event not only fires when a cell is brought into view, but it also fires when a cell binding is refreshed due to an underlying change in the datasource. This is where it becomes important that your underlying business objects fire INotifyPropertyChanged events appropriately.
private void WatchGrid_CellControlAttached(object sender, CellControlAttachedEventArgs e) { if(e.Cell.Column.Key=="Bid"){ if (((double)e.Cell.Value) > ((Stock)e.Cell.Row.Data).SellTrigger ) { e.Cell.Style = Application.Current.Resources['SellStyle'] as Style; } else { e.Cell.Style = Application.Current.Resources['NormalStyle'] as Style; } } else if(e.Cell.Column.Key=='Ask'){ if (((double)e.Cell.Value) < ((Stock)e.Cell.Row.Data).BuyTrigger) { e.Cell.Style = Application.Current.Resources['BuyStyle'] as Style; } else { e.Cell.Style = Application.Current.Resources['NormalStyle'] as Style; } }
Figure2: CellControlAttached handler, responsible for conditional formatting.
In Figure 2 above, it’s important to note a couple of items. First, we’re able to access the underlying business object that our grid row is bound to by using e.Cell.Row.Data. A simple cast is all that is necessary to turn this object pointer into our “Stock” business object. Second, notice that we’re assigning the e.Cell.Style property from inside of this event. You could either modify the style through code, or alternatively use a style that is defined in a resource dictionary. I chose to use a style defined in a resource dictionary (Application.Resources in app.xaml). The reason this is preferred is to keep your styles and your business logic separate. This way, if you choose to change the way “SellStyle” looks, you don’t have to open up your .cs file and modify properties through code.
If you’re used to working in ASP.NET, Silverlight throws you a curveball when it comes to threading. Windows client developers should be used to fighting with the UI Thread, and marshaling between background threads and the UI threads. In Silverlight there’s a DispatchTimer you can (and should) use when when working with the UI thread. It does the Dispatcher.BeginInvoke calls for you, so that you don’t have to manually marshall your objects.
Using the DispatchTimer, you can set the timer even to fire at any given interval. Don’t be scared to push the limits here. In this example I set my timer to fire at 20ms intervals, and the XamWebGrid handled it like a champ.
public StocksPage() { InitializeComponent(); //Create a DispatchTimer //NOTE: DispatchTimer must be used since we're dealing //with the UI Thread. DispatcherTimer timer=new DispatcherTimer(); timer.Interval=new TimeSpan(0,0,0,0,20); timer.Tick+=new EventHandler(timer_Tick); timer.Start(); } Random r = new Random(); void timer_Tick(object sender, EventArgs e) { //Update Values based on random generator foreach (Stock s in this.Stocks) { //Don't update indexes (ie. ^DJIA) if (!s.Symbol.StartsWith("^")) { //Rather than updating each cell every time, emulate real conditions //by skipping ~30% of our quotes during each update if (r.Next(10) > 3) { //Get our change value, subtract .5 to bring our value scale between -0.5, 0.5 double change = r.NextDouble() - .5; //When our business object properties are changed //INotifyPropertyChanged will fire, and the grid will //update cells with the new value. s.Ask += change; s.Bid += change; } } } }
Building a dynamically updating grid with conditional formatting using the XamDataGrid, is about as easy as you can get. Using the CellControlAttached event not only enables this scenario, but also ensures peak performance by firing the event only for cells which are visible.
Download Full Source: conditionalformatting.zip