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

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

50 461 0

Đ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

Nội dung

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 [...]... 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 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); 287 6323 _c0 5_final.qxd... instance, a SelectObject event handler may look like this: protected void CustomerDataSource_SelectObject( object sender, Csla.Web.SelectObjectArgs e) { e.BusinessObject = Customer.NewCustomer(); } Notice that SelectObjectArgs defines a BusinessObject property, which must be set to the object that is to be used as a data source A typical UpdateObject event handler is a bit different: protected void CustomerDataSource_UpdateObject(... Csla.Web.CslaDataSourceView Provides the actual implementation of data binding for CslaDataSource Csla.Web.CslaDataSourceDesigner The Visual Studio designer for CslaDataSource Csla.Web.CslaDesignerDataSourceView Provides schema information and sample data for the designer Csla.Web.ObjectSchema The schema object for a business object, responsible for returning an instance of ObjectViewSchema Csla.Web.ObjectViewSchema... declaration of the class itself, in which a [Designer()] attribute is used to connect CslaDataSource to CslaDataSourceDesigner within Visual Studio: [Designer(typeof(Csla.Web.Design.CslaDataSourceDesigner))] [ToolboxData("")] public class CslaDataSource : DataSourceControl { } Within the class itself, the code is largely concerned with providing... 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()); 6323 _c0 5_final.qxd 2/27/06 1:28 PM Page 291 CHAPTER 5 s COMPLETING THE FRAMEWORK The ScanObject() method uses reflection much like you’ve seen... complexity comes because ExecuteSelect() can be called either to retrieve a data source or to retrieve the number of rows in the data source If it is asked to retrieve the row count, the method still must call OnSelectObject() on CslaDataSource so the UI event handler can return the business object: // get the object from the page SelectObjectArgs args = new SelectObjectArgs(); _owner.OnSelectObject(args);... 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 6323 _c0 5_final.qxd 2/27/06 1:28 PM Page 295 CHAPTER 5 s COMPLETING... Csla.Web.InsertObjectArgs e) { Customer obj = Customer.NewCustomer(); Csla.Data.DataMapper.Map(e.Values, obj); obj.Save(); e.RowsAffected = 1; } protected void CustomerDataSource_DeleteObject( object sender, Csla.Web.DeleteObjectArgs e) { Customer obj = Customer.DeleteCustomer(e.Keys["Id"].ToString()); e.RowsAffected = 1; } All the custom EventArgs objects except SelectObjectArgs include a RowsAffected property that the... 307 CHAPTER 5 s COMPLETING THE FRAMEWORK public event EventHandler SelectObject; internal void OnSelectObject(SelectObjectArgs e) { if (SelectObject != null) SelectObject(this, e); } EventHandler is a generic template in the NET Framework that simplifies the declaration of event handler type events Notice that the OnSelectObject() method is internal in scope It will only be called... complex Table 5-6 lists the classes required to implement the CslaDataSource control’s runtime and design time support 303 6323 _c0 5_final.qxd 304 2/27/06 1:28 PM Page 304 CHAPTER 5 s COMPLETING THE FRAMEWORK Table 5-6 Classes Required to Implement the CslaDataSource Control Class Description Csla.Web.CslaDataSource The data source control itself; this is directly used by the UI developer Csla.Web.CslaDataSourceView . 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. 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(). 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

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

TỪ KHÓA LIÊN QUAN