Control-Oriented Vs. Data-Oriented Programming in Silverlight

If you build ASP.NET applications then you’re used to working with controls a lot.  Nearly everything you do requires accessing control IDs.  If you need to retrieve data entered by an end user you need to reference the controls that contain the data.  That’s just the way we do it in ASP.NET and if you’ve been writing ASP.NET applications very long its how you’re conditioned to think.

Silverlight changes the game quite a bit due to the way you can bind data to controls.  With Silverlight applications I don’t always name my controls since ultimately I care about accessing the data rather than the control that contains the data.  Sure, if I need to perform an animation or change a control’s style I’ll need to access the control directly by its name, but when it comes to accessing data there’s an easier way.  Silverlight provides two-way bindings that allow a data object to be bound to a control in a unique way.  If the user changes the data the source object is automatically updated without additional code on your part.  An example of a two-way binding defined in XAML is shown next:

<TextBox Text="{Binding MondayQuantity, Mode=TwoWay}" 
Style="{StaticResource TimeSheetTextBoxStyle}" />


This example binds the DataContext (the main object bound to the overall form) object’s MondayQuantity property to a TextBox control.  The Mode is set to TwoWay rather than the default OneWay binding which means that any changes to the TextBox are automatically moved back to the MondayQuantity property in the source object.  As a result, you don’t need to access the TextBox to get the value entered since the source object that was originally bound to the control contains up-to-date data.  Having done a lot of ASP.NET programming this took some time to get used to, but it’s nice to leverage once you know about it. 

I’m working on an application that uses a Silverlight DataGrid control with nested ComboBox and TextBox controls in each row.  As a user changes  quantity or hour values the totals need to be updated.

image

Let’s take a look at different solutions that can be used to update the totals values.

A Control-Oriented Approach

One solution to updating the totals is to iterate through the target row and locate each TextBox to get the values.  That’s the control-oriented approach we’d normally use in ASP.NET applications.  To update TextBlock controls that track totals at the end of each row (red area in the image shown above) as a user changes values, you could use the following code which iterates through each column in a given row and locates TextBox controls:

//Get the DataGrid's selected item
TimeSheetViewRow dataContext = (TimeSheetViewRow)TimeSheetDataGrid.SelectedItem;
decimal totalQty = 0;
decimal totalHours = 0;

//Loop through all the columns for the selected row
for (int i = 2; i < TimeSheetDataGrid.Columns.Last().DisplayIndex; i++)
{
    StackPanel sp = (StackPanel)TimeSheetDataGrid.Columns[i].GetCellContent(dataContext);
    foreach (UIElement elem in sp.Children)
    {
        //Find the TextBox controls
        if (elem is TextBox)
        {
            TextBox tb = (TextBox)elem;
            if (!String.IsNullOrEmpty(tb.Text))
            {
                decimal val = decimal.Parse(tb.Text);
                if (tb.Tag.ToString() == "Quantity") totalQty += val;
                if (tb.Tag.ToString() == "Hours") totalHours += val;
            }
        }
    }
}
//Find totals TextBlocks and update them
StackPanel totalsSP = (StackPanel)TimeSheetDataGrid.Columns[TimeSheetDataGrid.Columns.Last()
    .DisplayIndex].GetCellContent(dataContext);
((TextBlock)totalsSP.FindName("TotalQuantityTextBlock")).Text = totalQty.ToString();
((TextBlock)totalsSP.FindName("TotalHoursTextBlock")).Text = totalHours.ToString();


While this technique works fine, it’s definitely not the easiest way to total each day’s hours and quantity and would have to be re-written if the TextBox controls’ container changes from a StackPanel to something else.  Let’s look at a more flexible data-oriented approach.

A Data-Oriented Approach

Since the TextBoxes in each row all have TwoWay bindings defined, the source object that was originally bound (of type TimeSheetViewRow) is automatically updated as TextBox values change.  As a result, I can simply grab the selected item (which represents the bound object) from the DataGrid and then total up the property values.  Once the totals are calculated the appropriate quantity and hours total properties can be updated on the source object which automatically updates the grid TextBlock controls bound to those properties.  It’s important to note that the TimeSheetViewRow class implements INotifyPropertyChanged so that it can notify Silverlight as property values change so that the data can be re-bound to the controls.  The control-oriented approach shown earlier can be simplified to the following:

TimeSheetViewModel vm = (TimeSheetViewModel)LayoutRoot.DataContext;
vm.UpdateRowTotals((TimeSheetViewRow)TimeSheetDataGrid.SelectedItem);


The code that performs the actual calculations is shown next.  It’s located in a ViewModel class named TimeSheetViewModel that exposes an UpdateRowTotals method.  The ViewModel class contains all of the properties that are bound to the form (called the View) that the end user sees.  I used reflection to simplify the calculation process since each property has a set naming convention (MondayHours, MondayQuantity, TuesdayHours, TuesdayQuantity, etc.) but I certainly could’ve written code to add all of the Monday – Sunday quantity and hours properties together more explicitly.

 

public void UpdateRowTotals(TimeSheetViewRow row)
{
    decimal qty = 0M;
    decimal hours = 0M;
    Type t = typeof(TimeSheetViewRow);
    foreach (PropertyInfo prop in t.GetProperties())
    {
        object val = prop.GetValue(row, null);
        decimal decimalVal = (val==null)
            ?0.00M:decimal.Parse(val.ToString());
        if (prop.Name.EndsWith("Hours")) hours += decimalVal;
        if (prop.Name.EndsWith("Quantity")) qty += decimalVal;
    }
    row.HoursTotal = hours;
    row.QuantityTotal = qty;
}

As the HoursTotal and QuantityTotal properties change the corresponding TextBlock controls that they’re bound to will be updated automatically.

You can see that with Silverlight you can focus on working with data as opposed to working with controls by using the built-in support for TwoWay binding.  Controls are there to present the data to the end user and allow them to change values.  By focusing less on controls we can reduce the amount of code that has to be written in many cases.  It takes a little getting used to especially if you’re used to the ASP.NET control-centric approach, but once the concept clicks it really changes how you think about writing data-oriented code.

comments powered by Disqus

5 Comments

  • Well the data oriented approach wont work if the collection is a type of Observable! dont know why, but it is not working, at least it wont update the rows till a new item is inserted/ or removed from collection.. Please let me know if u can make it work !!

    Thanks,

    Gopi

  • Gopi,
    That's the whole point of Observable collection actually. &nbsp;Make sure the type you store in Observable collection implements INotifyPropertyChanged and that you're binding the type's properties using the TwoWay mode if you want them to auto-update as a user changes something. &nbsp;Otherwise the updates won't make it back into the source object. &nbsp;It definitely works though....I use it daily on our current client project.

  • Dan, would it be possible to release the entire source code for your modified DataGrid control?

  • Hi Dan,

    If you are using two-way binding couldn't you also update the data context's totals properties whenever a daily property changes and then let the binding infrastructure push the change back to the UI? What am I missing here? Are the totals not part of the original data context?

  • Rover,

    I'm updating the total(s) property which then automatically updates the UI. I'm guessing that's what you're talking about. The "row" variable in the code above represents the row object stored in the DataContext rather than the Grid row. I just named it "row" to keep things simple.

Comments have been disabled for this content.