Tải bản đầy đủ (.pdf) (50 trang)

Apress Expert C sharp 2005 (Phần 7) docx

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (491.93 KB, 50 trang )

A CallByName() helper method is used to abstract the use of reflection to retrieve the property
value based on the property name. It simply uses reflection to get a
PropertyInfo object for the
specified property, and then uses it to retrieve the property value.
If the property value is
null or is an empty string, then the rule is violated, so the Description
property of the RuleArgs object is set to describe the nature of the rule. Then false is returned from
the rule method to indicate that the rule is broken. Otherwise, the rule method simply returns
true
to indicate that that rule is not broken.
This rule is used within a business object by associating it with a property. A business object
does this by overriding the
AddBusinessRules() method defined by BusinessBase. Such code would
look like this (assuming a
using statement for Csla.Validation):
[Serializable()]
public class Customer : BusinessBase<Customer>
{
protected override void AddBusinessRules()
{
ValidationRules.AddRule(
new RuleHandler(CommonRules.StringRequired),
"Name");
}
// rest of class…
}
This associates the rule method with the Name property so that the PropertyHasChanged() call
within the property’s
set block will automatically invoke the rule. You’ll see this and other rule meth-
ods used in Chapter 8 within the sample application’s business objects.
StringMaxLength


A slightly more complex variation is where the rule method needs extra information beyond that
provided by the basic
RuleArgs parameter. In these cases, the RuleArgs class must be subclassed to
create a new object that adds the extra information. A rule method to enforce a maximum length
on a string, for instance, requires the maximum length value.
Custom RuleArgs Class
Here’s a subclass of RuleArgs that provides the maximum length value:
public class MaxLengthRuleArgs : RuleArgs
{
private int _maxLength;
public int MaxLength
{
get { return _maxLength; }
}
public MaxLengthRuleArgs(
string propertyName, int maxLength)
: base(propertyName)
{
_maxLength = maxLength;
}
CHAPTER 5 ■ COMPLETING THE FRAMEWORK274
6323_c05_final.qxd 2/27/06 1:28 PM Page 274
public override string ToString()
{
return base.ToString() + "!" + _maxLength.ToString();
}
}
All subclasses of RuleArgs will follow this basic structure. First, the extra data to be provided
is stored in a field and exposed through a property:
private int _maxLength;

public int MaxLength
{
get { return _maxLength; }
}
The data provided here will obviously vary based on the needs of the rule method. The construc-
tor must accept the name of the property to be validated, and of course, the extra data. The property
name is provided to the
RuleArgs base class, and the extra data is stored in the field declared in the
preceding code:
public MaxLengthRuleArgs(
string propertyName, int maxLength)
: base(propertyName)
{
_maxLength = maxLength;
}
Finally, the ToString() method is overridden. This is required! Recall that in Chapter 3 this value
is used to uniquely identify the corresponding rule within the list of broken rules for an object. The
ToString() value of the RuleArgs object is combined with the name of the rule method to generate
the unique rule name.
This means that the
ToString() implementation must return a string representation of the rule
that is unique within a given business object. Typically, this can be done by combining the name of
the rule (from the
RuleArgs base class) with whatever extra data you are storing in your custom object:
public override string ToString()
{
return base.ToString() + "!" + _maxLength.ToString();
}
The RuleArgs base class implements a ToString() method that returns a relatively unique value
(the name of the property). By combining this with the extra data stored in this custom class, the result-

ing name should be unique within the business object.
Rule Method
With the custom RuleArgs class defined, it can be used to implement a rule method. The
StringMaxLength() rule method looks like this:
public static bool StringMaxLength(
object target, RuleArgs e)
{
int max = ((MaxLengthRuleArgs)e).MaxLength;
string value = (string)Utilities.CallByName(
target, e.PropertyName, CallType.Get);
if (!String.IsNullOrEmpty(value) && (value.Length > max))
CHAPTER 5 ■ COMPLETING THE FRAMEWORK 275
6323_c05_final.qxd 2/27/06 1:28 PM Page 275
{
e.Description = String.Format(
"{0} can not exceed {1} characters",
e.PropertyName, max.ToString());
return false;
}
return true;
}
This is similar to the StringRequired() rule method, except that the RuleArgs parameter is cast
to the
MaxLengthRuleArgs type so that the MaxLength value can be retrieved. That value is then com-
pared to the length of the specified property from the target object to see if the rule is broken or not.
■Note It might seem like the RuleArgs parameter should just be of type MaxLengthRuleArgs. But it is
important to remember that this method must conform to the
RuleHandler delegate defined in Chapter 3;
and that defines the parameter as type RuleArgs.
A business object’s AddBusinessRules() method would associate a property to this rule like this:

protected override void AddBusinessRules()
{
ValidationRules.AddRule(
new RuleHandler(CommonRules.StringMaxLength),
new CommonRules.MaxLengthRuleArgs("Name", 50));
}
Remember that in Chapter 3 the ValidationRules.AddRule() method included an overload that
accepted a rule method delegate along with a
RuleArgs object. In this case, the RuleArgs object is an
instance of
MaxLengthRuleArgs, initialized with the property name and the maximum length allowed
for the property.
The
CommonRules class includes other similar rule method implementations that you may choose
to use as is, or as the basis for creating your own library of reusable rules for an application.
Data Access
Almost all applications employ some data access. Obviously, the CSLA .NET framework puts heavy
emphasis on enabling data access through the data portal, as described in Chapter 4. Beyond the
basic requirement to create, read, update, and delete data, however, there are other needs.
During the process of reading data from a database, many application developers find them-
selves writing repetitive code to eliminate
null database values. SafeDataReader is a wrapper
around any ADO.NET data reader object that automatically eliminates any
null values that might
come from the database.
When creating many web applications using either Web Forms or Web Services, data must be
copied into and out of business objects. In the case of Web Forms data binding, data comes from the
page in a dictionary of name/value pairs, which must be copied into the business object’s proper-
ties. With Web Services, the data sent or received over the network often travels through simple data
transfer objects (DTOs). The properties of those DTOs must be copied into or out of a business object

within the web service. The
DataMapper class contains methods to simplify these tasks.
CHAPTER 5 ■ COMPLETING THE FRAMEWORK276
6323_c05_final.qxd 2/27/06 1:28 PM Page 276
SafeDataReader
null values should be allowed in database columns for only two reasons. The first is when the busi-
ness rules dictate that the application cares about the difference between a value that was never
entered and a value that is zero (or an empty string). In other words, the end user actually cares about
the difference between
"" and null, or between 0 and null. There are applications where this matters—
where the business rules revolve around whether a field ever had a value (even an empty one) or
never had a value at all.
The second reason for using a
null value is when a data type doesn’t intrinsically support the
concept of an empty field. The most common example is the SQL
DateTime data type, which has no
way to represent an empty date value; it
always contains a valid date. In such a case, null values in
the database column are used specifically to indicate an empty date.
Of course, these two reasons are mutually exclusive. When using
null values to differentiate
between an empty field and one that never had a value, you need to come up with some other scheme
to indicate an empty
DateTime field. The solution to this problem is outside the scope of this book—
but thankfully, the problem itself is quite rare.
The reality is that very few applications ever care about the difference between an empty value
and one that was never entered, so the first scenario seldom applies. If it
does apply to your applica-
tion, then dealing with
null values at the database level isn’t an issue, because you’ll use nullable types

from the database all the way through to the UI. In this case, you can ignore
SafeDataReader entirely,
as it has no value for your application.
But for
most applications, the only reason for using null values is the second scenario, and this
one is quite common. Any application that uses date values, and for which an empty date is a valid
entry, will likely use
null to represent an empty date.
Unfortunately, a whole lot of poorly designed databases allow
null values in columns where
neither scenario applies, and we developers have to deal with them. These are databases that con-
tain
null values even if the application makes no distinction between a 0 and a null.
Writing defensive code to guard against tables in which
null values are erroneously allowed
can quickly bloat data access code and make it hard to read. To avoid this, the
SafeDataReader class
takes care of these details automatically, by eliminating
null values and converting them into a set
of default values.
As a rule, data reader objects are
sealed, meaning that you can’t simply subclass an existing
data reader class (such as
SqlDataReader) and extend it. However, like the SmartDate class with
DateTime, it is quite possible to encapsulate or “wrap” a data reader object.
Creating the SafeDataReader Class
To ensure that SafeDataReader can wrap any data reader object, it relies on the root System.Data.
IDataReader
interface that’s implemented by all data reader objects. Also, since SafeDataReader is
to

be a data reader object, it must implement that interface as well:
public class SafeDataReader : IDataReader
{
private IDataReader _dataReader;
protected IDataReader DataReader
{
get { return _dataReader; }
}
public SafeDataReader(IDataReader dataReader)
{
_dataReader = dataReader;
}
}
CHAPTER 5 ■ COMPLETING THE FRAMEWORK 277
6323_c05_final.qxd 2/27/06 1:28 PM Page 277
The class defines a field to store a reference to the real data reader that it is encapsulating. That
field is exposed as a protected property as well, allowing for subclasses of
SafeDataReader in the future.
There’s also a constructor that accepts the
IDataReader object to be encapsulated as a parameter.
This means that ADO.NET code in a business object’s
DataPortal_Fetch() method might appear
as follows:
SafeDataReader dr = new SafeDataReader(cm.ExecuteReader());
The ExecuteReader() method returns an object that implements IDataReader (such as
SqlDataReader) that is used to initialize the SafeDataReader object. The rest of the code in
DataPortal_Fetch() can use the SafeDataReader object just like a regular data reader object, because
it implements
IDataReader. The benefit, though, is that the business object’s data access code never
has to worry about getting a

null value from the database.
The implementation of
IDataReader is a lengthy business—it contains a lot of methods—so
I’m not going to go through all of it here. Instead I’ll cover a few methods to illustrate how the over-
all class is implemented.
GetString
There are two overloads for each method that returns column data, one that takes an ordinal col-
umn position, and the other that takes the
string name of the property. This second overload is
a convenience, but makes the code in a business object much more readable. All the methods that
return column data are “
null protected” with code like this:
public string GetString(string name)
{
return GetString(_dataReader.GetOrdinal(name));
}
public virtual string GetString(int i)
{
if( _dataReader.IsDBNull(i))
return string.Empty;
else
return _dataReader.GetString(i);
}
If the value in the database is null, the method returns some more palatable value—typically,
whatever passes for “empty” for the specific data type. If the value isn’t
null, it simply returns the
value from the underlying data reader object.
For
string values, the empty value is string.Empty; for numeric types, it is 0; and for Boolean
types, it is

false. You can look at the full code for SafeDataReader to see all the translations.
Notice that the
GetString() method that actually does the translation of values is marked as
virtual. This allows you to override the behavior of any of these methods by creating a subclass of
SafeDataReader.
The
GetOrdinal() method translates the column name into an ordinal (numeric) value, which
can be used to actually retrieve the value from the underlying
IDataReader object. GetOrdinal()
looks like this:
public int GetOrdinal(string name)
{
return _dataReader.GetOrdinal(name);
}
Every data type supported by IDataReader (and there are a lot of them) has a pair of methods
that reads the data from the underling
IDataReader object, replacing null values with empty default
values as appropriate.
CHAPTER 5 ■ COMPLETING THE FRAMEWORK278
6323_c05_final.qxd 2/27/06 1:28 PM Page 278
GetDateTime and GetSmartDate
Most types have “empty” values that are obvious, but DateTime is problematic as it has no “empty”
value:
public DateTime GetDateTime(string name)
{
return GetDateTime(_dataReader.GetOrdinal(name));
}
public virtual DateTime GetDateTime(int i)
{
if (_dataReader.IsDBNull(i))

return DateTime.MinValue;
else
return _dataReader.GetDateTime(i);
}
The minimum date value is arbitrarily used as the “empty” value. This isn’t perfect, but it does
avoid returning a
null value or throwing an exception. A better solution may be to use the SmartDate
type instead of DateTime. To simplify retrieval of a date value from the database into a SmartDate,
SafeDataReader implements two variations of a GetSmartDate() method:
public Csla.SmartDate GetSmartDate(string name)
{
return GetSmartDate(_dataReader.GetOrdinal(name), true);
}
public virtual Csla.SmartDate GetSmartDate(int i)
{
return GetSmartDate(i, true);
}
public Csla.SmartDate GetSmartDate(string name, bool minIsEmpty)
{
return GetSmartDate(_dataReader.GetOrdinal(name), minIsEmpty);
}
public virtual Csla.SmartDate GetSmartDate(
int i, bool minIsEmpty)
{
if (_dataReader.IsDBNull(i))
return new Csla.SmartDate(minIsEmpty);
else
return new Csla.SmartDate(
_dataReader.GetDateTime(i), minIsEmpty);
}

Data access code in a business object can choose either to accept the minimum date value as
being equivalent to “empty” or to retrieve a
SmartDate that understands the concept of an empty date:
SmartDate myDate = dr.GetSmartDate(0);
or
SmartDate myDate = dr.GetSmartDate(0, false);
CHAPTER 5 ■ COMPLETING THE FRAMEWORK 279
6323_c05_final.qxd 2/27/06 1:28 PM Page 279
GetBoolean
Likewise, there is no “empty” value for the bool type:
public bool GetBoolean(string name)
{
return GetBoolean(_dataReader.GetOrdinal(name));
}
public virtual bool GetBoolean(int i)
{
if (_dataReader.IsDBNull(i))
return false;
else
return _dataReader.GetBoolean(i);
}
The code arbitrarily returns a false value in this case.
Other Methods
The IDataReader interface also includes a number of methods that don’t return column values, such
as the
Read() method:
public bool Read()
{
return _dataReader.Read();
}

In these cases, it simply delegates the method call down to the underlying data reader object for
it to handle. Any return values are passed back to the calling code, so the fact that
SafeDataReader is
involved is entirely transparent.
The
SafeDataReader class can be used to simplify data access code dramatically, any time an
object is working with tables in which
null values are allowed in columns where the application
doesn’t care about the difference between an empty and a
null value. If your application does care
about the use of
null values, you can simply use the regular data reader objects instead.
DataMapper
Later in this chapter, you’ll see the implementation of a CslaDataSource control that allows business
developers to use Web Forms data binding with CSLA .NET–style business objects. When Web Forms
data binding needs to insert or update data, it provides the data elements in the form of a dictionary
object of name/value pairs. The name is the name of the property to be updated, and the value is the
value to be placed into the property of the business object.
Copying the values isn’t hard—the code looks something like this:
cust.FirstName = e.Values["FirstName"].ToString();
cust.LastName = e.Values["LastName"].ToString();
cust.City = e.Values["City"].ToString();
Unfortunately, this is tedious code to write and debug; and if your object has a lot of properties,
this can add up to a lot of lines of code. An alternative is to use reflection to automate the process of
copying the values.
CHAPTER 5 ■ COMPLETING THE FRAMEWORK280
6323_c05_final.qxd 2/27/06 1:28 PM Page 280
■Tip If you feel that reflection is too slow for this purpose, you can continue to write all the mapping code by
hand. Keep in mind, however, that data binding uses reflection extensively anyway, so this little bit of additional
reflection is not likely to cause any serious performance issues.

A similar problem exists when building web services. Business objects should not be returned
directly as a result of a web service, as that would break encapsulation. In such a case, your business
object interface would become part of the web service interface, preventing you from ever adding
or changing properties on the object without running the risk of breaking any clients of the web
service.
Instead, data should be copied from the business object into a DTO, which is then returned to
the web service client. Conversely, data from the client often comes into the web service in the form
of a DTO. These DTOs are often created based on WSDL or an XSD defining the contract for the data
being passed over the web service.
The end result is that the code in a web service has to map property values from business objects
to and from DTOs. That code often looks like this:
cust.FirstName = dto.FirstName;
cust.LastName = dto.LastName;
cust.City = dto.City;
Again, this isn’t hard code to write, but it’s tedious and could add up to many lines of code.
The
DataMapper class uses reflection to help automate these data mapping operations, from
either a collection implementing
IDictionary or an object with public properties.
In both cases, it is possible or even likely that some properties can’t be mapped. Business objects
often have read-only properties, and obviously it isn’t possible to set those values. Yet the
IDictionary
or DTO may have a value for that property. It is up to the business developer to deal on a case-by-case
basis with properties that can’t be automatically mapped.
The
DataMapper class will accept a list of property names to be ignored. Properties matching
those names simply won’t be mapped during the process. Additionally,
DataMapper will accept a
Boolean flag that can be used to suppress exceptions during the mapping process. This can be
used simply to ignore any failures.

Setting Values
The core of the DataMapper class is the SetValue() method. This method is ultimately responsible
for putting a value into a specified property of a target object:
private static void SetValue(
object target, string propertyName, object value)
{
PropertyInfo propertyInfo =
target.GetType().GetProperty(propertyName);
if (value == null)
propertyInfo.SetValue(target, value, null);
else
{
Type pType =
Utilities.GetPropertyType(propertyInfo.PropertyType);
if (pType.Equals(value.GetType()))
CHAPTER 5 ■ COMPLETING THE FRAMEWORK 281
6323_c05_final.qxd 2/27/06 1:28 PM Page 281
{
// types match, just copy value
propertyInfo.SetValue(target, value, null);
}
else
{
// types don't match, try to coerce
if (pType.Equals(typeof(Guid)))
propertyInfo.SetValue(
target, new Guid(value.ToString()), null);
else
propertyInfo.SetValue(
target, Convert.ChangeType(value, pType), null);

}
}
}
Reflection is used to retrieve a PropertyInfo object corresponding to the specified property on
the target object. The specific type of the property’s return value is retrieved using a
GetPropertyType()
helper method in the Utilities class. That helper method exists to deal with the possibility that the
property could return a value of type
Nullable<T>. If that happens, the real underlying data type
(behind the
Nullable<T> type) must be returned. Here’s the GetPropertyType() method:
public static Type GetPropertyType(Type propertyType)
{
Type type = propertyType;
if (type.IsGenericType &&
(type.GetGenericTypeDefinition() == typeof(Nullable)))
return type.GetGenericArguments()[0];
return type;
}
If Nullable<T> isn’t involved, then the original type passed as a parameter is simply returned.
But if
Nullable<T> is involved, then the first generic argument (the value of T) is returned instead:
return type.GetGenericArguments()[0];
This ensures that the actual data type of the property is used rather than Nullable<T>.
Back in the
SetValue() method, the PropertyInfo object has a SetValue() method that sets the
value of the property, but it requires that the new value have the same data type as the property itself.
Given that the values from an
IDictionary collection or DTO may not exactly match the prop-
erty types on a business object,

DataMapper.SetValue() attempts to coerce the original type to the
property type before setting the property on the target object.
To do this, it retrieves the type of the target property. If the new value is not
null, then the type
of the new value is compared to the type of the property to see if they match:
if (pType.Equals(value.GetType()))
{
// types match, just copy value
propertyInfo.SetValue(target, value, null);
}
If they do match, then the property is set to the new value. If they don’t match, then there’s an
attempt to coerce the new value to the same type as the property:
CHAPTER 5 ■ COMPLETING THE FRAMEWORK282
6323_c05_final.qxd 2/27/06 1:28 PM Page 282
// types don't match, try to coerce
if (pType.Equals(typeof(Guid)))
propertyInfo.SetValue(
target, new Guid(value.ToString()), null);
else
propertyInfo.SetValue(
target, Convert.ChangeType(value, pType), null);
For most common data types, the Convert.ChangeType() method will work fine. It handles
string, date, and primitive data types in most cases. But
Guid values won’t convert using that tech-
nique (because
Guid doesn’t implement IConvertible), so they are handled as a special case, by
using
ToString() to get a string representation of the value, and using that to create a new instance
of a
Guid object.

If the coercion fails,
Convert.ChangeType() will throw an exception. In such a case, the business
developer will have to manually set that particular property; adding that property name to the list of
properties ignored by
DataMapper.
Mapping from IDictionary
A collection that implements IDictionary is effectively a name/value list. The DataMapper.Map()
method assumes that the names in the list correspond directly to the names of properties on the
business object to be loaded with data. It simply loops through all the keys in the dictionary,
attempting to set the value of each entry into the target object:
public static void Map(
System.Collections.IDictionary source,
object target, bool suppressExceptions,
params string[] ignoreList)
{
List<string> ignore = new List<string>(ignoreList);
foreach (string propertyName in source.Keys)
{
if (!ignore.Contains(propertyName))
{
try
{
SetValue(target, propertyName, source[propertyName]);
}
catch
{
if (!suppressExceptions)
throw new ArgumentException(
String.Format("{0} ({1})",
Resources.PropertyCopyFailed, propertyName), ex);

}
}
}
}
While looping through the key values in the dictionary, the ignoreList is checked on each
entry. If the key from the dictionary is in the ignore list, then that value is ignored.
Otherwise, the
SetValue() method is called to assign the new value to the specified property
of the target object.
If an exception occurs while a property is being set, it is caught. If
suppressExceptions is true,
then the exception is ignored; otherwise it is wrapped in an
ArgumentException. The reason for
CHAPTER 5 ■ COMPLETING THE FRAMEWORK 283
6323_c05_final.qxd 2/27/06 1:28 PM Page 283
wrapping it in a new exception object is so the property name can be included in the message
returned to the calling code. That bit of information is invaluable when using the
Map() method.
Mapping from an Object
Mapping from one object to another is done in a similar manner. The primary exception is that the
list of source property names doesn’t come from the keys in a dictionary, but rather must be retrieved
from the source object.
■Note The Map() method can be used to map to or from a business object.
The GetSourceProperties() method retrieves the list of properties from the source object:
private static PropertyInfo[] GetSourceProperties(Type sourceType)
{
List<PropertyInfo> result = new List<PropertyInfo>();
PropertyDescriptorCollection props =
TypeDescriptor.GetProperties(sourceType);
foreach (PropertyDescriptor item in props)

if (item.IsBrowsable)
result.Add(sourceType.GetProperty(item.Name));
return result.ToArray();
}
This method filters out methods that are marked as [Browsable(false)]. This is useful when
the source object is a CSLA .NET–style business object, as the
IsDirty, IsNew, and similar properties
from
BusinessBase are automatically filtered out. The result is that GetSourceProperties() returns
a list of properties that are subject to data binding.
First, reflection is invoked by calling the
GetProperties() method to retrieve a collection of
PropertyDescriptor objects. These are similar to the more commonly used PropertyInfo objects,
but they are designed to help support data binding. This means they include an
IsBrowsable prop-
erty that can be used to filter out those properties that aren’t browsable.
A
PropertyInfo object is added to the result list for all browsable properties, and then that
result list is converted to an array and returned to the calling code.
The calling code is an overload of the
Map() method that accepts two objects rather than an
IDictionary and an object:
public static void Map(
object source, object target,
bool suppressExceptions,
params string[] ignoreList)
{
List<string> ignore = new List<string>(ignoreList);
PropertyInfo[] sourceProperties =
GetSourceProperties(source.GetType());

foreach (PropertyInfo sourceProperty in sourceProperties)
{
string propertyName = sourceProperty.Name;
if (!ignore.Contains(propertyName))
{
try
CHAPTER 5 ■ COMPLETING THE FRAMEWORK284
6323_c05_final.qxd 2/27/06 1:28 PM Page 284
{
SetValue(
target, propertyName,
sourceProperty.GetValue(source, null));
}
catch (Exception ex)
{
if (!suppressExceptions)
throw new ArgumentException(
String.Format("{0} ({1})",
Resources.PropertyCopyFailed, propertyName), ex);
}
}
}
}
The source object’s properties are retrieved into an array of PropertyInfo objects:
PropertyInfo[] sourceProperties =
GetSourceProperties(source.GetType());
Then the method loops through each element in that array, checking each one against the list
of properties to be ignored. If the property isn’t in the ignore list, the
SetValue() method is called to
set the property on the target object. The

GetValue() method on the PropertyInfo object is used to
retrieve the value from the source object:
SetValue(
target, propertyName,
sourceProperty.GetValue(source, null));
Exceptions are handled (or ignored) just like they are when copying from an IDictionary.
While the
DataMapper functionality may not be useful in all cases, it is useful in many cases,
and can dramatically reduce the amount of tedious data-copying code a business developer needs
to write to use data binding in Web Forms or to implement Web Services.
Reporting
When discussing report generation and objects, it is important to divide the idea of report generation
into two groups: small reports and large reports.
Some enterprise resource planning (ERP) and manufacturing resource planning (MRP) systems
make exactly this distinction: small reports are often called
lists, while large reports are called reports.
Lists can be generated at any time and are displayed immediately on the client, while reports are typ-
ically generated in the background and are later displayed through a viewer or printed out.
Of course, the exact delineation between a “small” and a “large” report varies. Ultimately, small
reports require small enough amounts of data that it’s reasonable to transfer that data to the client
immediately upon a user request. Large reports require too much data to transfer to the client imme-
diately, or they take too long to generate to have the user’s machine (or browser) tied up while
waiting for it to complete.
The problem faced with reporting is twofold. First, pulling back large amounts of data from
the server to the client just to generate a report is slow. In fact, it is a just a poor idea and should be
avoided. Large reports should be generated using report engines that physically run on or near the
database server to minimize the amount of data transferred across the network.
Second, for reports that require smaller data sets that
can be efficiently returned to the client
machine, few of the major report engine tools support data binding against custom objects. Reports

CHAPTER 5 ■ COMPLETING THE FRAMEWORK 285
6323_c05_final.qxd 2/27/06 1:28 PM Page 285
generated with popular tools such as Crystal Reports or Active Reports can only be generated
against ADO.NET objects such as the
DataSet.
■Tip To be fair, these report engines also work in an “unbound” mode, in which you have the opportunity to
supply the data to populate the report manually. This technique can certainly be used with business objects. You
can write code to pull the data out of your objects and provide that data to the report engine as it generates the
report. The trouble is that this is a lot of work, especially when compared to just binding the report to a DataSet.
Microsoft SQL Server 2005 Reporting Services and Developer Express Xtra Reports both support
data binding against objects in a manner similar to Windows Forms. Ideally, in the future, more of the
major report engine vendors will support data binding against objects just like Windows Forms and
Web Forms do, but that’s not the case today. Today, you can either generate the report from a
DataSet
or use the engines in unbound mode and provide the data manually.
To enable the use of major report-generation tools, the
ObjectAdapter class implements a con-
verter to load a
DataSet with data from objects. It allows you to convert an object into a DataSet. You
can then generate reports in standard report engines such as Crystal Reports or Active Reports by
using that
DataSet.
This approach is useful for lists, but not reports. By my definition, lists require relatively small
amounts of data, so it’s acceptable to transfer that data to a client and generate the report there.
Reports, on the other hand, require processing large amounts of data, and the closer you can do this
to the database the better. In this case, directly using Crystal Enterprise or some other server-based
reporting tool to generate the report physically close to or in the database is often the best solution.
ObjectAdapter
The Csla.Data.ObjectAdapter class is a utility that generates a DataSet (or more accurately, a
DataTable in a DataSet) based on an object (or a collection of objects). This isn’t terribly difficult,

because reflection can be used to get a list of the properties or fields on the objects, and then loop
through the objects’ properties to populate the
DataTable with their values.
ObjectAdapter is somewhat similar to a data adapter object such as OleDbDataAdapter, in that
it implements a
Fill() method that fills a DataSet with data from an object or collection.
To implement a
Fill() method that copies data from a source, such as a business object, into
a
DataSet, ObjectAdapter needs to support a certain amount of basic functionality. In ADO.NET,
data is stored in a
DataTable, and then that DataTable is held in a DataSet. This means that object
data will be copied into a
DataTable object.
To do this,
ObjectAdapter needs to get a list of the properties exposed by the source object. That
list will be used to define the list of columns to be created in the target
DataTable object. Alternatively,
it will also support the concept of a preexisting
DataTable that already contains columns. In that case,
ObjectAdapter will attempt to find properties in the source object that match the columns that already
exist in the target
DataTable object.
Also, rather obviously, the data values from the original data source must be retrieved. Reflection
will be used to do this because it allows dynamic retrieval of the values.
Operational Scope
Figure 5-5 illustrates the possible data sources supported by the ObjectAdapter class.
■Tip The code could be simplified by only supporting binding to an object—but by supporting any valid data
source (including ADO.NET objects, or arrays of simple values), it provides a more flexible solution.
CHAPTER 5 ■ COMPLETING THE FRAMEWORK286

6323_c05_final.qxd 2/27/06 1:28 PM Page 286
Ultimately, a list of column, property, or field names will be retrieved from the data source,
whether that be a
DataView, an array or collection, simple types (such as int or string) or complex
types (such as a
struct or an object).
In the end, all data sources implement the
IList interface that’s defined in the .NET Frame-
work. However, sometimes some digging is required to find that interface; or it must be added by
creating a collection. Some data source objects, such as a
DataSet, don’t expose IList directly.
Instead, they expose
IListSource, which can be used to get an IList. In the case of simple types
such as a string or a business object, an
ArrayList is created and the item is placed inside it, thus
providing an
IList with which to work.
Fill Method
Like the OleDbDataAdapter, the ObjectAdapter implements a Fill() method (actually, several over-
loads of
Fill() for easy use). In the end, though, they all route to a single Fill() method that fills
a
DataTable from data in a source object:
public void Fill(DataTable dt, object source)
{
if (source == null)
throw new ArgumentException(Resources.NothingNotValid);
// get the list of columns from the source
List<string> columns = GetColumns(source);
if (columns.Count < 1) return;

// create columns in DataTable if needed
foreach (string column in columns)
if (!dt.Columns.Contains(column))
dt.Columns.Add(column);
CHAPTER 5 ■ COMPLETING THE FRAMEWORK 287
Figure 5-5. Data sources supported by ObjectAdapter
6323_c05_final.qxd 2/27/06 1:28 PM Page 287
// get an IList and copy the data
CopyData(dt, GetIList(source), columns);
}
The first thing this method does is get a list of column names (typically, the public properties
and fields) from the data source. It does this by calling a
GetColumns() method (which will be cov-
ered later).
Next, the target
DataTable is checked to ensure that it has a column corresponding to every col-
umn name retrieved from
GetColumns(). If any columns are missing, they are added to the DataTable:
foreach (string column in columns)
if (!dt.Columns.Contains(column))
dt.Columns.Add(column);
This ensures that all properties or fields from the data source have a column in the DataTable
so they can be copied. With that done, all that remains is to initiate the copy of data from the source
object to the
DataTable:
CopyData(dt, GetIList(source), columns);
Unfortunately, this is complicated slightly by the fact that the source object could be one of
several object types. The
GetIList() method sorts that out and ensures that it is an IList that is
passed to the

CopyData() method.
GetIList() looks like this:
private IList GetIList(object source)
{
if (source is IListSource)
return ((IListSource)source).GetList();
else if (source is IList)
return source as IList;
else
{
// this is a regular object - create a list
ArrayList col = new ArrayList();
col.Add(source);
return col;
}
}
If the source object implements the IListSource interface, then its GetList() method is used
to retrieve the underlying
IList. This is typically the case with a DataTable, for instance.
If the source object directly implements
IList, then it is simply cast and returned.
Otherwise, the source object is assumed to be a simple type (such as string), a struct, or an
object. In order to return an
IList in this case, an ArrayList is created, the source object is added
to the
ArrayList, and it is returned as the result. Since ArrayList implements IList, the end result
is that an
IList is returned.
■Note This is the same technique used by the BindingSource object in Windows Forms data binding when
a simple type or object is provided as a data source for data binding.

Getting the Column Names
The Fill() method calls a GetColumns() method to retrieve a list of the column names from the
source object. If the source object is an ADO.NET
DataView, it really will return a list of column
CHAPTER 5 ■ COMPLETING THE FRAMEWORK288
6323_c05_final.qxd 2/27/06 1:28 PM Page 288
names. But more commonly, the source object will be a business object, in which case the list of
public properties and fields is returned.
GetColumns Method
The GetColumns() method determines the type of the source object and dispatches the work to type-
specific helper methods:
private List<string> GetColumns(object source)
{
List<string> result;
// first handle DataSet/DataTable
object innerSource;
IListSource iListSource = source as IListSource;
if (iListSource != null)
innerSource = iListSource.GetList();
else
innerSource = source;
DataView dataView = innerSource as DataView;
if (dataView != null)
result = ScanDataView(dataView);
else
{
// now handle lists/arrays/collections
IEnumerable iEnumerable = innerSource as IEnumerable;
if (iEnumerable != null)
{

Type childType = Utilities.GetChildItemType(
innerSource.GetType());
result = ScanObject(childType);
}
else
{
// the source is a regular object
result = ScanObject(innerSource.GetType());
}
}
return result;
}
As in GetIList(), if the source object implements IListSource, then its GetList() method is
called to retrieve the underlying
IList object.
ScanDataView Method
Next, that object is checked to see if it is a DataView. If so, a ScanDataView() method is called to pull
the column names off the
DataView object:
private List<string> ScanDataView(DataView ds)
{
List<string> result = new List<string>();
for (int field = 0; field < ds.Table.Columns.Count; field++)
result.Add(ds.Table.Columns[field].ColumnName);
return result;
}
CHAPTER 5 ■ COMPLETING THE FRAMEWORK 289
6323_c05_final.qxd 2/27/06 1:28 PM Page 289
This is the simplest scenario, since the DataView object provides an easy interface to retrieve
the list of columns.

GetChildItemType Method
If the source object isn’t a DataView, but does directly implement the IEnumerable interface, then the
type of the child object is retrieved using a helper method from the
Utilities class named
GetChildItemType():
IEnumerable iEnumerable = innerSource as IEnumerable;
if (iEnumerable != null)
{
Type childType = Utilities.GetChildItemType(
innerSource.GetType());
result = ScanObject(childType);
}
The Utilities.GetChildItemType() helper method checks to see if the type is an array. If so, it
returns the array’s element type—otherwise, it scans the properties of
listType to find the indexer:
public static Type GetChildItemType(Type listType)
{
Type result = null;
if (listType.IsArray)
result = listType.GetElementType();
else
{
DefaultMemberAttribute indexer =
(DefaultMemberAttribute)Attribute.GetCustomAttribute(
listType, typeof(DefaultMemberAttribute));
if (indexer != null)
foreach (PropertyInfo prop in listType.GetProperties(
BindingFlags.Public |
BindingFlags.Instance |
BindingFlags.FlattenHierarchy))

{
if (prop.Name == indexer.MemberName)
result = Utilities.GetPropertyType(prop.PropertyType);
}
}
return result;
}
The indexer property can be identified because it will have the [DefaultMember()] attribute
added by the compiler. If an indexer is found, the type returned by that indexer property is returned
as a result. If neither the array nor indexer approaches work, then it isn’t possible to determine the
type of the child object, so
null is returned.
ScanObject Method
Back in the GetColumns() method, a ScanObject() method is called, passing the type of the child object
as a parameter. The
ScanObject() uses reflection against that type. If you recall, the GetColumns()
method itself might also call ScanObject() if it detects that the source object wasn’t a collection but
was a single, complex
struct or object:
// the source is a regular object
return ScanObject(innerSource.GetType());
CHAPTER 5 ■ COMPLETING THE FRAMEWORK290
6323_c05_final.qxd 2/27/06 1:28 PM Page 290
The ScanObject() method uses reflection much like you’ve seen in other methods within the
framework. But in this case, it not only assembles a list of public properties, but also of public fields:
private List<string> ScanObject(Type sourceType)
{
List<string> result = new List<string>();
if (sourceType != null)
{

// retrieve a list of all public properties
PropertyInfo[] props = sourceType.GetProperties();
if (props.Length >= 0)
for (int column = 0; column < props.Length; column++)
if (props[column].CanRead)
result.Add(props[column].Name);
// retrieve a list of all public fields
FieldInfo[] fields = sourceType.GetFields();
if (fields.Length >= 0)
for (int column = 0; column < fields.Length; column++)
result.Add(fields[column].Name);
}
return result;
}
Given that this code is similar to other code you’ve seen earlier in the book, I won’t go through
it in detail. In the end, it returns a list of column names by finding the names of all public properties
and fields.
Copying the Data
The last step in the Fill() method is to call a CopyData() method to copy the data from the source
list to the
DataTable. The list of column names from GetColumns() is also passed as a parameter, and
that list is used to retrieve the data from each item in the source list.
private void CopyData(
DataTable dt, IList ds, List<string> columns)
{
// load the data into the DataTable
dt.BeginLoadData();
for (int index = 0; index < ds.Count; index++)
{
DataRow dr = dt.NewRow();

foreach (string column in columns)
{
try
{
dr[column] = GetField(ds[index], column);
}
catch (Exception ex)
{
dr[column] = ex.Message;
}
}
dt.Rows.Add(dr);
}
dt.EndLoadData();
}
CHAPTER 5 ■ COMPLETING THE FRAMEWORK 291
6323_c05_final.qxd 2/27/06 1:28 PM Page 291
Before doing any changes to the DataTable object, its BeginLoadData() method is called. This tells
the
DataTable that a batch of changes are about to happen, so it suppresses its normal event-handling
process. This not only makes the changes more efficient to process, but avoids the possibility of the UI
doing a refresh for every little change to the
DataTable.
Then the method loops through all the items in the source list. For each item, a new
DataRow object
is created, the values are copied from the source object, and the
DataRow is added to the DataTable. The
GetField() method, which is key to this process, is discussed in the following section.
When all the data has been copied into the
DataTable, its EndLoadData() method is called. This

tells the object that the batch of changes is complete so it can resume its normal event, index, and
constraint processing.
GetField Method
The workhorse of CopyData() is the GetField() method. This method retrieves the specified column
property or field value from the source object. Given that the source object could be a simple or
complex type,
GetField() is relatively long:
private static string GetField(object obj, string fieldName)
{
string result;
DataRowView dataRowView = obj as DataRowView;
if (dataRowView != null)
{
// this is a DataRowView from a DataView
result = dataRowView[fieldName].ToString();
}
else if (obj is ValueType && obj.GetType().IsPrimitive)
{
// this is a primitive value type
result = obj.ToString();
}
else
{
string tmp = obj as string;
if (tmp != null)
{
// this is a simple string
result = (string)obj;
}
else

{
// this is an object or Structure
try
{
Type sourceType = obj.GetType();
// see if the field is a property
PropertyInfo prop = sourceType.GetProperty(fieldName);
if ((prop == null) || (!prop.CanRead))
{
// no readable property of that name exists -
// check for a field
FieldInfo field = sourceType.GetField(fieldName);
if (field == null)
CHAPTER 5 ■ COMPLETING THE FRAMEWORK292
6323_c05_final.qxd 2/27/06 1:28 PM Page 292
{
// no field exists either, throw an exception
throw new DataException(
Resources.NoSuchValueExistsException +
" " + fieldName);
}
else
{
// got a field, return its value
result = field.GetValue(obj).ToString();
}
}
else
{
// found a property, return its value

result = prop.GetValue(obj, null).ToString();
}
}
catch (Exception ex)
{
throw new DataException(
Resources.ErrorReadingValueException +
" " + fieldName, ex);
}
}
}
return result;
}
One of the supported data sources is an ADO.NET DataView. A DataView contains a list of
DataRowView objects. Because of this, GetField() handles DataRowView objects as follows:
DataRowView dataRowView = obj as DataRowView;
if (dataRowView != null)
{
// this is a DataRowView from a DataView
result = dataRowView[fieldName].ToString();
}
The source list might also be an array of simple values such as int. In that case, a simple
value is returned:
else if (obj is ValueType && obj.GetType().IsPrimitive)
{
// this is a primitive value type
result = obj.ToString();
}
Similarly, the data source might be an array of string data, as shown here:
string tmp = obj as string;

if (tmp != null)
{
// this is a simple string
result = (string)obj;
}
If the data source was none of these, then it’s a more complex type—a struct or an object.
In this case, there’s more work to do, since reflection must be used to find the property or field
CHAPTER 5 ■ COMPLETING THE FRAMEWORK 293
6323_c05_final.qxd 2/27/06 1:28 PM Page 293
and retrieve its value. The first thing to do is get a Type object in order to provide access to type
information about the source object, as follows:
// this is an object or Structure
try
{
Type sourcetype = obj.GetType();
The code then checks to see if there’s a property with the name of the specified column,
as shown here:
// see if the field is a property
PropertyInfo prop =
sourcetype.GetProperty(fieldName);
if(prop == null || !prop.CanRead)
If there’s no such property (or if the property isn’t readable), then the assumption is that there’s
a matching field instead. However, if there
is a readable property, its value is returned:
else
{
// found a property, return its value
return prop.GetValue(obj, null).ToString();
}
On the other hand, if no readable property is found, then a similar process is used to look for

a field:
// no readable property of that name exists -
// check for a field
FieldInfo field = sourceType.GetField(fieldName);
if (field == null)
If there’s no field by this name, then an exception is thrown to indicate that the GetField()
method was unsuccessful:
throw new DataException(
Resources.NoSuchValueExistsException +
" " + fieldName);
However, if there is a matching field, then its value is returned, as follows:
// got a field, return its value
result = field.GetValue(obj).ToString();
If any other exception occurs during the process, it is caught and included as an inner excep-
tion. The reason for doing this is so the exception message can include the field name that failed
to make debugging easier:
catch(Exception ex)
{
throw new DataException(
Resources.ErrorReadingValueException +
" " + fieldName, ex);
}
The end result is that the GetField() method will return a property or field value from a row
in a
DataView, from an array of simple values, or from a struct or object.
At this point, the
ObjectAdapter is complete. Client code can use the Fill() methods to
copy data from virtually any object or collection of objects into a
DataTable. Once the data is in
a

DataTable, commercial reporting engines such as Crystal Reports or Active Reports can be
used to generate reports against the data.
CHAPTER 5 ■ COMPLETING THE FRAMEWORK294
6323_c05_final.qxd 2/27/06 1:28 PM Page 294
Windows Data Binding
Much of the focus in Chapter 3 was on ensuring that business objects support Windows Forms data
binding. That support from the objects is useful, but can be made even more useful by adding some
functionality to each form. This can be done using a type of Windows Forms control called an
extender
control
.
Extender controls are added to a form, and they in turn add properties and behaviors to other
controls on the form, thus extending those other controls. A good example of this is the
ErrorProvider
control, which extends other controls by adding the ability to display an error icon with a tooltip
describing the error.
ReadWriteAuthorization
Chapter 3 added authorization code to business objects, making them aware of whether each prop-
erty can be read or changed. The
CanReadProperty() and CanWriteProperty() methods were made
public so that code outside the object could easily determine whether the current user is allowed
to get or set each property on the object. One primary user of this functionality is the UI, which can
decide to alter its appearance to give users clues as to whether they are able to view or alter each
piece of data.
While this could be done by hand for each control on every form, the
ReadWriteAuthorization
control helps automate the process of building a UI that enables or disables controls based on whether
properties can be read or changed.
If a control is bound to a property, and the user does not have read access to that property due
to authorization rules, the

ReadWriteAuthorization control will disable that control. It also adds a
handler for the control’s
Format event to intercept the value coming from the data source, substitut-
ing an empty value instead. The result is that data binding is prevented from displaying the data to
the user.
Similarly, if the user doesn’t have write access to a property,
ReadWriteAuthorization will attempt
to mark any controls bound to that property as being read-only (or failing that, disabled); ensuring
that the user can’t attempt to alter the property value.
Like all Windows Forms components, extender controls inherit from
System.ComponentModel.
Component
. Additionally, to act as an extender control, the ReadWriteAuthorization control must
implement the
IExtenderProvider interface:
[DesignerCategory("")]
[ProvideProperty("ApplyAuthorization", typeof(Control))]
public class ReadWriteAuthorization : Component, IExtenderProvider
{
public ReadWriteAuthorization(IContainer container)
{ container.Add(this); }
}
The [ProvideProperty()] attribute is quite important. It specifies that ReadWriteAuthorization
extends components of type Control by adding an ApplyAuthorization property to them. In other
words, when a
ReadWriteAuthorization control is on a form, all other controls on the form get a
dynamically added
ApplyAuthorization property. Figure 5-6 shows a text box control’s Properties
window with the dynamically added
ApplyAuthorization property.

The UI developer can set this property to
true or false to indicate whether the
ReadWriteAuthorization control should apply authorization rules to that particular control.
You’ll see how this works as the control is implemented.
The
[DesignerCategory()] attribute is just used to help Visual Studio decide what kind of visual
designer to use when editing the control. The value used here specifies that the default designer should
be used.
CHAPTER 5 ■ COMPLETING THE FRAMEWORK 295
6323_c05_final.qxd 2/27/06 1:28 PM Page 295
The class also implements a constructor that accepts an IContainer parameter. This constructor
is required for extender controls, and is called by Windows Forms when the control is instantiated.
Notice that the control adds itself to the container as required by the Windows Forms infrastructure.
IExtenderProvider
The IExtenderProvider interface defines just one method: CanExtend(). This method is called by
Windows Forms to ask the extender control whether it wishes to extend any given control. Windows
Forms automatically calls
CanExtend() for every control on the form:
public bool CanExtend(object extendee)
{
if (IsPropertyImplemented(extendee, "ReadOnly")
|| IsPropertyImplemented(extendee, "Enabled"))
return true;
else
return false;
}
The ReadWriteAuthorization control can extend any control that implements either a ReadOnly
or Enabled property. This covers most controls, making ReadWriteAuthorization broadly useful. If
the potential target control implements either of these properties, a
true result is returned to indi-

cate that the control will be extended.
The
IsPropertyImplemented() method is a helper that uses reflection to check for the existence
of the specified properties on the target control:
private static bool IsPropertyImplemented(
object obj, string propertyName)
{
if (obj.GetType().GetProperty(propertyName,
BindingFlags.FlattenHierarchy |
BindingFlags.Instance |
BindingFlags.Public) != null)
return true;
else
return false;
}
CHAPTER 5 ■ COMPLETING THE FRAMEWORK296
Figure 5-6. ApplyAuthorization property added to textBox1
6323_c05_final.qxd 2/27/06 1:28 PM Page 296
ApplyAuthorization Property
The [ProvideProperty()] attribute on ReadWriteAuthorization specified that an ApplyAuthorization
property would be dynamically added to all controls extended by ReadWriteAuthorization. Of course,
the controls being extended really have no knowledge of this new property or what to do with it. All
the behavior associated with the property is contained within the extender control itself.
The extender control manages the
ApplyAuthorization property by implementing both
GetApplyAuthorization() and SetApplyAuthorization() methods. These methods are called by Win-
dows Forms to get and set the property value for each control that has been extended. The
Get and
Set are automatically prepended by Windows Forms to call these methods.
To manage a list of the controls that have been extended, a

Dictionary object is used:
private Dictionary<Control, bool> _sources =
new Dictionary<Control, bool>();
public bool GetApplyAuthorization(Control source)
{
if (_sources.ContainsKey(source))
return _sources[source];
else
return false;
}
public void SetApplyAuthorization(Control source, bool value)
{
if (_sources.ContainsKey(source))
_sources[source] = value;
else
_sources.Add(source, value);
}
When Windows Forms indicates that the ApplyAuthorization property has been set for a particu-
lar extended control, the
SetApplyAuthorization() method is called. This method records the value of
the
ApplyAuthorization property for that particular control, using the control itself as the key value
within the
Dictionary.
Conversely, when Windows Forms needs to know the property value of
ApplyAuthorization for
a particular control, it calls
GetApplyAuthorization(). The value for that control is retrieved from the
Dictionary object and returned. If the control can’t be found in the Dictionary, then false is returned,
since that control is obviously not being extended.

The end result here is that the
ReadWriteAuthorization control maintains a list of all the con-
trols it extends, along with their
ApplyAuthorization property values. In short, it knows about all the
controls it will affect, and whether it should be affecting them or not.
Applying Authorization Rules
At this point, the extender control’s basic plumbing is complete. It gets to choose which controls
to extend, and maintains a list of all the controls it does extend, along with the
ApplyAuthorization
property value for each of those controls.
When the UI developer wants to enforce authorization rules for the whole form, she can do
so by triggering the
ReadWriteAuthorization control. To allow this, the control implements a
ResetControlAuthorization() method. This method is public, so it can be called by code in the form
itself. Typically, this method will be called immediately after a business object has been loaded and
bound to the form, or immediately after the user has logged into or out of the application. It is also
a good idea to call it after adding a new business object to the database, since some objects will
CHAPTER 5 ■ COMPLETING THE FRAMEWORK 297
6323_c05_final.qxd 2/27/06 1:28 PM Page 297
change their authorization rules to be different for an old object than for a new object. You’ll see
how this works in Chapter 9 in the Windows Forms UI for the sample application.
The
ResetControlAuthorization() method loops through all the items in the list of extended
controls. This is the
Dictionary object maintained by Get/SetApplyAuthorization as discussed ear-
lier. The
ApplyAuthorization value for each control is checked, and if it is true, then authorization
rules are applied to that control:
public void ResetControlAuthorization()
{

foreach (KeyValuePair<Control, bool> item in _sources)
{
if (item.Value)
{
// apply authorization rules
ApplyAuthorizationRules(item.Key);
}
}
}
To apply the authorization rules, the code loops through the target control’s list of data bind-
ings. Each
Binding object represents a connection between a property on the control and a data
source, so it is possible to get a reference to the data source through the
DataSource property:
private void ApplyAuthorizationRules(Control control)
{
foreach (Binding binding in control.DataBindings)
{
// get the BindingSource if appropriate
if (binding.DataSource is BindingSource)
{
BindingSource bs =
(BindingSource)binding.DataSource;
// get the object property name
string propertyName =
binding.BindingMemberInfo.BindingField;
// get the BusinessObject if appropriate
if (bs.DataSource is Csla.Core.BusinessBase)
{
Csla.Core.BusinessBase ds =

(Csla.Core.BusinessBase)bs.DataSource;
ApplyReadRules(
control, binding, propertyName,
ds.CanReadProperty(propertyName));
ApplyWriteRules(
control, binding, propertyName,
ds.CanWriteProperty(propertyName));
}
else if (bs.DataSource is Csla.Core.IReadOnlyObject)
{
Csla.Core.IReadOnlyObject ds =
(Csla.Core.IReadOnlyObject)bs.DataSource;
CHAPTER 5 ■ COMPLETING THE FRAMEWORK298
6323_c05_final.qxd 2/27/06 1:28 PM Page 298

×