DataGrid – Per-Row Running Totals

, ,

The program I’m working on lists transactions in a WPF Toolkit DataGrid. Each grid row displays a transaction’s date, amount, etc.  Each row in the grid needed to show the sum of all transaction amounts prior to and including that row. You might call this a per-row running total. Example:

Date       | Check #   | Payee       | Amount    | Balance
2/1/10                   Frank         10.00       10.00
3/6/10       101         John          -6.95        3.05
4/9/10       102         Tom           -1.05        2.00

The service layer could provide to the view model an ordered transactions collection with balances calculated. Each grid resort (triggered by clicking on a grid column header) would necessitate a service layer re-query to fetch reordered data with balances recomputed. Accessing the service layer in response to UI events when no domain data has changed (just the ordering of its visual display) suggests a likely separation of concerns (SoC) violation.

Attaching to DataGrid’s OnSorting event might work (if that event is raised after sorting completes). This approach necessitates event handling code in the view’s code-behind file. Having balance-computation logic in the event handler would be hard to unit test and would violate SoC. The handler could be kept small, containing just enough code to pass a “recompute balances” call off to the view model, but would still involve some custom code in the view’s code-behind.

What if there was a way to databind a callback method from the view model to the data grid? With this approach, no extra extra display-related calls are made to the service layer, the view does not need any custom code-behind and the balance computation logic is in the view model where it both ideally belongs and can easily be unit tested.

How does this look to you?

public class AdvancedDataGrid : Microsoft.Windows.Controls.DataGrid
    {
        // A dependency property is required for data binding to work.
        public static readonly DependencyProperty SortCallbackProperty = DependencyProperty.Register("SortCallback", typeof(Action<System.Collections.IEnumerable>), typeof(AdvancedDataGrid));

        protected override void OnInitialized(EventArgs e)
        {
            base.OnInitialized(e);

            // Finding a way to trigger the initial call to compute balances was hard. I couldn't find a DataGrid event that was
            // called automatically after the control had been initialized enough for this processing to be done. Attaching an
            // event handler to the dependency property triggers processing each time that property's value changes
            // (including when it's initially set).
            var dependencyProperty = System.ComponentModel.DependencyPropertyDescriptor.FromProperty(SortCallbackProperty, typeof(AdvancedDataGrid));
            // TODO: (Left as an exercise to the visitor) Make this reference to ProcessSort() some kind of
            // weak event to prevent a memory leak. As is, this code will keep the garbage collector from
            // reclaiming instances of this class because the dependency property infrastructure holds a
            // strong reference to each instance's event handler and doesn't release that reference
            // until shutdown.
            dependencyProperty.AddValueChanged(this, (sender, ee) => ProcessSort());
	}

	protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
	{
		base.OnItemsSourceChanged(oldValue, newValue);
		ProcessSort();
	}

	// This is called when the user clicks on a data grid column header to trigger sorting.
	protected override void OnSorting(Microsoft.Windows.Controls.DataGridSortingEventArgs eventArgs)
	{
		base.OnSorting(eventArgs);
		// We want to calculate balances after the re-sort has occurred, so we call ProcessSort() after calling base.
		ProcessSort();
	}

	public Action<System.Collections.IEnumerable> SortCallback
	{
		get { return (Action<System.Collections.IEnumerable>)GetValue(SortCallbackProperty); }
		set { SetValue(SortCallbackProperty, value); }
	}

	private void ProcessSort()
	{
		if (SortCallback != null)
		{
			SortCallback(Items);
		}
	}
}

In XAML:

<local:AdvancedDataGrid ItemsSource="{Binding Transactions}"	SortCallback="{Binding SortCallback}" />

And in our view model, the SortCallback property is set:

SortCallback = (collection) =>
{
	decimal runningTotal = 0;

	foreach (Transaction item in collection)
	{
	    runningTotal += item.Amount;
	    item.Balance = runningTotal;
	}
};

Leave a Reply

Your email address will not be published. Required fields are marked *