Export (0) Print
Expand All
2 out of 2 rated this helpful - Rate this topic

Chapter 2 — Handling Data

 

patterns & practices Developer Center

Smart Client Architecture and Design Guide

David Hill, Brenton Webster, Edward A. Jezierski, Srinath Vasireddy and Mohammad Al-Sabt, Microsoft Corporation; Blaine Wastell, Ascentium Corporation; Jonathan Rasmusson and Paul Gale, ThoughtWorks; and Paul Slater, Wadeware LLC

June 2004

Related Links

Microsoft® patterns & practices Library http://msdn.microsoft.com/en-us/practices/default.aspx

Application Architecture for .NET: Designing Applications and Services http://msdn.microsoft.com/en-us/library/ms954595.aspx

Summary: This chapter examines the various considerations for handling data on the client, including data caching, data concurrency, and the use of datasets and Windows Forms data binding.

Contents

Types of Data
Caching Data
Data Concurrency
Using ADO.NET DataSets to Manage Data
Windows Forms Data Binding
Summary

In smart clients, application data is available on the client. If your smart clients are to function effectively, it is essential that this data be managed appropriately to make sure that it is kept valid, consistent, and secure.

Application data can be made available to the client by a server-side application (for example, through a Web service), or the application can use its own local data. If the data is provided by a server application, the smart client application may cache the data to improve performance or to enable offline usage. In this case, you need to decide how the client application should handle data that is out of date with respect to the server.

If your smart client application provides the ability to modify data locally, the client changes have to be synchronized with the server-side application at a later time. In this case, you have to decide how the client application can handle data conflicts and how to keep track of the changes that need to be sent to the server.

You need to carefully consider these and a number of other issues when designing your smart client application. This chapter examines the various considerations for handling data on the client, including:

  • Types of data.
  • Caching data.
  • Data concurrency.
  • Using ADO.NET datasets to manage data.
  • Windows Forms data binding.

A number of other issues related to handling data are not discussed in this chapter. In particular, data handling security issues are discussed in Chapter 5: Security Considerations and offline considerations are discussed in Chapter 4: Occasionally Connected Smart Clients

Types of Data

Smart clients generally have to handle two categories of data:

  • Read-only reference data
  • Transient data

Typically, these types of data need to be handled in different ways, so it is useful to examine each of them in more detail.

Read-Only Reference Data

Read-only reference data is data that is not changed by the client and that is used by the client for reference purposes. Therefore, from the client's point of view, the data is read-only data, and the client performs no update, insert, or delete operations on it. Read-only reference data is readily cached on the client. Reference data has a number of uses in a smart client application, including:

  • Providing static reference or lookup data. Examples include product information, price lists, shipping options, and prices.
  • Supporting data validation, allowing data entered by the user to be checked for correctness. An example is checking entered dates against a delivery schedule.
  • Helping to communicate with remote services. An example is converting a user selection to a product ID locally and then sending the information to a Web service.
  • Presenting data. Examples include presenting Help text or user interface labels.

By storing and using reference data on the client, you can reduce the amount of data that needs to travel from client to server, improve the performance of your application, help enable offline capabilities, and provide early data validation, which increases the usability of your application.

Although read-only reference data cannot be changed by the client, it can be changed on the server (for example, by an administrator or supervisor). You need to determine a strategy for updating the client when changes to the data occur. Such a strategy could involve pushing changes out to the client when a change occurs or pulling changes from the server at certain time intervals or prior to certain actions on the client. However, because the data is read-only at the client, you do not need to keep track of client-side changes. This simplifies the way in which read-only reference data needs to be handled.

Transient Data

Transient data can be changed on the client as well as the server. Generally, transient data changes as a direct or indirect result of user input and manipulation. In this case, changes that are made on either the client or server need to be synchronized at some point. This type of data has a number of uses in a smart client, including:

  • Adding new information. Examples include adding banking transactions or customer details.
  • Modifying existing information. An example is updating customer details.
  • Deleting existing information. An example is removing a customer from a database.

One of the most challenging aspects of dealing with transient data on smart clients is that it can generally be modified on multiple clients at the same time. This problem is exacerbated when the data is very volatile, because changes are more likely to conflict with one another.

You need to keep track of any client-side changes that you make to transient data. Until the data is synchronized with the server and any conflicts have been resolved, you should not consider transient data to be confirmed. You should be very careful not to rely on unconfirmed data to make important decisions or use it as the basis for other local changes without carefully considering how data consistency can be guaranteed even in the event of a synchronization failure.

For more details about the issues surrounding handling data when offline and how to handle data synchronization, see Chapter 4: Occasionally Connected Smart Clients

Caching Data

Smart clients often need to cache data locally, whether it is read-only reference data or transient data. Caching data has the potential to improve performance in your application and provide the data necessary to work offline. However, you need to carefully consider which data is cached on the client, how that data is to be managed, and the context in which that data can be used.

To enable data caching, your smart client application should implement some form of caching infrastructure that can handle the data caching details transparently. Your caching infrastructure should include one or both of the following caching mechanisms:

  • Short-term data caching. Caching data in memory is good for performance but is not persistent, so you may need to pull data from the source when the application is re-run. Doing so may prevent your application from operating when offline.
  • Long-term data caching. Caching data in a persistent medium, such as isolated storage or the local file system, allows you to use the application when there is no connectivity to the server. You may choose to combine long-term storage with short-term storage to improve performance.

Regardless of the caching mechanisms you adopt, you should ensure that only data to which the user has access is made available to the client. Also, sensitive data cached on the client requires careful handling to ensure that it is kept secure. Therefore, you may need to encrypt the data as it is transferred to the client and as it is stored on the client. For more information, see "Handling Sensitive Data" in Chapter 5: Security Considerations

As you design your smart client to support data caching, you should consider providing a mechanism for your client to request fresh data, regardless of the state of the cache. This means that you can be sure that the application is ready to perform new transactions without using stale data. You may also configure your client to pre-fetch data so that it can mitigate the risk of being offline when cached data expires.

Wherever possible, you should associate some form of metadata with the data to enable the client to manage the data in an intelligent way. Such metadata can be used to specify the data's identity and any constraints or desired behaviors associated with the data. Your client-side caching infrastructure should consume this metadata and use it to handle the cached data appropriately.

All data that is cached on the client should be uniquely identifiable (for example, through a version number or date stamp), so that it can be properly identified when determining whether it needs to be updated. Your caching infrastructure is then able to ask the server whether the data that it has is currently valid and determine if any updates are required.

Metadata can also be used to specify constraints or behaviors that relate to the usage and handling of the cached data. Examples include:

  • Temporal constraints. These constraints specify the time or date range in which the cached data can be used. When the data becomes stale or expires, it can be dropped from the cache or automatically refreshed by obtaining the latest data from the server. In some cases, it may be appropriate to let the client use out-of-date reference data and map it to up-to-date data when it is synchronized with the server.
  • Geographic constraints. Some data may be appropriate only for a particular region. For example, you may have different price lists for different locations. Your caching infrastructure can be used to access and store data on a per-location basis.
  • Security requirements. Data that is specifically intended for a particular user can be encrypted to ensure that only the appropriate user can access it. In this case, the data is provided already encrypted, and the user has to provide the credentials to the caching infrastructure to allow the data to be decrypted.
  • Business rules. You may have business rules associated with your cached data that dictate how it should be used. For example, your caching infrastructure may take into consideration the role of the user to determine what data is provided to him or her and how it is handled.

The metadata associated with the data enables your caching infrastructure to handle the data appropriately so that your application does not have to be concerned with data caching issues or implementation details. You can pass the metadata associated with the reference data within the data itself, or you can use an out-of-band mechanism. The exact mechanism used to transport the metadata to the client depends on how your application communicates with the network services. When using Web services, using SOAP headers to communicate the metadata to the client is a good solution.

The differences between read-only reference data and transient data sometimes mean that you need to use two caches, one for reference data and one for transient data. Reference data is read-only on the client and does not need to be synchronized back with the server, but it does need to be refreshed occasionally to reflect any changes and updates made on the server.

Transient data can be changed on the client as well as the server. With data in the cache being updated sometimes on the client, sometimes on the server, and sometimes on both, any changes made to the data on the client need to be synchronized with the server at some point. If the data has changed on the server in the meantime, a data conflict occurs and needs to be handled appropriately.

To help ensure that data consistency is maintained, and to avoid using data inappropriately, you should be careful to keep track of any changes that you make to transient data on the client. Such changes are uncommitted or tentative until they are successfully synchronized or confirmed with the server.

You should design your smart client application so that it can differentiate between data that has been successfully synchronized with the server and data that is still tentative. This distinction helps your application detect and handle data conflicts more easily. Also, you may want to restrict the application or the user from making important decisions or initiating important actions based on tentative data. Such data should not be relied on until it has been synchronized with the server. By using an appropriate caching infrastructure, you can keep track of tentative and confirmed data.

The Caching Application Block

The Caching Application Block is a Microsoft® .NET Framework extension that allows developers to easily cache data from service providers. It was built and designed to encapsulate Microsoft's recommended practices for caching in .NET Framework applications as described in Caching Architecture Guide for .NET Framework Applications at http://msdn.microsoft.com/en-us/library/ms978498.aspx.

The overall architecture of the caching block is shown in Figure 2.1.

Ff647254.cabwof01(en-us,PandP.10).gif

Figure 2.1   Caching block workflow

The caching workflow consists of the following steps:

  1. A client or service agent makes a request to the CacheManager for cached data items.
  2. If the item is already cached, the CacheManager retrieves the item from storage and returns it as a CacheItem object. If the item is not already cached, the client is notified.
  3. After retrieving noncached data from a service provider, the client sends the data to the CacheManager. The CacheManager adds a signature (that is, metadata), such as a key, expiration, or priority, to the item and loads it into the cache.
  4. The CacheService monitors the lifetime of CacheItems. When a CacheItem expires, the CacheService removes it and, optionally, calls a callback delegate.
  5. The CacheService can also flush all items from the cache.

The caching block offers a variety of caching expiration options, which are described in Table 2.1.

Table 2.1   Caching Block Expiration Options

ClassDescription
AbsoluteTimeUse to set the absolute time for an expiration.
ExtendedFormatTimeUse to set an expiration based on an expression (such as every minute or every Monday).
FileDependencyUse to set an expiration based on whether a file is changed.
SlidingTimeUse to set the lifetime for an item by specifying an expiration based on when an item is last accessed.

The following storage mechanisms are available for the caching block:

  • Memory-mapped file (MMF). MMFs are best suited for a client-based, high-performance caching scenario. You can use MMFs to develop a cache that can be shared across multiple application domains and processes within the same computer. The .NET Framework does not support MMFs, so any implementation of an MMF cache runs as unmanaged code and does not benefit from any .NET Framework features, including memory management features (such as garbage collection) and security features (such as code access security).
  • Singleton object. A .NET remoting singleton object can be used to cache data that can be shared across processes in one or several computers. This is done by implementing a caching service using a singleton object that serves multiple clients through .NET remoting. Singleton caching is simple to implement, but it lacks the performance and scalability provided by solutions based on Microsoft SQL Server™.
  • Microsoft SQL Server 2000 database. SQL Server 2000 storage is best suited to an application that requires high durability or when you need to cache a very large amount of data. Because the cache service needs to access SQL Server over a network and the data is retrieved using database queries, data access is relatively slow.
  • Microsoft SQL Server Desktop Engine (MSDE). MSDE is a lightweight database alternative to SQL Server 2000. It provides reliability and security features but has a smaller client footprint than SQL Server, so it requires less setup and configuration. Because MSDE supports SQL, developers also gain much of the power of a database. You can migrate an MSDE database to a SQL Server database if necessary.

Data Concurrency

As mentioned earlier, one problem with using smart clients is that changes to the data held on the server can occur before any client-side changes are synchronized with the server. You need a mechanism to ensure that when the data is synchronized, any data conflicts are handled appropriately and the resultant data is consistent and correct. The ability for data to be updated by more than one client is known as data concurrency.

There are two approaches that you could use to handle data concurrency:

  • Pessimistic concurrency. Pessimistic concurrency allows one client to maintain a lock over the data to prevent any other clients from modifying the data until the client's own changes are complete. In such cases, if another client attempts to modify the data, the attempt fails or is blocked until the lock's owner releases the lock.
  • Pessimistic concurrency can be problematic, because a single user or client may hold on to a lock for a significant period of time, possibly inadvertently. Therefore, the lock could prevent important resources, such as database rows or files, from being released in a timely manner, which can seriously affect the scalability and usability of the application. However, pessimistic concurrency may be appropriate when you need to have complete control over changes made to important resources. Note that it cannot be used if your clients are to work offline, because they are not able to put a lock on data.
  • Optimistic concurrency. Optimistic concurrency does not lock the data .To decide whether an update is actually required, the original data can be sent along with the update request and the changed data. The original data is then checked against the current data to see if it has been updated in the meantime. If the original data and the current data match, the update is executed; otherwise, the request is denied, producing an optimistic failure. To optimize this process, you can use a time stamp or an update counter in the data instead of sending the original data, and in this case only the time stamp or counter needs to be checked.

    Optimistic concurrency provides a good mechanism for updating master data that does not change very often, such as a customer's phone number or address. Optimistic concurrency allows everyone to read the data, and in situations where updates are less likely than read operations, the risk of an optimistic failure may be acceptable. Optimistic concurrency may not be suitable in situations where the data is changed often and where the optimistic updates are likely to fail often.

In most smart client scenarios, including those in which clients are to work offline, optimistic concurrency is the correct approach because it allows multiple clients to work on data at the same time without unnecessarily locking data and affecting all other clients.

For more information about optimistic and pessimistic concurrency, see "Optimistic Concurrency" in the .NET Framework Developer's Guide at http://msdn.microsoft.com/en-us/library/aa0416cz(VS.100).aspx.

Using ADO.NET DataSets to Manage Data

A DataSet is an object that represents one or more relational database tables. Datasets store data in a disconnected cache. The structure of a dataset is similar to that of a relational database: It exposes a hierarchical object model of tables, rows, and columns. In addition, it contains constraints and relationships defined for the dataset.

An ADO.NET DataSet contains a collection of zero or more tables represented by DataTable objects. A DataTable is defined in the System.Data namespace and represents a single table of memory-resident data. It contains a collection of columns represented by a DataColumnCollection and constraints represented by a ConstraintCollection, which together define the schema of the table. A DataTable also contains a collection of rows represented by the DataRowCollection, which contains the data in the table. Along with its current state, a DataRow retains both its current and original versions to identify changes to the values stored in the row.

Datasets can be strongly typed or untyped. A typed DataSet inherits from the DataSet base class but adds strong typed language functionality to the DataSet, allowing users to access content in a more strongly typed programmatic manner. Either type can be used when building applications. However, the Microsoft Visual Studio® development system has more support for typed datasets, and they make programming with the dataset easier and less error prone.

Datasets are particularly useful in a smart client environment, because they offer functionality that helps clients to work with data while offline. They can keep track of local changes made to the data, which helps to synchronize the data with the server and reconcile data conflicts, and they can be used to merge data from different sources.

For more information about working with datasets, see "Introduction to Datasets" in Visual Basic and Visual C# Concepts at http://msdn.microsoft.com/en-us/library/8bw9ksd6(VS.71).aspx.

Merging Data with Datasets

Datasets have the ability to merge the contents of DataSet, DataTable, or DataRow objects into existing datasets. This functionality is particularly useful for keeping track of changes on the client and merging with updated content from the server. Figure 2.2 shows a smart client requesting an update from the Web service, and the new data being returned as a data transfer object (DTO). A DTO is an enterprise pattern that allows you to package all the data required to communicate with a Web service into one object. Using a DTO often means that you can make a single call to a Web service rather than multiple calls.

Ff647254.dataf02(en-us,PandP.10).gif

Figure 2.2   Merging data on the client by using datasets

In this example, when the DTO is returned to the client, the DTO is used to create a new dataset locally on the client.

Note   After a merge operation, ADO.NET does not automatically change the row state from modified to unchanged. Therefore, after merging the new dataset with the local client dataset, you need to invoke the AccceptChanges method on your dataset to reset the RowState property to unchanged.

For more information about using datasets, see "Merging DataSet Contents" in the .NET Framework Developer's Guide at http://msdn.microsoft.com/en-us/library/aszytsd8(VS.80).aspx.

Increasing the Performance of Datasets

Datasets can often contain a large amount of data, which, if passed over the network, can lead to performance problems. Fortunately, with ADO.NET DataSets, you can use the GetChanges method on your datasets to ensure that only the data that is changed in a dataset is communicated between the client and the server, packaging the data in a DTO. Then the data is merged into the dataset at its destination.

Figure 2.3 shows a smart client that makes changes to local data and uses the GetChanges method on a dataset to submit only changed data to the server. The data is transferred to a DTO for performance reasons.

Ff647254.dataf01(en-us,PandP.10).gif

Figure 2.3   Using a DTO to improve performance

The GetChanges method can be used for smart client applications that need to go offline. When an application is again online, you can use the GetChanges method to determine what information has changed and then generate a DTO to communicate with the Web service to ensure that the changes are submitted to a database.

Windows Forms Data Binding

Windows Forms data binding enables you to connect the user interface of your application to the application's underlying data. Windows Forms data binding supports bidirectional binding so you can bind a data structure to the user interface, display the current data values to the user, allow the user to edit the data, and then update the underlying data automatically, using the values entered by the user.

You can use Windows Forms data binding to bind virtually any data structure or object to any property of the user interface controls. You can bind a single item of data to a single property of a control, or you can bind more complex data (for example, a collection of data items or a database table) to the control so it can display all of the data in a data grid or list box.

Note   You can bind any object that supports one or more public properties. You can bind only to public properties of your classes and not to public members.

Windows Forms data binding allows you to provide a flexible, data-driven user interface with your applications. You can use data binding to provide customizable control over the look and feel of your user interface (for example, by binding to control properties such as the background or foreground color, size, image, or icon).

Data binding has many uses. For example, it can be used to:

  • Display read-only data to users.
  • Allow users to update data from the user interface.
  • Provide master-detail views on data.
  • Allow users to explore complex related data items.
  • Provide lookup table functionality, allowing the user interface to connect user-friendly display names.

This section examines some features of data binding and discusses some of the data binding features that you frequently need to implement in a smart client application.

For in-depth information about data binding, see "Windows Forms Data Binding and Objects" at http://msdn.microsoft.com/en-us/library/ef2xyb33(VS.80).aspx.

Windows Forms Data Binding Architecture

Windows Forms data binding provides a flexible infrastructure to bidirectionally connect data to the user the interface. Figure 2.4 shows a schematic representation of the overall architecture of Windows Forms data binding.

Ff647254.bindf01(en-us,PandP.10).gif

Figure 2.4   Architecture of Windows Forms data binding

Windows Forms data binding uses the following objects:

  • Data source. Data sources are the objects that contain the data to be bound to the user interface. Data providers can be any object that has public properties, an array or a collection that supports the IList interface or an instance of a complex data class (for example, DataSet or DataTable).
  • CurrencyManager. The CurrencyManager object keeps track of the current position of the data within an array, collection, or table that is bound to the user interface. The CurrencyManager allows you to bind a collection of data to the user interface and to navigate through that data, updating the user interface to reflect the currently selected item within the collection.
  • PropertyManager. The PropertyManager object is responsible for maintaining the current property of an object that is bound to a control. Both the PropertyManager and CurrencyManager classes inherit from a common base class, BindingManagerBase. All data providers bound to a control to have an associated CurrencyManager or PropertyManager object.
  • BindingContext. Each Windows Form has a default BindingContext object that keeps track of all of the CurrencyManager and PropertyManager objects on the form. The BindingContext object allows you to easily retrieve the CurrencyManager or PropertyManager object for a specific data source. You can assign a specific BindingContext object to a container control (such as a GroupBox, Panel, or TabControl) that contains data-bound controls. Doing so allows each part of your form to be managed by its own CurrencyManager or PropertyManager objects.
  • Binding. The Binding objects are used to create and maintain a simple binding between a single property of a control and either the property of another object or the property of the current object in a list of objects.

Binding Data to Windows Forms Controls

There are a number of properties and methods that you can use to bind to specific Windows Forms controls. Table 2.2 shows some of the more important ones.

Table 2.2   Properties and Methods for Binding to Windows Forms Controls

Property or methodWindows Forms controlDescription
DataSource propertyListControls (for example, ListBox or Combo Box),

DataGrid control

Allows you to specify the data provider object to be bound to the user interface control.
DisplayMember propertyListControlsAllows you to specify the member of the data provider to be displayed to the user.
ValueMember propertyListControlsAllows you to specify the value associated with the displayed value for the internal use of your application.
DataMember propertyDataGrid controlIf the data source contains more than one source of data (for example, if you specify a DataSet that contains multiple tables), use the DataMember property to specify the one to be bound to the grid. (See note following table.)
SetDataBinding methodDataGrid controlAllows you to reset the DataSource method at run time.
Note   If the DataSource is a DataTable, DataView, collection, or array, setting the DataMember property is not required.

You can also use the DataBindings collection property available on all Windows Forms control objects to add Binding objects explicitly to any control object. Binding objects are used to bind a single property on the control to a single data member of the data provider. The following code example adds a binding between the Text property of a text box control to the customer name in the customers table of a data set.

textBox1.DataBindings.Add(
        new Binding( "Text", dataset, "customers.customerName" ) );

When you construct a Binding instance with the Binding constructor, you must specify the name of the control property to bind to, the data source, and the navigation path that resolves to a list or property in the data source. The navigation path can be an empty string, a single property name, or a period-delimited hierarchy of names. You can use a hierarchical navigation path to navigate through data tables and relations in a DataSet object, or over an object model where an object's properties return instances to other objects. If you set the navigation path to an empty string, the ToString method is called on the underlying data source object.

Note   If a property is read-only (that is, the object does not support a set operation for that property), data binding does not by default make the bound Windows Forms control read-only. This can lead to confusion for the user, because the user can edit the value in the user interface, but the value in the bound object will not be updated. Therefore, make sure the read-only flags are set to true for all Windows Forms controls that are bound to read-only properties.

Binding Controls to DataSets

It is often useful to bind controls to datasets. Doing so allows you to display the dataset data in a data grid, and it allows the user to easily update the data. You can bind a data grid control to a DataSet using the following code.

DataSet newDataSet = webServiceProxy.GetDataSet();
this.dataGrid.SetDataBinding( newDataSet, "tableName" );

Sometimes you need to replace the contents of your dataset after all of the bindings with your controls have already been established. However, when you replace existing sets with new ones, the bindings all remain with the old data set.

Rather than manually recreating the data bindings with the new data source, you can use the Merge method of the DataSet class to bring the data from the new data set into the existing one, as shown in the following code example.

DataSet newDataSet = myService.GetDataSet();
this.dataSet1.Clear();   
this.dataSet1.Merge( newDataSet );
Note   To avoid threading issues, you should only update bound data objects on the UI thread. For more information, see Chapter 6: Using Multiple Threads

Navigating Through a Collection of Data

If your data sources contain a collection of items, you can bind the data collection to your Windows Forms controls and navigate through the collection of data one item at a time. The user interface is automatically updated to reflect the current item in the collection.

You can bind to any collection object that supports the IList interface. When you bind to a collection of objects, you can allow the user to navigate through each item in the collection, automatically updating the user interface for each item. Many of the collection and complex data classes provided by the .NET Framework already support the IList interface, so you can easily bind to arrays or complex data such as data rows or data views. For example, any array object that is an instance of the System.Array class implements the IList interface by default, and so can be bound to the user interface. Many ADO.NET objects also support the IList interface, or a derivative of it, allowing these objects to be easily bound too. For example, the DataViewManager, DataSet, DataTable, DataView, and DataColumn classes all support data binding in this way.

Data sources that implement the IList interface are managed by the CurrencyManager object. This object maintains an index into the data collection though its Position property. The index is used to ensure that all controls bound to the data source read and write to the same item in the data collection.

If your form contains controls bound to multiple data sources, it will have multiple CurrencyManager objects, one for each distinct data source. The BindingContext object provides easy access to all CurrencyManager objects on the form. The following code example shows how to increment the current position within a collection of customers.

this.BindingContext[ dataset, "customers" ].Position += 1;

You should use the Count property on the CurrencyManager object as shown in the following code example to ensure that an invalid position is not set.

if ( this.BindingContext[ dataset, "customer" ].Position <
     ( this.BindingContext[ dataset, "customer" ].Count – 1 ) )
{
    this.BindingContext[ dataset, "customers" ].Position += 1;
}

The CurrencyManager object also supports a PositionChanged event. You can create a handler for this event so that you can update your user interface to reflect the current binding position. The following code example displays a label to show the current position and the total number of records.

this.BindingContext[ dataset, "customers" ].PositionChanged +=
        new EventHandler( this.BindingPositionChanged );

The method BindingPositionChanged is implemented as follows.

private void BindingPositionChanged( object sender, System.EventArgs e )
{   
    positionLabel.Text = string.Format( "Record {0} of {1}",
        this.BindingContext[dsPubs1, "authors"].Position + 1, 
        this.BindingContext[dsPubs1, "authors"].Count );
}

Custom Formatting and Data Type Conversion

You can provide custom formatting for data bound to a control using the Format and Parse events of the Binding class. These events allow you to control how data is displayed in the user interface and how data is taken from the user interface and parsed, so that the underlying data can be updated. These events can also be used to convert data types so that the source and destination data types are compatible.

Note   If the data type of the bound property on the control does not match the data type of the data in the data source, an exception is thrown. If you need to bind incompatible types, you should use the Format and Parse events on the Binding object.

The Format event occurs when data is read from the data source and displayed in the control, and when the data is read from the control and used to update the data source. When the data is read from the data source, the Binding object uses the Format event to display the formatted data in the control. When the data is read from the control and used to update the data source, the Binding object parses the data using the Parse event.

The Format and Parse events allow you to create custom formats for displaying data. For example, if the data in a table is of type Decimal, you can display the data in the local currency format by setting the Value property of the ConvertEventArgs object to the formatted value in the Format event. You must consequently format the displayed value in the Parse event.

The following code sample binds an order amount to a text box. The Format and Parse events are used to convert between the string type expected by the text box and the decimal types expected by the data source.

private void BindControl()
{
    Binding binding = new Binding( "Text", dataset,
"customers.custToOrders.OrderAmount" );
    // Add the delegates to the event.
    binding.Format += new ConvertEventHandler( DecimalToCurrencyString );
    binding.Parse  += new ConvertEventHandler( CurrencyStringToDecimal );
    text1.DataBindings.Add( binding );
}
private void DecimalToCurrencyString( object sender, ConvertEventArgs cevent )
{
    // The method converts only to string type. Test this using the 
DesiredType.
    if( cevent.DesiredType != typeof( string ) ) return;

    // Use the ToString method to format the value as currency ("c").
    cevent.Value = ((decimal)cevent.Value).ToString( "c" );
}
private void CurrencyStringToDecimal( object sender, ConvertEventArgs cevent )
{
    // The method converts back to decimal type only. 
    if( cevent.DesiredType != typeof( decimal ) ) return;

    // Converts the string back to decimal using the static Parse method.
    cevent.Value = Decimal.Parse( cevent.Value.ToString(),
                NumberStyles.Currency, null );
}

Using the Model-View-Controller Pattern to Implement Data Validation

Binding a data structure to a user interface element allows the user to edit the data and ensures that these changes are then written back to the underlying data structure. Often, you will need to check the changes that the user makes to the data to ensure that the values entered are valid.

The Format and Parse events described in the previous section provide one way to intercept the changes the user makes to the data, so that the data can be checked for validity. However, this approach requires that the data validation logic be implemented together with the custom formatting code, typically at the form level. Implementing these two responsibilities together in the event handlers can make your code difficult to understand and maintain.

A more elegant approach is to design your code so that it uses the Model-View-Controller (MVC) pattern. The pattern provides natural separation of the various responsibilities involved with editing and changing data through data binding. You should implement custom formatting within the form that is responsible for presenting the data in a certain format, and then associate the validation rules with the data itself, so that the rules can be reused across multiple forms.

In the MVC pattern, the data itself is encapsulated in a model object. The view object is the Windows Forms control that the data is bound to. All changes to the model are handled by an intermediary controller object, which is responsible for providing access to the data and for controlling any changes made to the data through the view object. The controller object provides a natural location for validating changes made to the data, and all user interface validation logic should be implemented here.

Figure 2.5 depicts the structural relationship between the three objects in the MVC pattern.

Ff647254.uipfig02(en-us,PandP.10).gif

Figure 2.5   Objects in Model-View-Controller pattern

Using a controller object in this way has a number of advantages. You can configure a generic controller to provide custom validation rules, which are configurable at run time according to some contextual information (for example, the role of the user). Alternatively, you can provide a number of controller objects, with each controller object implementing specific validation rules, and then select the appropriate object at run time. Either way, because all validation logic is encapsulated in the controller object, the view and model objects do not need to change.

In addition to separating data, validation logic, and user interface controls, the MVC model gives you a simple way to automatically update the user interface when the underlying data changes. The controller object is responsible for notifying the user interface when changes to the data have occurred by some other programmatic means. Windows Forms data binding listens for events generated by the objects that are bound to the controls so that the user interface can automatically respond to changes made to the underlying data.

To implement automatic updates of the user interface, you should ensure that the controller implements a change notification event for each property that may change. Events should follow the naming convention <property>Changed, where <property> is the name of the property. For example, if the controller supports a Name property, it should also support a NameChanged event. If the value of the name property changes, this event should be fired so Windows Forms data binding can handle it and update the user interface.

The following code example defines a Customer object, which implements a Name property. The CustomerController object handles the validation logic for a Customer object and supports a Name property, which in turn represents the Name property on the underlying Customer object. This controller fires an event whenever the name is changed.

public class Customer
{
    private string _name;
    public Customer( string name ) { _name = name; }
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}
public class CustomerController
{
    private Customer _customer = null;
    public event EventHandler NameChanged;
    public Customer( Customer customer )
    {
        this._customer = customer;
    }
    public string Name
    {
        get { return _customer.Name; }
        set
        {
             // TODO: Validate new name to make sure it is valid.
            _customer.Name = value; 
            // Notify bound control of change.
            if ( NameChanged != null )
                NameChanged( this, EventArgs.Empty );
        }
    }
}
Note   Customer data source members need to be initialized when they are declared. In the preceding example, the customer.Name member needs to be initialized to an empty string. This is because the .NET Framework does not have a chance to interact with the object and set the default setting of an empty string before the data binding occurs. If the customer data source member is not initialized, the attempt to retrieve a value from an uninitialized variable causes a run-time exception.

In the following code example, the form has a TextBox object, textbox1, which needs to be bound to the customer's name. The code binds the Text property of the TextBox object to the Name property of the controller.

_customer = new Customer( "Kelly Blue" );
_controller = new CustomerController( _customer );
Binding binding = new Binding( "Text", _controller, "Name" );
textBox1.DataBindings.Add( binding );

If the name of the customer is changed (using the Name property on the controller), the NameChanged event is fired and the text box is automatically updated to reflect the new name value.

Updating the User Interface When the Underlying Data Changes

You can use Windows Forms data binding to automatically update the user interface when the corresponding underlying data changes. You do this by implementing a change notification event on the bound object. Change notification events are named according to the following convention.

public event EventHandler <propertyName>Changed;

So, for example, if you bind an object's Name property to the user interface and then that object's name changes as a result of some other processing, you can automatically update the user interface to reflect the new Name value by implementing the NameChanged event on the bound object.

Summary

There are many different considerations involved in determining how to handle data in your smart clients. You need to determine whether and how to cache your data, and how to handle data concurrency issues. You will often decide to use ADO.NET datasets to handle your data, and you will probably also decide to take advantage of the Windows Forms data binding functionality.

In many cases, read-only reference data and transient data needs to be dealt with differently. Because smart clients typically use both types of data, you need to determine the best way to handle each category of data in your application.

patterns & practices Developer Center

Show:
© 2014 Microsoft. All rights reserved.