1. Trang chủ
  2. » Công Nghệ Thông Tin

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

50 470 0
Tài liệu đã được kiểm tra trùng lặp

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 50
Dung lượng 491,93 KB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

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

Trang 1

A CallByName() helper method is used to abstract the use of reflection to retrieve the propertyvalue based on the property name It simply uses reflection to get a PropertyInfo object for thespecified 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 Descriptionproperty of the RuleArgs object is set to describe the nature of the rule Then false is returned fromthe 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 objectdoes this by overriding the AddBusinessRules() method defined by BusinessBase Such code wouldlook like this (assuming a using statement for Csla.Validation):

StringMaxLength

A slightly more complex variation is where the rule method needs extra information beyond thatprovided by the basic RuleArgs parameter In these cases, the RuleArgs class must be subclassed tocreate 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;

}

Trang 2

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 tor must accept the name of the property to be validated, and of course, the extra data The property

construc-name is provided to the RuleArgs base class, and the extra data is stored in the field declared in the

}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 rulethat 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 TheStringMaxLength() 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))

Trang 3

{ 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 pared to the length of the specified property from the target object to see if the rule is broken or not

com-■ Note It might seem like the RuleArgsparameter should just be of type MaxLengthRuleArgs But it isimportant to remember that this method must conform to the RuleHandlerdelegate 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 thataccepted a rule method delegate along with a RuleArgs object In this case, the RuleArgs object is aninstance of MaxLengthRuleArgs, initialized with the property name and the maximum length allowedfor 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 heavyemphasis on enabling data access through the data portal, as described in Chapter 4 Beyond thebasic 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 selves writing repetitive code to eliminate null database values SafeDataReader is a wrapperaround any ADO.NET data reader object that automatically eliminates any null values that mightcome from the database

them-When creating many web applications using either Web Forms or Web Services, data must becopied into and out of business objects In the case of Web Forms data binding, data comes from thepage 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 datatransfer objects (DTOs) The properties of those DTOs must be copied into or out of a business objectwithin the web service The DataMapper class contains methods to simplify these tasks

Trang 4

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 theconcept 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 differentiatebetween 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 allowedcan 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 existingdata 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;

} }

Trang 5

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 neverhas 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

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 thevalue from the underlying data reader object

For string values, the empty value is string.Empty; for numeric types, it is 0; and for Booleantypes, 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 asvirtual This allows you to override the behavior of any of these methods by creating a subclass ofSafeDataReader

The GetOrdinal() method translates the column name into an ordinal (numeric) value, whichcan 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 methodsthat reads the data from the underling IDataReader object, replacing null values with empty defaultvalues as appropriate

Trang 6

GetDateTime and GetSmartDate

Most types have “empty” values that are obvious, but DateTime is problematic as it has no “empty”

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 doesavoid 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(

Trang 7

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 isinvolved is entirely transparent

The SafeDataReader class can be used to simplify data access code dramatically, any time anobject 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 businessdevelopers to use Web Forms data binding with CSLA NET–style business objects When Web Formsdata binding needs to insert or update data, it provides the data elements in the form of a dictionaryobject of name/value pairs The name is the name of the property to be updated, and the value is thevalue to be placed into the property of the business object

Copying the values isn’t hard—the code looks something like this:

Trang 8

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 returneddirectly 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 tothe 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, fromeither 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 objectsoften 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 matchingthose 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()))

Trang 9

{ // 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 onthe 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 theproperty 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 thevalue 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 theproperty 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 valuepropertyInfo.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 anattempt to coerce the new value to the same type as the property:

Trang 10

// types don't match, try to coerce

if (pType.Equals(typeof(Guid)))propertyInfo.SetValue(

target, new Guid(value.ToString()), null);

elsepropertyInfo.SetValue(

target, Convert.ChangeType(value, pType), null);

For most common data types, the Convert.ChangeType() method will work fine It handlesstring, 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 businessdeveloper will have to manually set that particular property; adding that property name to the list ofproperties 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 eachentry 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

Trang 11

wrapping it in a new exception object is so the property name can be included in the messagereturned 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 thelist of source property names doesn’t come from the keys in a dictionary, but rather must be retrievedfrom 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 whenthe source object is a CSLA NET–style business object, as the IsDirty, IsNew, and similar propertiesfrom 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 ofPropertyDescriptor 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 thatresult 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 anIDictionary 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

Trang 12

{ 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, smallreports 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 fromthe 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

Trang 13

generated with popular tools such as Crystal Reports or Active Reports can only be generatedagainst 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 tosupply the data to populate the report manually This technique can certainly be used with business objects Youcan write code to pull the data out of your objects and provide that data to the report engine as it generates thereport 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 supportdata binding against objects in a manner similar to Windows Forms Ideally, in the future, more of themajor report engine vendors will support data binding against objects just like Windows Forms andWeb 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 verter to load a DataSet with data from objects It allows you to convert an object into a DataSet Youcan then generate reports in standard report engines such as Crystal Reports or Active Reports byusing that DataSet

con-This approach is useful for lists, but not reports By my definition, lists require relatively smallamounts 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-basedreporting 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, aDataTable 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 loopthrough 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 objectdata will be copied into a DataTable object

To do this, ObjectAdapter needs to get a list of the properties exposed by the source object Thatlist 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 alreadyexist in the target DataTable object

Also, rather obviously, the data values from the original data source must be retrieved Reflectionwill 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

Trang 14

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 work However, sometimes some digging is required to find that interface; or it must be added by

Frame-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);

Figure 5-5.Data sources supported by ObjectAdapter

Trang 15

// 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 propertiesand 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 umn name retrieved from GetColumns() If any columns are missing, they are added to the DataTable:foreach (string column in columns)

col-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 sourceobject to the DataTable:

CopyData(dt, GetIList(source), columns);

Unfortunately, this is complicated slightly by the fact that the source object could be one ofseveral object types The GetIList() method sorts that out and ensures that it is an IList that ispassed 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 anobject 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 BindingSourceobject 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 thesource object If the source object is an ADO.NET DataView, it really will return a list of column

Trang 16

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) {

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 iscalled 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;

}

Trang 17

This is the simplest scenario, since the DataView object provides an easy interface to retrievethe list of columns.

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, itreturns 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;

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 butwas a single, complex struct or object:

// the source is a regular objectreturn ScanObject(innerSource.GetType());

Trang 18

The ScanObject() method uses reflection much like you’ve seen in other methods within theframework 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();

}

Trang 19

Before doing any changes to the DataTable object, its BeginLoadData() method is called This tellsthe DataTable that a batch of changes are about to happen, so it suppresses its normal event-handlingprocess This not only makes the changes more efficient to process, but avoids the possibility of the UIdoing 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 TheGetField() 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 Thistells the object that the batch of changes is complete so it can resume its normal event, index, andconstraint processing

GetField Method

The workhorse of CopyData() is the GetField() method This method retrieves the specified columnproperty or field value from the source object Given that the source object could be a simple orcomplex 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)

Trang 20

{ // 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;

// this is a DataRowView from a DataViewresult = 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 typeresult = 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 stringresult = (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

Trang 21

and retrieve its value The first thing to do is get a Type object in order to provide access to typeinformation about the source object, as follows:

// this is an object or Structuretry

{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 propertyPropertyInfo 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 valuereturn prop.GetValue(obj, null).ToString();

However, if there is a matching field, then its value is returned, as follows:

// got a field, return its valueresult = field.GetValue(obj).ToString();

If any other exception occurs during the process, it is caught and included as an inner tion The reason for doing this is so the exception message can include the field name that failed

excep-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 tocopy 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 beused to generate reports against the data

Trang 22

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 othercontrols 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 CanReadPropprop-erty() and CanWritePropprop-erty() 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 ReadWriteAuthorizationcontrol 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 bindsubstitut-ing is prevented from displaysubstitut-ing 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:

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 theReadWriteAuthorization 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 visualdesigner to use when editing the control The value used here specifies that the default designer should

be used

Trang 23

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 byWindows Forms to ask the extender control whether it wishes to extend any given control WindowsForms 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 Ifthe 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;

} Figure 5-6.ApplyAuthorization property added to textBox1

Trang 24

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 bothGetApplyAuthorization() 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 lar extended control, the SetApplyAuthorization() method is called This method records the value of

particu-the ApplyAuthorization property for that particular control, using particu-the control itself as particu-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 trols it extends, along with their ApplyAuthorization property values In short, it knows about all the

con-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

Trang 25

change their authorization rules to be different for an old object than for a new object You’ll seehow 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 extendedcontrols 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 authorizationrules 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 ings Each Binding object represents a connection between a property on the control and a datasource, so it is possible to get a reference to the data source through the DataSource property:

bind-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;

Csla.Core.IReadOnlyObject ds = (Csla.Core.IReadOnlyObject)bs.DataSource;

Ngày đăng: 06/07/2014, 00:20

TỪ KHÓA LIÊN QUAN