Experienced Windows programmers are familiar with writing the code to place data values in controls and to store values from controls:
public Form1 : Form
{
private MyType myDataValue;
private TextBox textBoxName;
private void InitializeComponent( )
{
textBoxName.Text = myDataValue.Name;
this.textBoxName.Leave += new
System.EventHandler( this.OnLeave );
}
private void OnLeave( object sender, System.EventArgs e )
{
myDataValue.Name = textBoxName.Text;
}
}
It's simple, repetitive codeyou know, the kind you hate to write because there must be a better way. There is. The .NET Framework supports data binding, which maps a property of an object to a property in the control:
textBoxName.DataBindings.Add ( "Text",
myDataValue, "Name" );
The previous code binds the "Text" property of the textBoxName control to the "Name" property of the myDataValue object. Internally, two objects, the BindingManager and the CurrencyManager, implement the transfer of data between the control and the data source. You've probably seen this construct in many samples, particularly with DataSets and DataGrids. You've also done simple binding to text boxes. You've likely only scratched the surface of the capabilities you get from data binding. You can avoid writing repetitive code by utilizing data binding more effectively.
A full treatment of data binding would span at least one book, if not two. Both Windows applications and web applications support data binding. Rather than write a complete treatise of data binding, I want to make sure you remember the key advantages of it. First, using data binding is much simpler than writing your own code. Second, you should use it for more than text itemsother display properties can be bound as well. Third, on Windows forms, data binding handles synchronizing multiple controls that examine related data sources.
For example, suppose you get a requirement to display the text in red whenever the data shows an invalid value. You could write the following snippet:
if ( src.TextIsInvalid )
{
textBox1.ForeColor = Color.Red;
} else
{
textBox1.ForeColor = Color.Black;
}
That's well and good, but you need to call that snippet of code whenever the text in your source changes. That could be when the user edits the text or when the underlying data source changes. There are a lot of events to handle and many places that you might miss. Instead, use data binding. Add a property in your src object to return the proper foreground color.
Other logic will set the value of that variable to the proper color based on the state of the text message:
private Color _clr = Color.Black;
public Color ForegroundColor
{
get
{
return _clr;
}
}
private string _txtToDisplay;
public string Text
{
get
{
return _txtToDisplay;
}
set
{
_txtToDisplay = value;
UpdateDisplayColor( IsTextValid( ) );
}
}
private void UpdateDisplayColor( bool bValid )
{
_clr = ( bValid ) ? Color.Black : Color.Red;
}
Then simply add the binding to the text box:
textBox1.DataBindings.Add ("ForeColor",
src, "ForegroundColor");
When the data binding is configured, textBox1 will draw its text in the correct color, based on the internal value of the source object. You've done more to decouple the control from the data source. Instead of having multiple event handlers and multiple locations where the display color changes, you have two. Your data source object keeps track of the properties that affect the proper display. Your form controls the data binding.
Although the samples I've shown are Windows forms, the same principle works for web applications: You can bind properties of data sources to a property in the web control as well:
<asp:TextBox id=TextBox1 runat="server"
Text="<%# src.Text %>"
ForeColor="<%# src.ForegroundColor %>">
This means that when you create the types that your application displays in its UI, you should add the necessary properties to create and update your UI in response to user needs.
What do you do if the objects you have don't support the properties you need? You wrap what you have and add what you need. Consider this data structure:
public struct FinancialResults
{
public decimal Revenue
{
get { return _revenue; }
}
public int NumberOfSales
{
get { return _numSales; }
}
public decimal Costs
{
get { return _cost;}
}
public decimal Profit
{
get { return _revenue - _cost; }
}
}
You have requirements to display these in a form with some special formatting notes. If the profit is negative, you must display the profit in red. If the number of sales drops below 100, it should be bold. If the cost is above 10,000, it should be bold. The developer who created the FinancialResults structure did not add UI capabilities into the structure. That was most likely the right choice. FinancialResults should limit its capabilities to storing the actual values. You can create a new type to include the UI formatting properties with the original store properties in the FinancialResults structure:
public struct FinancialDisplayResults
{
private FinancialResults _results;
public FinancialResults Results
{
get { return _results; }
}
public Color ProfitForegroundColor
{
get
{
return ( _results.Profit >= 0 ) ?
Color.Black : Color.Red;
}
}
// other formatting options elided
}
You have created a single data structure to facilitate data binding of your contained structure:
// Use the same datasource. That creates one Binding Manager
textBox1.DataBindings.Add ("Text",
src, "Results.Profit");
textBox1.DataBindings.Add ("ForeColor",
src, "ProfitForegroundColor");
I've created one read-only property that allows access to the core financial structure. That construct doesn't work if you intend to support read/write access to the data. The FinancialResults struct is a value type, which means that the get accessor does not provide access to the existing storage; it returns a copy. This idiom has happily returned a copy that cannot be modified using data binding. However, if you intended editing, the FinancialResults type would be a class, not a struct (see Item 6). As a reference type, your get accessor returns a reference to the internal storage and would support edits by the user. The internal structure would need to respond to changes made to the internal storage. The FinancialResults would raise events to notify other code of changes in state.
It's important to remember to use the data source for all related controls in the same form. Use the DataMember property to differentiate the property displayed in each control. You could have written the binding construct this way:
// Bad practice: creates two binding managers
textBox1.DataBindings.Add ("Text",
src.Results, "Profit");
textBox1.DataBindings.Add ("ForeColor",
src, "ProfitForegroundColor");
That would create two binding managers, one for the src object and one for the src.Results object. Each data source is managed by a different binding manager. If you want the binding manager to update all property changes when the data source changes, you need to make sure that the data sources match.
You can use data binding for almost any property of a Windows or web control. The values displayed in the control, the font, the read-only state, and even the location of the control can be the target of a binding operation. My advice is to create the class or struct that contains the values you need to display your data in the manner requested by your users. Then use data binding to update the controls.
In addition to simple controls, data binding often involves DataSets and DataGrids. It's very powerful: You bind the DataGrid to the DataSet, and all the values in the DataSet are displayed. If your DataSet has multiple tables, you can even navigate between tables. What's not to love?
Well, the problem arises if your data set does not contain the fields you want to display. In those cases, you must add a column to the DataSet that computes the value needed for the user interface. If the value can be computed using a SQL expression, the DataSet can compute the value for you. The following code adds a column n to the Employees data table that displays a formatted version of the name:
DataTable dt = data.Tables[ "Employees" ];
dt.Columns.Add( "EmployeeName",
typeof( string ),
"lastname + ', ' + firstname");
By adding columns to the DataSet, you can add columns to the DataGrid. You build layers of objects on top of the stored data objects to create the data presentation you want to give the user.
All the items I showed you so far are string types. The framework does handle converting strings to numeric values: It tries to convert the user's input to the proper type. If that fails, the original value is restored. It works, but the user gets absolutely no feedback, their input is silently ignored. You add that feedback by processing the Parse event from the binding context. That event occurs when the binding manager updates the value in the data source from the value in the control. ParseEventArgs gives you the text typed by the user and the desired type to convert the text. You can trap this event and perform your own notification, even going so far as to modify the value and update the text with your own value:
private void Form1_Parse( object sender, ConvertEventArgs e )
{
try {
Convert.ToInt32 ( e.Value );
} catch
{
MessageBox.Show (
string.Format( "{0} is not an integer",
e.Value.ToString( ) ) );
e.Value = 0;
}
}
You might also want to handle the Format event. This is the hook that lets you format the data that comes from your data source and goes into the control. You can modify the Value field of ConvertEventArgs to format the string that should be displayed.
The .NET Framework provides the generic framework for you to support data binding. Your job is to provide the specific event handlers for your application and your data. Both the Windows Forms and Web forms subsystems contain rich data-binding capabilities. The library already contains all the tools you need, so your UI code should really be describing the data sources and properties to be displayed and what rules should be followed when you store those elements back in the data source. You concentrate on building the data types that describe the display parameters, and the Winforms and Webforms data binding does the rest. There is no way around writing the code that transfers values between the user controls and the data source objects. Somehow, data must get from your business objects to the controls that your users interact with. But by building layers of types and leveraging data-binding concepts, you write a lot less of it. The framework handles the transfers for you, in both Windows and web applications.