The video links require Microsoft Silverlight 1.0. If you do not have it, you will be prompted to install it when you click on one of the links. After the short installation is completed, close the browser window that you did the install in and re-click the video you want to watch. You can also download it here. Windows XP or Vista required.
Video demo of Declarative Programming UI Features
Table of Contents
Introduction
This is Part 3 in a series of articles on writing WPF Business Applications in VB.NET using Visual Studio 2008. This series tries to take a complex form or application and deliver articles on small pieces so that each piece can be individually understood.
If you have not yet read Parts 1 and 2, please take a few minutes and read them. This series builds on previous articles and does not repeat information.
This article will focus on "how to use" the business entity base and validation classes rather than "how these classes work". The next article will cover how the business entity base class and validation code actually works.
This article is not very sexy, but there is a lot of meat and functionality here, and is the reason I chose to write two articles instead of a single long one. Form and data object data validation is essential to any data entry or transactional processing application. The material and controls presented here will help the developer to accomplish these tasks in a very straightforward manner.
I know that this "how to" article is long. In the end, you will see that the entire article compresses down to using a few attributes on business entity objects and adding a few properties to the TextBox binding statements. It is that simple. So let's get our foundation so that we can take advantage of the features and code presented in this article.
I will be providing the full source code in C# in the future. Once the entire application has been completed (this should be before all articles are written), I'll translate and post the C# version. Some great C# developers from WPF Disciples have volunteered to help me with this important task. For now, any .NET language can use the "Core" assembly by referencing it.
Previous Articles
This Article
- Part 3 Business Object Declarative Programming: How To Implement Object Validation and Logging
Future Articles
- Part 4 Business Object Declarative Programming: How the Business Entity Base Class Works
- Part 5 Business Object Declarative Programming: Dynamic Form Control
- Part 6 Exception Handling and Logging
- Part 7 Loading Menus and ToolBar From Object Source and Application Security
- Part 8 Business Application MDI
- Part 9 Displaying SQL Server Reporting Services Reports in WPF
- Part 10 Complete Layered WPF Business Application
- Part 11 Complete Layered ASP.NET Business Application
New to WPF?
There are many fine WPF authors here on CodeProject with great insight and code. If you are new to WPF, can I suggest that you bookmark this article and take the time to read the foundational tutorials that CodeProject MVPs Josh Smith and Sacha Barber have authored? In addition to their tutorials, each have many WPF articles here on CodeProject and their blogs with sample code.
I should also mention that the SDK Team has put a lot of work into providing good, simple examples in the WPF SDK that are easy to learn from. When you install Visual Studio 2008, this is also installed. Take advantage of this free code. There is also a new Framework 3.5 SDK that was recently released. There is an issue with the install package, and when you install this new 3.5 SDK, XAML intellisense will quit working. Please read my blog post for the Registry entry fix.
If you have not read the MSDN Data Binding Overview since the .NET 3.5 framework was released, please read it here. This documentation gives the reader a comprehensive understanding of the WPF data binding pipeline and the new features in .NET 3.5. The section on data validation is outstanding. Don't miss the Debugging Mechanism section near the bottom of the article that covers the PresentationTraceSources.TraceLevel
attached property. When used, this property turns on a flow of great data binding debug information in the Visual Studio Output window. The data binding topic is essential for all WPF developers to understand.
Series Goal
The primary goal of the WPF Business Application Series is to assemble disparate pieces, tools, and technologies together, and deliver a maintainable, multi-tiered application that is documented, and most of the code can be or is actually generated.
Background
Sorry, it has taken me an extra 2-3 weeks to publish Part 3 of this series. I was at MIX08 for a week, and the background material for this article took me several weeks of research and code testing. The writing of the validation code took only two days, and was very straightforward. I got hung up in code generation, and have explained the issues at the bottom of this article.
What is object validation? When should objects be validated? Where should objects be validated? Should object validation code be in the UI? How do I perform object validation when I have several UI technologies like WPF, ASP.NET, Silverlight, and mobile devices being used to expose application data by consuming the same business objects? I will attempt to address these and other questions in this article.
The use of ASP.NET to consume these business objects will be a subject of the last article in the series, where I'll write an ASP.NET AJAX web site and use the same business objects this WPF application uses. Out of the box, ASP.NET controls do not support the IDataErrorInfo
Interface like WPF and WinForms does. I'll show you how to do this in that article. In the next week or so, I'll be publishing an article entitled, "iPhone + ASP.NET + Silverlight = Cool Application" here on CodeProject that demonstrates consuming business objects that implement the IDataErrorInfo
Interface and use this declarative business object validation model. Currently, Silverlight controls do not support the IDataErrorInfo
Interface, but I'll show you a way to do this in that article.
These next short paragraphs are written to get you thinking about object validation and application maintainability. For many of these questions, there is no perfect answer, and you will find different opinions about each of them when you research them.
Object Validation
Object validation is the enforcement of business rules. An example of a business rule could be: the Email field is stored as lower case, is required, can't be longer than 50 characters, and must be a properly formatted Internet email address. Business rules are typically established and refined during the application requirements and specification phases. I store my requirements and specifications in a database so that I can use this valuable metadata for database and Stored Procedure generation, code generation, class library help file documentation, and printed documentation.
When to Validate
The question of when an object should be validated can draw widely different answers among architects and developers, and is closely tied to another question, "should objects allow invalid property value changes" or "should an object be allowed to be in an invalid state?" For example, if the user attempts to update email with the value "first name@.com", what should the business object do? Some would throw an exception because the email text string is not a properly formatted email, some would not accept the invalid entry and would report a broken rule, others would allow the property to be set, but mark the object as invalid and report the validation rule violation.
Another question is what to do with the invalid text in the UI textbox? Should the invalid email be removed from the textbox, or should it remain as is and then visually indicate that there is a problem so that the entry can be edited and corrected?
Try this one on for size; the user has a TextBox
that is bound to an Integer
property. The user enters a letter in the TextBox
, now what should happen? Should the letter be allowed in the TextBox
in the first place, should the invalid entry be removed, or should the invalid entry remain and then display a visual indicator that a data entry error occurred? Consider if your application UI has a WPF, WinForm, ASP.NET, Silverlight, and Mobile interfaces. Do you want all these UI interfaces to work the same? Meaning, if you are going to have a numeric textbox in WPF, are you going to write similar controls for the other UIs? Will your numeric textbox handle keyboard entry, cut and paste entries, drag and drop entries, stylus entries? This is a lot to think about.
In the above string in a textbox bound to an integer property example, the WPF data binding pipeline will actually throw an exception when trying to convert a letter to an integer. The business object's property value will remain unchanged. So now, we have a form that is invalid, and potentially a business object that is invalid. So you can easily see that we must ensure that both the form controls and the bound business objects are both valid before allowing an object to be persisted. We must also provide a unified way for both the form and business object errors to be displayed to the user. I will cover solutions for all these scenarios in this article.
In my applications, I allow the user to enter anything into a textbox and then validate and format the entry and display any input errors, including exceptions thrown by the data binding pipeline. Objects are validated before persisting or transferring the object. No invalid object is ever persisted or passed to another tier.
Remember that objects are not always bound to a UI. These objects must still be validated prior to being persisted or passed to another tier. The validation system presented in this article handles these cases with no additional code on the developer's part, except for actually checking if the object is valid.
Where to Validate
The question of where objects should be validated can also draw a wide spectrum of replies. Some place the validation rules within the business object, others encapsulate the validation logic in a separate class, and a business entity is validated against that class. I know of one project that uses Workflow to validate the objects as part of their processing pipeline.
I place the validation rules inside the business object. I can query the object directly for its validation state. This makes code generation super easy, allowing me to generate the object in its entirety in one file. To see what business rules make this object valid, I only need to look in one location. Also, if that object is transferred between tiers, the validation logic goes with it.
UI Validation
Another question that needs addressing is, "should validation logic be placed in the UI?" Let's use the above email field as our example. I have two applications that will be binding to the business object that has an email property, a WPF application and an ASP.NET AJAX web site. The email has a maximum length of 50 characters. Do we limit the length of the entered text by setting the MaxLength
property on the TextBox
es? If you do, you have just placed a form of validation logic in your UI. And if we limit the length of the input, why not set up the RequiredFieldValidator
and RegularExpressionValidator
too? Relax; Karl is not a purist by any stretch of imagination, I'm just trying to get you to think.
Do I use ASP.NET validation controls? Answer; on every web page. It is not that much work to adjust a TextBox
property when an application requirement changes. Before I get flamed, consider that I designed, developed, and maintain three separate applications deployed in too many different customer sites. Each application has 50 or less pages. Very easy for one developer to maintain since they very rarely change. Now, consider you have a team of developers and some were not involved in the original design or are new .NET developers. Now you need a change management process with good application documentation and good developer supervision. So you can see how your development environment comes into play in some of your decisions.
For both WPF and ASP.NET, I also cheat. I have a UI generation program I wrote for our ASP.NET applications. For our WPF applications, we have an application that allows us to drag and drop a column from a database to a form; the correct UI control and all properties are pre-populated for us. Remember that application design metadata that I have stored in a database, well, I use it to construct the XAML for the control. A simple drag and drop to Blend or Visual Studio, and presto, a control is on the form with all the required properties set. So for me, to add some business rules like max length, RegEx validations and required field validators to the UI is a snap. Also, I don't have developers entering tooltips since these are in the metadata also.
Let's take a step back and think about our long term, "Series Goal". Our goal is to generate as much code as possible and deliver maintainable applications. Let me ask you a question. If you have a large system and a business requirement comes down that the email fields are increasing by 10 characters in length, how much work would that be for you? Can you very easily identify all places in your application that now need attention?
This is where an argument for the business rules for the email field only being in one location gets a strong vote. However, even if this was the case, there are several other locations this same change would need to be made, such as the data layer where the email database parameter is created, and in every Stored Procedure where the email is a parameter along with the database column. These locations are code generated so that no human person would have to change a line of code unless there were some non-generated Stored Procedures or non-generated classes that were written to maintain the Email
property separately from the whole business object.
So we can see that change management can drive your decision on how much validation to place in your UI. How often an application changes also comes into play. Additionally, each application has its own performance and scalability requirements. If you are designing a high volume web site, performance considerations will drive the application design. We can conclude that there is no "one size fits all" application design. Again, I'm just trying to get readers to think about application design and application maintainability.
It is easy to want to follow a purist design, but in real world applications, there are many factors that must be taken into consideration when designing maintainable applications. My suggestion; study and read system design and architecture. Know your application requirements for security, scalability, performance, durability, functionality, documentation, code generation, your staff's skill sets, maintainability, and change management requirements, and then make decisions that you, your team, and customers agree with, that will serve you the best over the long haul.
ToolBar Buttons and Data Layer
To keep this example as simple as possible so that the reader can focus on learning data validation, I have not yet implemented RoutedCommands for the ToolBar buttons and have not yet implemented a true data layer. These will both be implemented and made part of the code in a future article. For now, we are just covering one item at a time, attempting to take small steps.
BusinessEntityBase
Business object declarative programming empowers developers by adding a lot of functionality to their business entity objects with or without almost no code on their part. Classes that derive from BusinessEntityBase
inherit all of its powerful features, as you will soon see.
I must give great credit to Rockford Lhotka's CSLA and the Microsoft Enterprise Library Validation Application Block. I studied both of these frameworks, and stepped through code for hours, learning from them and increasing my knowledge of .NET programming in general. Both of these solutions provide entity validation as part of their full featured frameworks. If you are familiar with either of these two solutions, you will find very similar patterns and code here. For example, methods for shared and instance rule declaration, management, invocation, and broken rule reporting. I learned and extracted from both of these frameworks, then added additional code that I needed. The reason I went to the trouble of writing this library was that I wanted to be able to maintain and fully understand all aspects of my Core library. In doing so, I also simplified and flatted class hierarchies where possible so that I could have the simplest possible code to maintain.
BusinessEntityBase Features
The BusinessEntityBase
class provides the following features to deriving classes:
- Shared and instance rule management, invocation, and broken rule reporting. This includes multiple ruleset capabilities.
- Error reporting at the property and object level. Errors can be extracted as either a string or generic list.
- dirty tracking.
- Very simplified method for setters to call that encapsulates the validation, event raising, and property text case logic.
- Audit message creation based on decorated properties. Message can be a string or collection of property name value pairs.
- Message creation for the entire class. Message can be a string or collection of property name value pairs.
- Change notification for all entity properties.
- Changing notification for all entity properties.
- Intelligent change notification for the
IDataErrorInfo.Error
property. This permits the Error
property to be data bound and always be reported correctly in the UI.
- Rich set of overridable methods that provide the deriving classes flexibility in programming business logic if required.
BusinessEntityBase Implemented Interfaces
System.ComponentModel.INotifyPropertyChanged
- used to notify binding clients that a property value has changed. WPF and WinForms use this Interface.
System.ComponentModel.INotifyPropertyChanging
- used to notify binding clients that a property value is changing. LINQ uses this Interface for change tracking.
System.ComponentModel.IDataErrorInfo
- provides custom error information for each property and the object as a whole.
Core.BusinessEntity.IBusinessEntityAudit
- provides for simple creation of audit or class; property, property value messages.
BusinessEntityBase Events
System.ComponentModel.PropertyChanged
- raised when a property value changes.
System.ComponentModel.PropertyChanging
- raised when a property value is changing.
BusinessEntityBase Public Properties
ActiveRuleSet
- used to have a specific rule set checked in addition to all rules that are not assigned a specific rule set. For example, if you set this property to Insert
, when the rules are checked, all general rules will be checked and the Insert rules will also be checked.
IDataErrorInfo.Error
- string containing all entity validation errors, separated by a carriage return line feed. Has property change notification.
HasBeenValidated
- indicates if the entity has been validated since the last change. Checking this property before calling CheckAllRules
can save processing the rules on a valid object. Has property change notification.
IsDirty
- indicates if the object has been changed since it was created or loaded. Has property change notification.
IDataErrorInfo.Item
- string containing all property validation errors separated by a carriage return line feed.
ValidationErrors
- a dictionary object of all broken entity validation rules.
BusinessEntityBase Public Methods
AddInstanceRule
- used by sub-classes and classes that consume the sub-class business entity and need to add an instance rule to the list of rules to be enforced.
BeginLoading
- called when the business object is being loaded from a database. This saves time and processing, by passing property setter logic during loading. After the business object has been loaded, EndLoading
must be called.
CheckAllRules
- validates the entity against all shared and instance rules.
CheckRulesForProperty
- validates the property against all shared and instance rules for the property.
EndLoading
- after a business object has been loaded and the BeginLoading
method is called, developers must call this method. This method marks the entity IsDirty = False
, HasBeenValidated = False
, and raises these property changed events.
GetAllBrokenRules
- a List
of ValidationError
objects for the entity.
GetBrokenRulesForProperty
- a List
of ValidationError
objects for the property.
IsValid
- runs the CheckAllRules
method and returns a boolean indicating if the object is valid (does it pass all Shared and Instance rules).
ObjectPersisted
- this method should be called by the business layer after a valid business object has been persisted to a database, Web Service, etc. Calling this method marks this business object as not dirty.
ToAuditDictionary
- used to generate a Dictionary(Of String, String)
for each property decorated with AuditAttribute
. The Dictionary
is property name, property value. This method also has an overload that allows a target IDictionary
object to be passed in. This feature is useful when generating a name value pair dictionary to store in an Exception
object's Data
property.
ToAuditString
- returns a string of each property decorated with the AuditAttribute
. The string displays the property name, property friendly name, and property value. This function is typically used when a developer needs to make an audit log entry. It provides a very simple method to generate these messages.
ToClassIDictionary
- used to generate a Dictionary(Of String, String)
for each property in the entity. The Dictionary
is property name, property value. This method also has an overload that allows a target IDictionary
object to be passed in. This feature is useful when generating a name value pair dictionary to store in an Exception
object's Data
property.
ToClassString
- returns a string with each property and value in the class. The string displays the property name, property friendly name, and property value.
The above ToAuditDictionary
, ToAuditString
, ToClassIDictionary
, and ToClassString
methods are all sensitive to the presence of the Caption
attribute. If the Caption
attribute is applied to the property, these methods will use that value for the friendly name. If not, these methods will use the Core.StringFormatting.CamelCaseString.GetWords
method to parse the property name into a friendly name.
BusinessEntityBase Protected Methods
AddInstanceBusinessValidationRules
- override this method in your business class to be notified when you need to set up instance business rules.
AddSharedBusinessValidationRules
- override this method in your business class to be notified when you need to set up shared business rules.
AddSharedCharacterCasingFormattingRules
- override this method in your business class to be notified when you need to set up shared character casing rules.
AfterPropertyChanged
- derived classes can override this method to execute logic after the property is set.
BeforePropertyChanged
- derived classes can override this method to execute logic before the property is set.
OnPropertyChanged
- raises the PropertyChanged
event, and invokes the AfterPropertyChanged
method.
OnPropertyChanging
- raises the PropertyChanging
event, and invokes the BeforePropertyChanged
method.
SetPropertyValue(Of T)
- called by business entity sub-classes in their property setters to set the non-string value of the property and perform validation.
SetPropertyValue
- called by business entity sub-classes in their property setters to set the string value of the property, and perform validation and string case correction.
Declarative Object Validation
If you have not read the MSDN Data Binding Overview since the .NET 3.5 framework was released, please read it here. This documentation gives the reader a comprehensive understanding of the WPF data binding pipeline and the new features in .NET 3.5. The section on data validation is outstanding. Don't miss the Debugging Mechanism section near the bottom of the article that covers the PresentationTraceSources.TraceLevel
attached property. When used, this property turns on a flow of great data binding debug information in the Visual Studio Output window. The data binding topic is essential for all WPF developers to understand. (This paragraph is repeated from the introduction because it's very important.)
Public Class Customer _
Inherits BusinessEntityBase
<Audit(3)> _
<CharacterCasingFormatting(CharacterCasing.LowerCase)> _
<StringLengthValidator(1, 50)> _
<RegularExpressionValidator(RegularExpressionPatternType.Email, True)> _
Public Property Email() As String
Get
Return _strEmail
End Get
Set(ByVal Value As String)
MyBase.SetPropertyValue("Email", _strEmail, Value)
End Set
End Property
The declarative object validation system allows for rules to be applied to objects in three different ways.
The first way is to decorate the properties with Validator attributes. By decorating the property, the developer is declaring the rules, hence the term Declarative Programming that I used in the title. Some may call it Attribute Programming. A friend of mine who is a very smart programmer and author told me this is called Aspect-Oriented Programming. Regardless of the programming name, the two above Validator attributes, StringLengthValidator
and RegularExpressionValidator
, declaratively apply two shared business rules to the Email
property. Looking at the above two attributes, a developer can easily see that the email length must be between 1 and 50, and must pass a Regular Expression validation for Internet email. One of the reasons I wrote this code to use attributes was because it made reading and generating the code super simple. If a developer wants to know about the Email
property, just look at it, it's self explanatory.
The second way is to add shared validation rules in code. For those that do not want to or can't decorate their properties with Validator attributes, the BusinessEntityBase
class provides the overridable method AddSharedBusinessValidationRules
, which is called the first time the business object is instantiated. Some would ask, when would I not be able to add attributes to my properties? Good question, and one that caused me two weeks delay in this article. If you are generating your business entity objects and the tool you are using can't add these attributes as part of the generation process, you'll need to add your business validation rules in code. I have provided a discussion of code generation issues at the bottom of this article.
Protected Overrides Sub AddSharedBusinessValidationRules( _
ByVal mgrValidation As Core.Validation.ValidationRulesManager)
mgrValidation.AddRule(AddressOf ComparisionValidationRules.CompareValueRule, _
New CompareValueRuleEventArgs("YearJoined", _
ComparisionType.GreaterThanEqual, 2000, False))
End Sub
In the above AddSharedBusinessValidationRules
method, the developer would add the necessary code to add the required business rules for each property in the class. This code can also be easily generated from metadata and placed in a partial class if code generation tools don't allow the developer to modify their property declarations without the risk of losing their changes. This is the main reason I wrote my own code generator so that I could decorate my class properties the way I needed.
The third way to add a validation rule is to add one or more instance rules in code. The AddInstanceBusinessValidationRules
overridable method is called each time a business object entity is instantiated. Deriving classes can override this method and add instance validation rules. The method provides a means of adding validation rules based on a value determined at run-time rather than a static value assigned to a property in an attribute.
Protected Overrides Sub AddInstanceBusinessValidationRules()
MyBase.AddInstanceRule(AddressOf ComparisionValidationRules.CompareValueRule, _
New CompareValueRuleEventArgs("YearJoined", _
ComparisionType.LessThanEqual, Now.Year, False))
End Sub
Let's take a close look at the rule being added. The rule targets the YearJoined
property. For this example, our application specifications call for the YearJoined
property to be less than or equal to the current year. Attributes require that constant expressions be assigned to property values, so in order to get the validation rule to test against the current year, we add the validation rule in code and pass the Now.Year
value when the rule gets created. This results in a validation rule that meets the business requirements.
In the above two examples, the first parameter is the delegate for the validation rule. At your option, you can easily add more validation rules to handle your business requirements.
Additional Declarative Object Validation Examples
<CompareValueValidator(ComparisionType.GreaterThan, 0.0#, True)> _
Public Property Deposit() As Double
Get
Return _dblDeposit
End Get
Set(ByVal Value As Double)
MyBase.SetPropertyValue("Deposit", _dblDeposit, Value)
End Set
End Property
The above example shows how to add a rule that validates the value is greater than zero. Note the use of the "#" to tell the compiler that a Double
is being passed as the value to test against. The True
parameter tells the rules engine that a value must be provided.
<Audit(4)> _
<CharacterCasingFormatting(CharacterCasing.OutlookPhoneProperName)> _
<Caption("Work Phone Number")> _
<StringLengthValidator(50, PropertyFriendlyName:="Work Phone Number")> _
Public Property WorkPhone() As String
Get
Return _strWorkPhone
End Get
Set(ByVal Value As String)
MyBase.SetPropertyValue("WorkPhone", _strWorkPhone, Value)
End Set
End Property
The above example shows how to add a rule that limits the length of a String
to 50 characters but does not require an entry. Note the use of the PropertyFriendlyName
property in the StringLengthValidator
constructor. This provides a means to have friendly names for property validation error messages that are displayed to the user. Without the PropertyFriendlyName
being assigned, the validation messages would use "Work Phone" in the message. By adding the PropertyFriendlyName
value, the validation messages would now use "Work Phone Number". This is just an example of the capabilities of this validation code. In a real world application, this feature provides developers an easy way to have user friendly messages when the developer has no control over the property names or the property names are old, poorly named, or cryptic.
Validation Rules
Core.StringValidationRules.RegularExpressionRule
- uses a Regular Expression to validate a String
.
Core.StringValidationRules.StringLengthRule
- validates a String
, checking for Null
s, DBNull
s, minimum and maximum lengths.
Core.ComparisionValidationRules.ComparePropertyRule
- validates a property by comparing it against another property in the entity object.
Core.ComparisionValidationRules.CompareValueRule
- validates a property by comparing it against a value.
Core.ComparisionValidationRules.InRangeRule
- validates a property ensuring that its value is within the rule's lower and upper limits.
Core.ComparisionValidationRules.NotNullRule
- validates a property ensuring that it is not Null
or DBNull
.
Validation Review
From the above code and examples, it's very easy to see that business entity validation is very straightforward and simple to implement. By deriving from the BusinessEntityBase
class and simply decorating the entity properties, the developer can have entity validation with almost no programming effort. Even if the developer did not have the business rules stored in a database (which I strongly recommend for all projects), they could be applied with ease. Having the business entities generated really makes development painless and very accurate.
BusinessEntityBase.SetPropertyValue Method
This method does the heavy lifting for property value assignment. When I wrote this, I wanted the business entity classes to be as simple and clean as possible. One line of code meets this requirement: MyBase.SetPropertyValue("WorkPhone", _strWorkPhone, Value)
.
Public Property WorkPhone() As String
Get
Return _strWorkPhone
End Get
Set(ByVal Value As String)
MyBase.SetPropertyValue("WorkPhone", _strWorkPhone, Value)
End Set
End Property
The method encapsulates, and is responsible for:
- Intelligently setting the entity sub-class private member value. The entity private member that stores the property value is passed by reference to this method. In addition to actually setting this value, it also permits this value to be modified by this method and reflected back to the calling sub-class. This modification of the value is used for
String
properties where the CharacterCaseFormatting
attribute has been applied to the property and the formatting is applied within this method.
- Raising the required
PropertyChanging
and PropertyChanged
events.
- Validating the property value against all rules for that property.
- Modifying the entity state to mark it as dirty and that it has not yet been validated (
IsDirty = True
, HasBeenValidated = False
). Raises the required PropertyChanged
events for these properties.
- If the property is a
String
and a CharacterCaseFormatting
rule has been applied, reformat the value according to the rule.
- By-passing the above logic if the entity is being loaded.
Character Case Formatting
In the above image, the first and last names have been formatted using ProperCase, the email was formatted using LowerCase, and the work phone was formatted using OutlookPhoneProperName. Let's see how this character case formatting works.
To have a property's String
value case changed, apply the CharacterCaseFormatting
attribute to the property. Character case formatting can also be applied in the deriving sub-class in the AddSharedCharacterCasingFormattingRules
method. Applying the case formatting in code is used when the developer does not have access to the property to decorate it with attributes, or if the rules need to be dynamically applied based on business requirements.
Here is an example of the CharacterCasingFormatting
attribute applied to an entity property:
<CharacterCasingFormatting(CharacterCasing.OutlookPhoneProperName)> _
Public Property WorkPhone() As String
Get
Return _strWorkPhone
End Get
Set(ByVal Value As String)
MyBase.SetPropertyValue("WorkPhone", _strWorkPhone, Value)
End Set
End Property
The following are the options for the character case formatting of String
s:
LowerCase
- changes the String
to lower case.
None
- no changes made.
OutlookPhoneNoProperName
- formats the String
like a Microsoft Outlook phone number, but does not change the case of the text after the phone number.
OutlookPhoneProperName
- formats the String
like a Microsoft Outlook phone number and changes the case of the text after the phone number to proper case.
OutlookPhoneUpper
- formats the String
like a Microsoft Outlook phone number and changes the case of the text after the phone number to upper case.
ProperName
- changes the case of the String
to proper case.
UpperCase
- changes the case of the String
to upper case.
The proper case function is a custom function I have been using for many years in all my UIs. I demonstrated this function in the video at the top of the article. The name of the function is Core.StringFormatting.FormatText.ApplyCharacterCasing
. As part of this function's processing, if the rule calls for proper casing, it checks all proper case rules in the shared Core.StringFormatting.CharacterCasingChecks.GetChecks
method, in addition to some very general rules I have put into the function itself.
Have a look at the Core.StringFormatting.CharacterCasingChecks
class. You will see how the rules are added once, and can come from code, an external file, or database. Let's take a look at a few rules in the CharacterCasingChecks.GetChecks
method.
Public Shared Function GetChecks() As CharacterCasingChecks
If _instance Is Nothing Then
_instance = New CharacterCasingChecks
_instance.Add(New CharacterCasingCheck("Po Box", "PO Box"))
_instance.Add(New CharacterCasingCheck("Vpn ", "VPN "))
_instance.Add(New CharacterCasingCheck("Xp ", "XP "))
_instance.Add(New CharacterCasingCheck(" Or ", " or "))
_instance.Add(New CharacterCasingCheck(" Nw ", " NW "))
_instance.Add(New CharacterCasingCheck(" Ne ", " NE "))
_instance.Add(New CharacterCasingCheck(" Sw ", " SW "))
_instance.Add(New CharacterCasingCheck(" Se ", " SE "))
_instance.Add(New CharacterCasingCheck(" Llc. ", " LLC. "))
_instance.Add(New CharacterCasingCheck(" Llc ", " LLC "))
End If
Return _instance
End Function
You can easily allow your customers to have their own case correction dictionaries by loading these values from a disk file or database. Since the function only loads the collection once, there is no overhead in using this technique.
Business Form Binding to Business Entity Objects
Now that you have been introduced to entity validation, let's get into business form binding to business entity objects. When I use the term "form", I'm referring to a WPF UserControl that contains UI controls that make up the data entry or data maintenance form.
Before we can jump into writing our form, there are a number of WPF converters I need to cover first. These converters will be used by the form TextBox
es.
ForceReReadConverter
I posted the following INotifyPropertyChanged event fires, but UI not updated if business class changes property value after UI update on the MSDN WPF forum a while back. The basic problem is that if entity classes alter the value of the property that was set in the UI during a data binding operation, the altered value will not be re-read and displayed in the UI, even though the PropertyChanged
event was fired. You can see that the character case feature of the BusinessEntityBase
class would be worthless unless the UI re-reads the entity changed property value. Thankfully, the Microsoft WPF Team provided a technique for getting the data binding pipeline to re-read the property value from the entity after it is assigned during the data binding operation.
This technique involves placing a converter in the TextBox
binding statement. The presence of the converter causes the data binding pipeline to re-read the property value before calling the converter. If the business entity object has modified the property value in any way, that new value will now be reflected in the UI.
Core.WPF.ForceReReadConverter
is a dummy converter that returns the same value it was passed in. For any form TextBox
es that data bind to a property with character case rules, or any TextBox
that data binds to a property that could potentially alter the value based on a business rule implemented in the entity object, you will need to add the ForeReReadConverter
to that TextBox
's binding statement.
This is where having the ability to generate business entities as well as form control code comes in real handy as developers don't have to remember to add this converter. The UI code generator automatically adds the required code. If you forget to add the converter, it will be obvious the very first time you enter data in the TextBox
and the case does not change as expected.
Example showing how to force the FirstName
property to be re-read after the property value is set on the entity:
<TextBox Text="{Binding Path=FirstName, Mode=TwoWay,
Converter={StaticResource forceReReadConverter}}" />
FormattingConverter
Core.WPF.FormattingConverter
will format an object based on that object type's standard .NET format string or a custom format string. This is the converter that is used for formatting dates and numbers. I have an extensive blog post, formatting data binding dates and numbers, that you can read on this converter.
The FormattingConverter.ConvertBack
code has been updated in this application from Part 2. If the ConvertBack
operation fails, this function will return the value passed in, unmodified.
It is vital that your value converters not throw or be allowed to bubble exceptions because the data binding pipeline does not catch, swallow, or handle these exceptions. Instead, your users will get those messy exception messages because there is no method to catch and handle exceptions in the XAML markup. Ensure that if your converter fails that you gracefully handle this conversion or convert back operation.
Below is the new ConvertBack
code:
Public Function ConvertBack(ByVal value As Object, _
ByVal targetType As System.Type, ByVal parameter As Object, _
ByVal culture As System.Globalization.CultureInfo) _
As Object Implements System.Windows.Data.IValueConverter.ConvertBack
Dim objTypeConverter As System.ComponentModel.TypeConverter = _
System.ComponentModel.TypeDescriptor.GetConverter(targetType)
Dim objReturnValue As Object = Nothing
If objTypeConverter.CanConvertFrom(value.[GetType]()) Then
Try
objReturnValue = objTypeConverter.ConvertFrom(value)
Catch ex As Exception
objReturnValue = value
End Try
End If
Return objReturnValue
End Function
Example formatting the Deposit
property like 7,500.00:
<TextBox Text="{Binding Path=Deposit,
Converter={StaticResource formattingConverter},
ConverterParameter=\{0:N\}}" />
NullableIntegerFormatConverter
In order to data bind to an entity property that is a generic Nullable(Of Type)
, a WPF value converter is required. The Core library has four of these converters, NullableDateTimeFormatConverter
, NullableDecimalFormatConverter
, NullableDoubleFormatConverter
, and NullableIntegerFormatConverter
. If you need additional converters for other types, these are very easy to write.
These converters support both the IValueConverter.Convert
and IValueConverter.ConvertBack
functions, and correctly handle the setting of Null
values in the entity object if the TextBox.Text
property is an empty String
.
Additionally, they serve as a formatting converter for these types.
Below is an example of the Core.WPF.NullableIntegerFormatConverter
that will correctly set the entity property YearJoined
to Null
if the TextBox.Text
property is an empty String
, and also formats the number as a whole number without a thousand separator.
<TextBox Text="{Binding Path=YearJoined,
Converter={StaticResource nullableIntegerFormatConverter},
ConverterParameter=\{0:D\}}" />
Reporting of Business Entity Validation Errors and Business Form Exceptions
The above image shows the unified reporting of a broken entity validation rule and a business form exception.
We have covered a lot of ground so far, and it's almost time to reap the benefits of our labors. Business entity validation errors are reported though the IDataErrorInfo.Error
property. Remember, this String
property represents each of the currently broken validation rules in the entity as a whole.
If we have business entity validation errors, then what are business form exceptions? I came up with this term that describes exceptions that are thrown either by validation rule checking code as a result of a broken rule or by validation code that had an unexpected exception thrown. The validation system presented here does not throw exceptions when property validation rules are violated; rather, property broken rules are reported through the IDataErrorInfo.Item
property. However, if the validation code or property setter code throws an unexpected exception, it will be caught by the data binding pipeline and reported as a business form exception.
Another type of business form exception is an exception that is thrown by the WPF data binding pipeline when attempting to set a property value but the value is not of the correct type. Take the YearJoined
property for example. This property is of type Integer
. If the user enters "abc" in the TextBox
and presses Tab, the WPF data binding pipeline will attempt to set the YearJoined
property to "abc". Since this String
can't be converted into an Integer
, an exception will be thrown, caught by the data binding system and reported as a business form exception.
From the above paragraph, my theology on data entry forms comes to light. I prefer to allow users to enter data in TextBox
es, validate and report entry errors, and leave any invalid entry in the TextBox
for the user to see and correct. Don't forget that TextBox
es in WPF can get input from the keyboard, copy and paste, drag and drop, or the stylus. I have seen code examples on the Internet for disabling paste operations in WPF TextBox
es, but don't feel that you should limit your users' ability to get data into their form TextBox
es.
I know the concept of entity validation errors and business form exceptions is a little difficult to understand. However, it is essential for proper and complete WPF data binding error reporting, so I'll cover it again from a different angle.
It is possible for the bound business entity object to be valid and the business form have an invalid entry in a TextBox
. Let me provide an example. Let's say that the user entered all the form fields correctly. There are no validation errors on any of the form controls and the FormNotification
control does not report any errors. Now the user goes to the YearJoined
textbox and enters "abc" and presses Tab. The TextBox
will now have a validation exception in its Validation.Errors
collection. This makes the business form invalid. However, the bound entity object is still valid since the WPF data binding pipeline aborted the attempt to set the property and threw an exception. I have demonstrated this condition in the video at the top of this article.
I wanted to come up with a solution that required no additional coding on the developer's part to recognize and report this condition to the user. Additionally, as a developer, you do not want to allow Save, Update, or Insert commands to be executed if the form is invalid even though the business object is valid.
Let me give you the high points of the solution and the Core.WPF.UserControlBase
class. The UserControlBase.ValidationExecptionErrors
property, as previously stated, is a String
that represents all Validation.Errrors
exception messages for all the controls on the form. The UserControlBase
class implements the System.ComponentModel.INotifyPropertyChanged
interface and raises the PropertyChanged
event for this property when an exception is thrown or validation exception errors are cleared from the internal collection. The UserControlBase.ClearValidationExceptionErrors
method should be called by the entity sub-classes when a new record is loaded to create a new internal Validation.Errors
cache in the UserControlBase
class.
The UserControlBase
class handles the System.Windows.Controls.Validation.ErrorEvent
for all controls in the form. The RoutedEvent handler is established in the constructor of the UserControlBase
class, as follows:
Public Sub New()
Me.AddHandler(System.Windows.Controls.Validation.ErrorEvent, _
New RoutedEventHandler(AddressOf ExceptionValidationErrorHandler), True)
End Sub
The UserControlBase.ExceptionValidationErrorHandler
method records the entity class name, property name, and a reference to the control that has the exception. All validation exception errors, including broken rules reported by the IDataErrorInfo.Item
property, will raise this ErrorEvent
. However, the ExceptionValidationErrorHandler
method tests for ExceptionValidationRule
, and only records exceptions and not the broken rules. Notice the above code comment requirement for the TextBox
binding statement to have the property NotifyOnValidationError = True
. This property instructs the data binding pipeline to raise the ErrorEvent
when an exception is thrown during a data binding operation. I will cover all the required binding statement properties below.
You can see that the reporting and recording of data binding business form exceptions is very straightforward. The ExceptionValidationErrorHandler
method handles both the adding and removing of exceptions from the internal collection.
Notice that I only record the entity class name, property name, and a reference to the control, and not the actual exception. When UserControlBase.ValidationExceptionErrors
is checked, each form control that currently has an exception associated with it is checked and all exceptions are listed individually.
The UserControlBase.ValidationExceptionErrors
property is checked quite often during form processing since this property is data bound to the FormNotification
control and participates in the multi-value data binding along with the entity's IDataErrorInfo.Error
property. Consequentially, this function must run super fast so that it will not slow down the tabbing between UI controls. I have seen developer solutions for reporting the current form exceptions by walking the logical tree and checking each TextBox
for the presence of Validation.Errrors
. The FormNotification
custom control provides the developer the means to display, in real-time, all form exceptions and entity broken rules. By tracking the form exceptions and maintaining a collection of exceptions, the reporting of form exceptions is super fast and efficient and does not require walking the logical tree to discover exceptions associated with form controls.
FormNotification Control
The FormNotification
control was the subject of Part 2 of this series. I have added FormNotificationErrorMessageConverter
to the Core library to enable unified reporting of entity validation errors and business form exceptions. Since Part 2, I have modified the FormNotification
control to sort the error messages it displays. This was required since we are now binding to two or more sources for the error information.
FormNotificationErrorMessageConverter
Core.WPF.FormNotificationErrorMessageConverter
is a multi-value converter that is used to concatenate the validation error messages from the business entity and the business form exceptions into a single string. In reality, this converter actually concatenates two or more strings in a binding statement.
Below is an example of the Core.WPF.FormNotification.ErrorMessage
property being data bound to two different objects that both report errors. This provides codeless unified error reporting from the business entity and the form. The error path is from the IDataErrorInfo.Error
property of the form DataContext
. The UserControlBase.ValidationExceptionErrors
property is on the base class that all business forms derive from. This property is a String
that represents all the exceptions in each of the form control's Validation.Errrors
collection.
<Core_WPF:FormNotification WatermarkMessage="New Record"
Height="28" Panel.ZIndex="99" AutoCollapseTimeout="2">
<Core_WPF:FormNotification.ErrorMessage>
<MultiBinding Converter="{StaticResource formNotificationErrorMessageConverter}">
<Binding Path="Error" />
<Binding Path="ValidationExceptionErrors" ElementName="ucPartThree" />
</MultiBinding>
</Core_WPF:FormNotification.ErrorMessage>
</Core_WPF:FormNotification>
Form TextBox Binding Statement
In order to activate the behavior and functionality discussed in this article, a number of data binding properties must be set in each of the form TextBox
es. Let's have a look at the XAML for a form TextBox
and get an understanding of the required properties.
<TextBox Text="{Binding Path=FirstName,
Mode=TwoWay,
UpdateSourceTrigger=LostFocus,
ValidatesOnDataErrors=True,
ValidatesOnExceptions=True,
NotifyOnValidationError=True,
Converter={StaticResource forceReReadConverter}}" />
The above Mode
and UpdateSourceTrigger
properties are both set to the default values for a TextBox
, and are really not required to be set in the binding XAML. I have done this for the purpose of clarity since other WPF controls do not share these same default values. It is up to each software shop supervisor to establish coding standards and enforce them. Being consistent throughout all your code is very important from a maintenance standpoint.
ValidatesOnDataErrors
This property is new to .NET 3.5. From the MSDN documentation: Setting this property provides an alternative to using the DataErrorValidationRule
element explicitly. DataErrorValidationRule
is a built-in validation rule that checks for errors that are raised by the IDataErrorInfo
implementation of the source object. If an error is raised, the binding engine creates a ValidationError
with the error and adds it to the Validation.Errors
collection of the bound element. The lack of an error clears this validation feedback, unless another rule raises a validation issue.
When a validation rule is violated and this property is set to True
, the validation rule violation will be available for reporting to the user with the TextBox
Validation.ErrrorTemplate
. This is covered below. Note, the entity IDataErrorInfo.Error
property will report any validation error regardless of this property's value since entity broken rule errors are discovered when the property setter is called which has nothing to do with this property. This property is only used to place entity broken rules in the TextBox
's Validation.Errors
collection.
ValidatesOnExceptions
This property is new to .NET 3.5. From the MSDN documentation: Setting this property provides an alternative to using the ExceptionValidationRule
element explicitly. ExceptionValidationRule
is a built-in validation rule that checks for exceptions that are thrown during the update of the source property. If an exception is thrown, the binding engine creates a ValidationError
with the exception and adds it to the Validation.Errors
collection of the bound element. The lack of an error clears this validation feedback, unless another rule raises a validation issue.
When an exception is thrown during the update of the source and this property is set to True
, the validation rule violation will be available for reporting to the user with the TextBox
Validation.ErrrorTemplate
. This is covered below.
NotifyOnValidationError
From the MSDN documentation: If the binding has ValidationRule
s associated with it, the binding engine checks each rule each time it transfers the target property value to the source property. If a rule invalidates a value, the binding engine creates a ValidationError
object and adds it to the Validation.Errors
collection of the bound object. When the Validation.Errors
property is not empty, the Validation.HasError
attached property of the object is set to True
. If the NotifyOnValidationError
property of the Binding
is set to True
, then the binding engine raises the Validation.Error
attached event on the object.
This is the property that allows the UserControlBase
class to monitor binding exceptions by handling the Validation.Errror
attached event. This was covered above.
When set to True
, this property instructs the WPF data binding pipeline to raise the Validation.Error
event for all data binding rule violations and exceptions. This is why the UserControlBase.ExceptionValidationErrorHandler
tests for the type of validation exception, and only adds or removes binding exceptions and not binding validation rules to the internal collection of UI controls with exceptions.
Form TextBox Validation.ErrrorTemplate
The below code is from the OfficeBlueResourceDictionary.xaml file.
<ControlTemplate x:Key="validationTemplate">
<DockPanel>
<DockPanel.Resources>
<Core_WPF:ValdiationErrorGetErrorMessageConverter
x:Key="valdiationErrorGetErrorMessageConverter" />
</DockPanel.Resources>
<TextBlock Margin="5,0,5,0" Foreground="Red" FontSize="16"
VerticalAlignment="Center" Text="*"
ToolTip="{Binding ElementName=errorAdorner,
Path=AdornedElement.(Validation.Errors),
Converter={StaticResource valdiationErrorGetErrorMessageConverter}}" />
<AdornedElementPlaceholder x:Name="errorAdorner" />
</DockPanel>
</ControlTemplate>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Validation.ErrorTemplate"
Value="{DynamicResource validationTemplate}" />
</Style>
Let me cover the Core.WPF.ValdiationErrorGetErrorMessageConverter
and then walk through this template.
ValdiationErrorGetErrorMessageConverter
The Validation.Errors
attached property returns a collection of all active ValidationError
objects on the bound element, in this case the TextBox
. The ValidationError
object can be a broken entity validation rule or an exception that occurred in the WPF data binding pipeline.
In most example code I've seen, including the MSDN documentation, the ToolTip
used to report the broken rule or exception to the user uses the ErrorContent
property similar to this: Path=(Validation.Errors)[0].ErrorContent
. The problem with using this code for the ToolTip
text source is that some exceptions that are reported in the WPF data binding pipeline are wrapped in other exceptions. Displaying a TargetInvocationException
message to a user or yourself is worthless, and should not be done.
This is where the ValdiationErrorGetErrorMessageConverter
comes into play. This converter will look for an inner exception and returns that message instead of the outer exception message. If the inner exception is Nothing
, then the ErrorContent
property is returned. Additionally, this converter is passed in the entire Validation.Errors
collection, and will report all active data binding errors and not just the first one. Having more than one broken rule is not the norm, but is possible, so let's code for this condition.
In the above template, the Validation.Errors
collection is passed to this converter using this code: Path=AdornedElement.(Validation.Errors)
.
Validation.ErrorTemplate
Each developer or designer will have their own take on ErrorTemplate
s. The look and usage is normally determined during the application specification phase. For this application series, I have decided to display a red *
next to UI controls that have broken validation rules or data binding exceptions. One reason for doing this is, when a new and empty form is displayed, several controls have the red *
next to them; these are the controls that are bound to the required entry properties. Users generally associate the red *
with a required input field. By doing this, the red *
serves two purposes. One by initially reporting required entry fields to the user that have not yet been filled in, and error reporting for data entry errors and exceptions.
Another reason for displaying a red *
in the template TextBlock
is so that the error can be displayed in a ToolTip
. A lot of example code and the MSDN documentation use a trigger on the target TextBox
to set the TextBox
ToolTip
to the Validation.Errors[0].ErrorConent
property. However, if you use this technique and the TextBox
has an assigned ToolTip
, the error message will not be displayed in the TextBox ToolTip
. This may be a bug, I'm not sure. Triggers are supposed to replace values when active, but for some reason, the ToolTip
's content does not get changed if it has content, but only gets set when the ToolTip
content is empty. You can test it out and verify this behavior in your own code. I set ToolTip
s for all my form controls to assist users, so I had to place the ErrorContext
in another control's ToolTip
.
PartThree.xaml and PartThree.xaml.vb
Take a look at the PartThree.xaml file. As you can see, this is very simple and light XAML markup. I have already covered most of the markup in this file, so let's move on to the PartThree.xaml.vb code file.
Private Sub NewRecord()
Me.frmNotification.NotificationMessage = String.Empty
Me.frmNotification.ErrorHeaderText = "Required Items"
MyBase.ClearValidationExceptionErrors()
_objCustomer = New Customer()
Me.DataContext = _objCustomer
_objCustomer.CheckAllRules()
End Sub
Private Sub btnSave_Click(ByVal sender As Object, _
ByVal e As System.Windows.RoutedEventArgs) Handles btnSave.Click
UpdateFocusedField()
If Not MyBase.HasValidationExceptionErrors AndAlso _
_objCustomer.IsValid Then
Me.frmNotification.NotificationMessage = _
"Customer record saved"
Else
Me.frmNotification.ErrorHeaderText = "Save Errors"
End If
End Sub
The NewRecord
method initializes the FormNotification
control, clears out any previous ValidationExceptionErrors
in the base class, creates a new Customer
object and assigns it to the form's DataContext
, then runs the CheckAllRules
method against the new Customer
object to force all invalid properties to report their status.
Nothing in this method is new or noteworthy except for the second line, Me.frmNotification.ErrorHeaderText = "Required Items"
. Josh Smith and I were speaking on the phone about the Part 2 article, and he suggested that the initial message displayed by the FormNotification
control not say "Entry Errors", like it did in Part 2. So I have set the ErrorHeaderText
to display "Required Items" when a new record is displayed. This makes the interface cleaner, more accurate, and user friendly. Josh, thank you for the suggestion, it enhances the UI.
The btnSave_Click
event handler is very similar to the same handler from Part 2, with some minor changes. UpdateFocusedField
was covered in Part 2.
New to this handler is the testing for business form exceptions by checking the HasValidationExceptionErrors
property. If any form exceptions are present or the business entity is not valid, the save routine will not be called.
When I implement RoutedCommands in the ToolBar buttons, it will be very easy to program the CanExecute
method for the Save button. The HasValidationExceptionErrors
property can be tested, and returns an accurate result for the CanExecute
method. The difference between the ValidationExceptionErrors
property and the HasValidationExceptionErrors
property, besides that one returns a String
and the other a Boolean
, is when the ValidationExceptionErrors
is read, it actually reads through the collection and creates a new String
. The HasValidationExceptionErrors
just checks the current internal validation errors collection for the presence of items in the collection, without any processing required.
Notice what happens when the Save button is clicked and the form has exceptions or the business entity has broken rules. The FormNotification
control ErrorHeaderText
property is changed from "Required Items" to "Save Errors" with the following code: Me.frmNotification.ErrorHeaderText = "Save Errors"
. This enhances the user's experience as I have stated above.
If you sit back and think about it, this form has very little code for all the functionality it provides. All the time you have spent reading and studying this material will pay huge dividends in your application development. With the material presented here, you can quickly decorate your entity classes as required, set up your binding statements as demonstrated, and you have all this functionality with very little effort on your part.
Declarative Logging
The above image shows the result of the call to the ToAuditString
method.
I added the logging methods and attributes to this library to make it very easy to generate predefined audit messages for entities and to generate property name and property value pair collections when logging exceptions.
<Audit(1)> _
Public Property FirstName() As String
Get
Return _strFirstName
End Get
Set(ByVal Value As String)
MyBase.SetPropertyValue("FirstName", _strFirstName, Value)
End Set
End Property
The key to the audit messages is that they are predefined. A developer does not have to take time coding audit messages, and when changes to the entity are made, no code has to be touched to add or remove properties from the generated audit messages. Remember that our business entities are generated from the specification metadata. As changes are made to the application over time, the new entities are regenerated and the Audit
attributes are applied automatically.
The constructor of the Audit
attribute takes an optional Integer
parameter that specifies the sort order for the property in String
audit messages or in the property name, property value Dictionary
.
The logging generation methods are all located in the BusinessEntityBase
class, and have been previously discussed.
Private Sub btnSave_Click(ByVal sender As Object, _
ByVal e As System.Windows.RoutedEventArgs) Handles btnSave.Click
UpdateFocusedField()
If Not MyBase.HasValidationExceptionErrors AndAlso _
_objCustomer.IsValid Then
Me.frmNotification.NotificationMessage = "Customer record saved"
MessageBox.Show(_objCustomer.ToAuditString(String.Empty, vbCrLf), _
"Customer Class Audit Message", MessageBoxButton.OK, _
MessageBoxImage.Information)
MessageBox.Show(_objCustomer.ToClassString(vbCrLf, True), _
"Customer Class Message", MessageBoxButton.OK, _
MessageBoxImage.Information)
Else
Me.frmNotification.ErrorHeaderText = "Save Errors"
End If
End Sub
Run the program. Fill in the form and press the Save button. The first message box displays a ToAuditString
. Check out the string that is returned. You can easily reformat this string to meet your requirements. I use these audit strings when saving log messages in the database or event log. Since all four audit methods are members of the Core.BusinessEntity.IBusinessEntityAudit
Interface, the entity object can be passed to other library objects that consume IBusinessEntityAudit
objects, and the log processing can be placed in other classes.
The second message box displays a ToClasssString
.
I use the ToAuditIDictionary
method when logging an exception, and place the results of this method in the Exception.Data
property. Since our company has customers all over the United States, comprehensive exception reporting is vital to excellent customer support and troubleshooting, should the need arise. Getting the business object values at the time of the exception can be helpful when trying to troubleshoot remotely or troubleshoot a nightly process that threw an exception. These logging methods provide the necessary functionality for logging entity member values.
Code Generation Requirements
I stated at the beginning of the article that I was delayed in getting this article out because I was looking at code generation tools. Here is Karl's short list of requirements for a code generation tool:
- Apply metadata to entity property and classes from external metadata source in the form of
Summary
and Remarks
XML comments.
- Generate the
Parameters
, Exceptions
, and Returns
XML comment fields.
- Decorate classes, properties, and methods with required attributes for LINQ, WCF, Astoria, and user driven requirements like this validation system.
- Apply namespace to entities.
- Apply base class to entities.
- When generating
Select
and SelectAll
data access and business entities, ensure that if both return the complete entity to use the same object. Currently, SQLMetal generates different entities for each operation.
- (...)
Someone would ask, what does code generation have to do with business entity object validation? It has nothing to do with validation, but it has everything to do with delivering a stable, maintainable, and documented application to your customer. This series is about writing a small, medium, or large business application. For these types of applications, code generation is an imperative.
Starting with metadata, the generator should be able to crank out documented database objects, documented business entities, documented business layer objects for processing the business entities, and documented data layer code. We must position ourselves to generate this code. Take a small application with 75 tables, for example. Now, start counting the number of CRUD Stored Procedures, data layers classes and access methods, business entities, and textboxes in the UI, and you'll quickly agree that if we can generate most of this code, we are way ahead of the game. When I say generate the code, I'm also implying regenerating the code when the application requirements change, which they always do.
Code generation is a complete and complex area of programming in itself. It is part of the development cycle, and needs to be planned for up front. There are many great code generation tools and even a few books on the market.
I got tangled up in code generation when I started using the Visual Studio 2008 and SQLMetal ORM tools. I very quickly found out that my approach to codeless validation will not work with these tools because I'm using attributes on entity members as a way of declaring the validation rules for that property. Additionally, I have other attributes that I need on the entities. One of the reasons I added the ability to add shared rules in code was to work around the limitations of the current ORM tools.
Another problem with these tools is that I had no way to place XML comments on the entity properties or classes. XML comments on entity properties and classes is essential for Intellisense and class help files. Yes, you can generate the code with the ORM tools then add the attributes and XML comments, but they will be lost if the entity needs to be regenerated. To me, this is a show stopper. I have written this blog post, Visual Studio 2008 Designers and Code Generators You Have Overlooked (...), that covers this issue in detail.
Two weeks ago, I went to an MSDN event on the Visual Studio Team System. There I learned that Microsoft has more cool ORM tools. However, these tools have the same problems I've mentioned in the above paragraph.
I would like to suggest that a (the) senior level architect at Microsoft convene a group of industry experts on code generation, ORM, and real world business product requirements like I have listed above, and design a metadata system that the Microsoft ORM tools uniformly consume when generating code. It would also be nice to edit the template files that the ORM tools use to generate the code. I would even settle for a data structure that the Microsoft ORM tools use, and we in the development community populate that data structure from our application specification database.
Close
First, thanks for reading this article. I know that it was very long and not sexy at all. But you now have the tools to very quickly, with almost no effort, validate your business entity objects, and report in a unified manner, all form exceptions and entity broken validation rules.
This long article can be distilled down to a few attributes on entities and a few properties in data binding statements. If this statement is true for your development efforts, then I have accomplished my mission.
I hope that you got something from this article in the WPF Business Application Series, and that you have seen some of WPF's potential for writing great business applications.
In addition to this Business Application Series, I also have a WPF Sample Series on my blog. I post several WPF sample applications a month. I won't be posting every sample here on CodeProject, but you can still read them and download the code from my blog.
Have a great day!
History
- 1 April 2008: Initial release.