Data-Centric Coding in Silverlight Applications

Focus on the data, not the controls, to build cleaner, better code

Silverlight 101

Data-Centric Coding in Silverlight Applications

Focus on the data, not the controls, to build cleaner, better code

By Dan Wahlin

If you build ASP.NET applications, 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, it's how you're conditioned to think.

Silverlight changes the game quite a bit because of the way you can bind data to controls. You don't have to name your controls in many situations, since ultimately you care about accessing the data rather than the control that contains the data. Granted, if you need to perform an animation or change a control's style, you'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. I covered the different types of bindings available in Silverlight in my previous article, "Silverlight Data Binding," but here's a quick example of a two-way binding defined in XAML:

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. This takes some time to get used to, but it's nice to leverage once you know about it.

My company is currently working on an application that uses a Silverlight DataGrid control with TextBox controls in each row. As a user changes quantity or hour values, the totals need to be updated. Figure 1 shows a simplified version of the DataGrid that's available with this article's sample code. Let's look at different solutions that can be used to update the totals values, starting with the traditional control-oriented approach.

Figure 1: Using the DataGrid control to capture quantity and hours.

Control-Oriented Approach

One solution that can be used to update the totals is to iterate through the target row and locate each TextBox to get the values. That's the control-oriented approach that would normally be used in ASP.NET applications. The TextBox controls are defined in DataGrid cell templates, as shown in Figure 2.

To update TextBlock controls that track totals at the end of each row (red area in Figure 1) as a user changes hours and quantity values, you can use the code shown in Figure 3. This code iterates through each column in a given DataGrid row and locates TextBox controls.

This code starts by first locating the selected item (the selected row) and converting it back to a TimeSheetViewRow. The TimeSheetViewRow class (see Figure 4) is bound to each row and contains the weekly hours and quantity values displayed and captured by the DataGrid. Once the appropriate item is found, the code iterates through all the controls in the row, locates TextBox controls, and uses their values to update the totals. Once all the TextBox controls are found, the last column in the DataGrid is located and the TextBlock controls in it are updated with the hours and quantity totals.

While this technique works fine, it's definitely not the easiest way to total each day's hours and quantity. Also, it 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.

Data-Oriented Approach

Since the TextBoxes in each row all have TwoWay bindings defined (refer to Figure 2), the TimeSheetViewRow object that was originally bound is automatically updated as TextBox values change. As a result, you can grab the selected item (which represents the bound object) from the DataGrid, then total up the property values of the TimeSheetViewRow object.

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 (refer to Figure 4), so that it can notify Silverlight as property values change and the data can be rebound to the TextBox and TextBlock controls. The control-oriented approach shown earlier can be simplified to the following code, which could be called as each TextBox loses focus:

_ViewModel.UpdateRowTotals((TimeSheetViewRow)
TimeSheetDataGrid.SelectedItem);

The UpdateRowTotals method that performs the actual calculations is located in a ViewModel class named TimeSheetViewModel. The ViewModel class contains all the properties that are bound to the UI (called the View) that the end user sees. Think of it as the client-side container for the data that's bound to Silverlight controls. You can bind the ViewModel class through code (LayoutRoot.DataContext = YourViewModelInstance) or declaratively in XAML, as Figure 5 demonstrates.

The code in Figure 5 references the ViewModel namespace, defines the ViewModel class as a resource, binds it to the layout root's DataContext, and binds the DataGrid to a TimeSheetViewRows property exposed through the ViewModel instance. A lot can be done without actually writing C# or Visual Basic code.

Figure 6 shows the UpdateRowTotals method that's located in the TimeSheetViewModel class. Looking through the code, you'll see that it relies on reflection to simplify the calculation process since each property has a set naming convention (e.g., MondayHours, MondayQuantity, TuesdayHours, TuesdayQuantity). The code could have explicitly added all the TimeSheetViewRow properties together if desired, using MondayHours + TuesdayHours + WednesdayHours type of syntax, but the "real world" solution I'm building called for more flexibility. Notice that no controls are referenced at all in the code.

As the HoursTotal and QuantityTotal properties are updated at the end of the UpdateRowTotals method, the corresponding TextBlock controls that they're bound to in the DataGrid will be updated automatically. It's a nice way to code since you don't have to be so concerned about controls and can focus on the actual data.

Focus on Data for Better Code

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 the user to change values. By focusing less on controls, you can often reduce the amount of code that has to be written. It takes a little getting used to, especially if you're familiar with the ASP.NET control-centric approach, but once the concept clicks, it really changes how you think about writing data-oriented code.

Dan Wahlin, a Microsoft MVP for ASP.NET and XML web services, founded the XML for ASP.NET Developers website and The Wahlin Group (http://www.TheWahlinGroup.com), which specializes in .NET, Silverlight, and SharePoint consulting and training solutions.

Source code accompanying this article is available for download.

Figure 2: Defining TextBox controls in a DataGrid cell template

HeaderStyle="{StaticResource TimeSheetDayHeaderStyle}">

Text="{Binding MondayQuantity, Mode=TwoWay}"

LostFocus="TextBox_LostFocus"

Style="{StaticResource TimeSheetTextBoxStyle}" />

Text="{Binding MondayHours, Mode=TwoWay}"

LostFocus="TextBox_LostFocus"

Style="{StaticResource TimeSheetTextBoxStyle}" />

Figure 3: Using the control-oriented approach to accessing data in 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();

Figure 4: The TimeSheetViewRow class bound to each row of the DataGrid

public partial class TimeSheetViewRow : INotifyPropertyChanged

{

private decimal HoursTotalField;

private decimal QuantityTotalField;

private decimal? FridayHoursField;

private decimal? FridayQuantityField;

private decimal? MondayHoursField;

private decimal? MondayQuantityField;

private decimal? SaturdayHoursField;

private decimal? SaturdayQuantityField;

private decimal? SundayHoursField;

private decimal? SundayQuantityField;

private decimal? ThursdayHoursField;

private decimal? ThursdayQuantityField;

private decimal? TuesdayHoursField;

private decimal? TuesdayQuantityField;

private decimal? WednesdayHoursField;

private decimal? WednesdayQuantityField;

public decimal? MondayHours

{

get

{

return this.MondayHoursField;

}

set

{

if ((this.MondayHoursField.Equals(value) != true))

{

this.MondayHoursField = value;

this.RaisePropertyChanged("MondayHours");

}

}

}

public decimal? MondayQuantity

{

get

{

return this.MondayQuantityField;

}

set

{

if ((this.MondayQuantityField.Equals(value) != true))

{

this.MondayQuantityField = value;

this.RaisePropertyChanged("MondayQuantity");

}

}

}

//Other Properties ommitted for the sake of brevity

public event PropertyChangedEventHandler PropertyChanged;

protected void RaisePropertyChanged(string propertyName)

{

PropertyChangedEventHandler propertyChanged = this.PropertyChanged;

if ((propertyChanged != null))

{

propertyChanged(this,

new PropertyChangedEventArgs(propertyName));

}

}

}

Figure 5: Declaratively binding a ViewModel class to a view using XAML

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:data="clr-namespace:System.Windows.Controls;

assembly=System.Windows.Controls.Data"

xmlns:viewModel="clr-namespace:SilverlightDataBindingOptions.ViewModel">

ItemsSource="{Binding TimeSheetView.TimeSheetViewRows}">

Figure 6: A data-centric approach to updating the totals in the DataGrid

public void UpdateRowTotals(TimeSheetViewRow row)

{

decimal qty = 0M;

decimal hours = 0M;

Type t = typeof(TimeSheetViewRow);

//Iterate through Monday Sunday property values

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;

}

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish