Imports System.Collections.Specialized ''' ''' Tracks the business rules broken within a business object. ''' _ Public Class BrokenRules #Region " Rule structure " ''' ''' Stores details about a specific broken business rule. ''' _ Public Structure Rule Private mRule As String Private mDescription As String Private mProperty As String Friend Sub New(ByVal Rule As String, ByVal Description As String) mRule = Rule mDescription = Description End Sub Friend Sub New(ByVal Rule As String, ByVal Description As String, ByVal [Property] As String) mRule = Rule mDescription = Description mProperty = [Property] End Sub ''' ''' Provides access to the name of the broken rule. ''' ''' ''' This value is actually readonly, not readwrite. Any new ''' value set into this property is ignored. The property is only ''' readwrite because that is required to support data binding ''' within Web Forms. ''' ''' The name of the rule. Public Property Rule() As String Get Return mRule End Get Set(ByVal Value As String) ' the property must be read-write for Web Forms data binding ' to work, but we really don't want to allow the value to be ' changed dynamically so we ignore any attempt to set it End Set End Property ''' ''' Provides access to the description of the broken rule. ''' ''' ''' This value is actually readonly, not readwrite. Any new ''' value set into this property is ignored. The property is only ''' readwrite because that is required to support data binding ''' within Web Forms. ''' ''' The description of the rule. Public Property Description() As String Get Return mDescription End Get Set(ByVal Value As String) ' the property must be read-write for Web Forms data binding ' to work, but we really don't want to allow the value to be ' changed dynamically so we ignore any attempt to set it End Set End Property ''' ''' Provides access to the property affected by the broken rule. ''' ''' ''' This value is actually readonly, not readwrite. Any new ''' value set into this property is ignored. The property is only ''' readwrite because that is required to support data binding ''' within Web Forms. ''' ''' The property affected by the rule. Public Property [Property]() As String Get Return mProperty End Get Set(ByVal Value As String) ' the property must be read-write for Web Forms data binding ' to work, but we really don't want to allow the value to be ' changed dynamically so we ignore any attempt to set it End Set End Property End Structure #End Region #Region " RulesCollection " ''' ''' A collection of currently broken rules. ''' ''' ''' This collection is readonly and can be safely made available ''' to code outside the business object such as the UI. This allows ''' external code, such as a UI, to display the list of broken rules ''' to the user. ''' _ Public Class RulesCollection Inherits CSLA.Core.BindableCollectionBase Private mLegal As Boolean = False ''' ''' Returns a object ''' containing details about a specific broken business rule. ''' ''' ''' Default Public ReadOnly Property Item(ByVal Index As Integer) As Rule Get Return CType(list.Item(Index), Rule) End Get End Property ''' ''' Returns the first object ''' corresponding to the specified property. ''' ''' ''' ''' When a rule is marked as broken, the business developer can provide ''' an optional Property parameter. This parameter is the name of the ''' Property on the object that is most affected by the rule. Data binding ''' may later use the IDataErrorInfo interface to query the object for ''' details about errors corresponding to specific properties, and this ''' value will be returned as a result of that query. ''' ''' Code in a business object or UI can also use this value to retrieve ''' the first broken rule in that corresponds ''' to a specfic Property on the object. ''' ''' ''' The name of the property affected by the rule. Public ReadOnly Property RuleForProperty(ByVal [Property] As String) As Rule Get Dim item As Rule For Each item In list If item.Property = [Property] Then Return item End If Next Return New Rule() End Get End Property Friend Sub New() AllowEdit = False AllowRemove = False AllowNew = False End Sub Friend Sub Add(ByVal Rule As String, ByVal Description As String) Remove(Rule) mLegal = True list.Add(New Rule(Rule, Description)) mLegal = False End Sub Friend Sub Add(ByVal Rule As String, ByVal Description As String, ByVal [Property] As String) Remove(Rule) mLegal = True list.Add(New Rule(Rule, Description, [Property])) mLegal = False End Sub Friend Sub Remove(ByVal Rule As String) Dim index As Integer ' we loop through using a numeric counter because ' the base class Remove requires a numberic index mLegal = True For index = 0 To list.Count - 1 If CType(list.Item(index), Rule).Rule = Rule Then list.Remove(list.Item(index)) Exit For End If Next mLegal = False End Sub Friend Function Contains(ByVal Rule As String) As Boolean Dim index As Integer For index = 0 To list.Count - 1 If CType(list.Item(index), Rule).Rule = Rule Then Return True End If Next Return False End Function ''' ''' Prevents clearing the collection. ''' Protected Overrides Sub OnClear() If Not mLegal Then Throw New NotSupportedException("Clear is an invalid operation") End If End Sub ''' ''' Prevents insertion of items into the collection. ''' Protected Overrides Sub OnInsert(ByVal index As Integer, ByVal value As Object) If Not mLegal Then Throw New NotSupportedException("Insert is an invalid operation") End If End Sub ''' ''' Prevents removal of items from the collection. ''' Protected Overrides Sub OnRemove(ByVal index As Integer, ByVal value As Object) If Not mLegal Then Throw New NotSupportedException("Remove is an invalid operation") End If End Sub ''' ''' Prevents changing items in the collection. ''' Protected Overrides Sub OnSet(ByVal index As Integer, _ ByVal oldValue As Object, ByVal newValue As Object) If Not mLegal Then Throw New NotSupportedException("Changing an element is an invalid operation") End If End Sub End Class #End Region Private mBrokenRules As New RulesCollection() _ Private mTarget As Object #Region " Rule Manager " ''' ''' Sets the target object so the Rules Manager functionality ''' has a reference to the object containing the data to ''' be validated. ''' ''' ''' The object here is typically your business object. In your ''' business class you'll implement a method to set up your ''' business rules. As you do so, you need to call this method ''' to give BrokenRules a reference to your business object ''' so it has access to your object's data. ''' ''' A reference to the object containing ''' the data to be validated. Public Sub SetTargetObject(ByVal target As Object) mTarget = target End Sub #Region " RuleHandler delegate " ''' ''' Delegate that defines the method signature for all rule handler methods. ''' ''' ''' ''' When implementing a rule handler, you must conform to the method signature ''' defined by this delegate. You should also apply the Description attribute ''' to your method to provide a meaningful description for your rule. ''' ''' The method implementing the rule must return True if the data is valid and ''' return False if the data is invalid. ''' ''' Public Delegate Function RuleHandler(ByVal target As Object, ByVal e As RuleArgs) As Boolean #End Region #Region " RuleArgs class " ''' ''' Object providing extra information to methods that ''' implement business rules. ''' Public Class RuleArgs Private mPropertyName As String Private mDescription As String ''' ''' The (optional) name of the property to be validated. ''' Public ReadOnly Property PropertyName() As String Get Return mPropertyName End Get End Property ''' ''' Set by the rule handler method to describe the broken ''' rule. ''' ''' ''' ''' If the rule handler sets this property, this value will override ''' any description attribute value associated with the rule handler ''' method. ''' ''' The description string returned via this property ''' is provided to the UI or other consumer ''' about which rules are broken. These descriptions are intended ''' for end-user display. ''' ''' The description value is a .NET format string, and it can include ''' the following tokens in addition to literal text: ''' ''' {0} - the RuleName value ''' ''' {1} - the PropertyName value ''' ''' {2} - the full type name of the target object ''' ''' {3} - the ToString value of the target object ''' ''' You can use these tokens in your description string and the ''' appropriate values will be substituted for the tokens at ''' runtime. ''' ''' Public Property Description() As String Get Return mDescription End Get Set(ByVal Value As String) mDescription = Value End Set End Property ''' ''' Creates an instance of RuleArgs. ''' Public Sub New() End Sub ''' ''' Creates an instance of RuleArgs. ''' ''' The name of the property to be validated. Public Sub New(ByVal propertyName As String) mPropertyName = propertyName End Sub #Region " Empty " Private Shared mEmptyArgs As New RuleArgs() ''' ''' Returns an empty RuleArgs object. ''' Public Shared ReadOnly Property Empty() As RuleArgs Get Return mEmptyArgs End Get End Property #End Region End Class #End Region #Region " Description attribute " ''' ''' Defines the description of a business rule. ''' ''' ''' ''' The description in this attribute is used by BusinessRules ''' as information that is provided to the UI or other consumer ''' about which rules are broken. These descriptions are intended ''' for end-user display. ''' ''' The description value is a .NET format string, and it can include ''' the following tokens in addition to literal text: ''' ''' {0} - the RuleName value ''' ''' {1} - the PropertyName value ''' ''' {2} - the full type name of the target object ''' ''' {3} - the ToString value of the target object ''' ''' You can use these tokens in your description string and the ''' appropriate values will be substituted for the tokens at ''' runtime. ''' ''' Instead of using this attribute, a rule handler method can ''' set the Description property of the RuleArgs parameter to ''' a description string. That approach can provide a more dynamic ''' way to generate descriptions of broken rules. ''' ''' _ Public Class DescriptionAttribute Inherits Attribute Private mText As String = "" ''' ''' Initializes the attribute with a description. ''' Public Sub New(ByVal description As String) mText = description End Sub ''' ''' Returns the description value of the attribute. ''' Public Overrides Function ToString() As String Return mText End Function End Class #End Region #Region " RuleMethod Class " ''' ''' Tracks all information for a rule. ''' Private Class RuleMethod Private mHandler As RuleHandler Private mTarget As Object Private mRuleName As String Private mArgs As RuleArgs Private mDescription As String ''' ''' Returns the name of the method implementing the rule ''' and the property, field or column name to which the ''' rule applies. ''' Public Overrides Function ToString() As String If RuleArgs.PropertyName Is Nothing Then Return mHandler.Method.Name Else Return mHandler.Method.Name & "!" & RuleArgs.PropertyName End If End Function ''' ''' Returns the delegate to the method implementing the rule. ''' Public ReadOnly Property Handler() As RuleHandler Get Return mHandler End Get End Property ''' ''' Returns the user-friendly name of the rule. ''' Public ReadOnly Property RuleName() As String Get Return mRuleName End Get End Property ''' ''' Returns the name of the field, property or column ''' to which the rule applies. ''' Public ReadOnly Property RuleArgs() As RuleArgs Get Return mArgs End Get End Property ''' ''' Returns the formatted description of the rule. ''' Public ReadOnly Property Description() As String Get If Len(mArgs.Description) > 0 Then Return String.Format(mArgs.Description, RuleName, RuleArgs.PropertyName, TypeName(mTarget), mTarget.ToString) Else Return String.Format(mDescription, RuleName, RuleArgs.PropertyName, TypeName(mTarget), mTarget.ToString) End If End Get End Property ''' ''' Retrieves the description text from the Description ''' attribute on a RuleHandler method. ''' Private Function GetDescription(ByVal handler As RuleHandler) As String Dim attrib() As Object = handler.Method.GetCustomAttributes(GetType(DescriptionAttribute), False) If attrib.Length > 0 Then Return attrib(0).ToString Else Return "{2}.{0}:" End If End Function ''' ''' Creates and initializes the rule. ''' ''' Reference to the object containing the data to validate. ''' The address of the method implementing the rule. ''' The user-friendly name of the rule. ''' A RuleArgs object containing data related to the rule. Public Sub New(ByVal target As Object, ByVal handler As RuleHandler, ByVal ruleName As String, ByVal ruleArgs As RuleArgs) mTarget = target mHandler = handler mDescription = GetDescription(handler) mRuleName = ruleName mArgs = ruleArgs End Sub ''' ''' Creates and initializes the rule. ''' ''' Reference to the object containing the data to validate. ''' The address of the method implementing the rule. ''' The user-friendly name of the rule. ''' The field, property or column to which the rule applies. Public Sub New(ByVal target As Object, ByVal handler As RuleHandler, ByVal ruleName As String, ByVal propertyName As String) mTarget = target mHandler = handler mDescription = GetDescription(handler) mRuleName = ruleName mArgs = New RuleArgs(propertyName) End Sub ''' ''' Invokes the rule to validate the data. ''' ''' True if the data is valid, False if the data is invalid. Public Function Invoke() As Boolean Return mHandler.Invoke(mTarget, mArgs) End Function End Class #End Region #Region " RulesList property " _ Private mRulesList As HybridDictionary Private ReadOnly Property RulesList() As HybridDictionary Get If mRulesList Is Nothing Then mRulesList = New HybridDictionary() End If Return mRulesList End Get End Property #End Region #Region " Adding Rules " ''' ''' Returns the ArrayList containing rules for a rule name. If ''' no ArrayList exists one is created and returned. ''' Private Function GetRulesForName(ByVal ruleName As String) As ArrayList ' get the ArrayList (if any) from the Hashtable Dim list As ArrayList = CType(RulesList.Item(ruleName), ArrayList) If list Is Nothing Then ' there is no list for this name - create one list = New ArrayList() RulesList.Add(ruleName, list) End If Return list End Function ''' ''' Adds a rule to the list of rules to be enforced. ''' ''' ''' ''' A rule is implemented by a method which conforms to the ''' method signature defined by the RuleHandler delegate. ''' ''' The ruleName is used to group all the rules that apply ''' to a specific field, property or concept. All rules applying ''' to the field or property should have the same rule name. When ''' rules are checked, they can be checked globally or for a ''' specific ruleName. ''' ''' The propertyName may be used by the method that implements the rule ''' in order to retrieve the value to be validated. If the rule ''' implementation is inside the target object then it probably has ''' direct access to all data. However, if the rule implementation ''' is outside the target object then it will need to use reflection ''' or CallByName to dynamically invoke this property to retrieve ''' the value to be validated. ''' ''' ''' The method that implements the rule. ''' ''' A user-friendly identifier for the field/property ''' to which the rule applies. ''' Public Sub AddRule(ByVal handler As RuleHandler, ByVal ruleName As String) ' get the ArrayList (if any) from the Hashtable Dim list As ArrayList = GetRulesForName(ruleName) ' we have the list, add our new rule list.Add(New RuleMethod(mTarget, handler, ruleName, RuleArgs.Empty)) End Sub ''' ''' Adds a rule to the list of rules to be enforced. ''' ''' ''' ''' A rule is implemented by a method which conforms to the ''' method signature defined by the RuleHandler delegate. ''' ''' The ruleName is used to group all the rules that apply ''' to a specific field, property or concept. All rules applying ''' to the field or property should have the same rule name. When ''' rules are checked, they can be checked globally or for a ''' specific ruleName. ''' ''' ''' The method that implements the rule. ''' ''' A user-friendly identifier for the field/property ''' to which the rule applies. ''' ''' A RuleArgs object containing data ''' to be passed to the method implementing the rule. Public Sub AddRule(ByVal handler As RuleHandler, ByVal ruleName As String, ByVal ruleArgs As RuleArgs) ' get the ArrayList (if any) from the Hashtable Dim list As ArrayList = GetRulesForName(ruleName) ' we have the list, add our new rule list.Add(New RuleMethod(mTarget, handler, ruleName, ruleArgs)) End Sub ''' ''' Adds a rule to the list of rules to be enforced. ''' ''' ''' ''' A rule is implemented by a method which conforms to the ''' method signature defined by the RuleHandler delegate. ''' ''' The ruleName is used to group all the rules that apply ''' to a specific field, property or concept. All rules applying ''' to the field or property should have the same rule name. When ''' rules are checked, they can be checked globally or for a ''' specific ruleName. ''' ''' The propertyName may be used by the method that implements the rule ''' in order to retrieve the value to be validated. If the rule ''' implementation is inside the target object then it probably has ''' direct access to all data. However, if the rule implementation ''' is outside the target object then it will need to use reflection ''' or CallByName to dynamically invoke this property to retrieve ''' the value to be validated. ''' ''' ''' The method that implements the rule. ''' ''' A user-friendly identifier for the field/property ''' to which the rule applies. ''' ''' ''' The property name on the target object where the rule implementation can retrieve ''' the value to be validated. ''' Public Sub AddRule(ByVal handler As RuleHandler, ByVal ruleName As String, ByVal propertyName As String) ' get the ArrayList (if any) from the Hashtable Dim list As ArrayList = GetRulesForName(ruleName) ' we have the list, add our new rule list.Add(New RuleMethod(mTarget, handler, ruleName, propertyName)) End Sub #End Region #Region " Checking Rules " ''' ''' Checks all the rules for a specific ruleName. ''' ''' The ruleName to be validated. Public Sub CheckRules(ByVal ruleName As String) Dim list As ArrayList ' get the list of rules to check list = CType(RulesList.Item(ruleName), ArrayList) If list Is Nothing Then Exit Sub ' now check the rules Dim rule As RuleMethod For Each rule In list If rule.Invoke() Then UnBreakRule(rule) Else BreakRule(rule) End If Next End Sub ''' ''' Checks all the rules for a target object. ''' Public Sub CheckRules() ' get the rules for each rule name Dim de As DictionaryEntry For Each de In RulesList Dim list As ArrayList list = CType(de.Value, ArrayList) ' now check the rules Dim rule As RuleMethod For Each rule In list If rule.Invoke() Then UnBreakRule(rule) Else BreakRule(rule) End If Next Next End Sub Private Sub UnBreakRule(ByVal rule As RuleMethod) If rule.RuleArgs.PropertyName Is Nothing Then Assert(rule.ToString, "", False) Else Assert(rule.ToString, "", rule.RuleArgs.PropertyName, False) End If End Sub Private Sub BreakRule(ByVal rule As RuleMethod) If rule.RuleArgs.PropertyName Is Nothing Then Assert(rule.ToString, rule.Description, True) Else Assert(rule.ToString, rule.Description, rule.RuleArgs.PropertyName, True) End If End Sub #End Region #End Region ' Rule Manager #Region " Assert methods " ''' ''' This method is called by business logic within a business class to ''' indicate whether a business rule is broken. ''' ''' ''' Rules are identified by their names. The description field is merely a ''' comment that is used for display to the end user. When a rule is marked as ''' broken, it is recorded under the rule name value. To mark the rule as not ''' broken, the same rule name must be used. ''' ''' The name of the business rule. ''' The description of the business rule. ''' True if the value is broken, False if it is not broken. Public Sub Assert(ByVal Rule As String, ByVal Description As String, ByVal IsBroken As Boolean) If IsBroken Then mBrokenRules.Add(Rule, Description) Else mBrokenRules.Remove(Rule) End If End Sub ''' ''' This method is called by business logic within a business class to ''' indicate whether a business rule is broken. ''' ''' ''' Rules are identified by their names. The description field is merely a ''' comment that is used for display to the end user. When a rule is marked as ''' broken, it is recorded under the rule name value. To mark the rule as not ''' broken, the same rule name must be used. ''' ''' The name of the business rule. ''' The description of the business rule. ''' The property affected by the business rule. ''' True if the value is broken, False if it is not broken. Public Sub Assert(ByVal Rule As String, ByVal Description As String, ByVal [Property] As String, ByVal IsBroken As Boolean) If IsBroken Then mBrokenRules.Add(Rule, Description, [Property]) Else mBrokenRules.Remove(Rule) End If End Sub #End Region #Region " Status retrieval " ''' ''' Returns a value indicating whether there are any broken rules ''' at this time. If there are broken rules, the business object ''' is assumed to be invalid and False is returned. If there are no ''' broken business rules True is returned. ''' ''' A value indicating whether any rules are broken. Public ReadOnly Property IsValid() As Boolean Get Return mBrokenRules.Count = 0 End Get End Property ''' ''' Returns a value indicating whether a particular business rule ''' is currently broken. ''' ''' The name of the rule to check. ''' A value indicating whether the rule is currently broken. Public Function IsBroken(ByVal Rule As String) As Boolean Return mBrokenRules.Contains(Rule) End Function ''' ''' Returns a reference to the readonly collection of broken ''' business rules. ''' ''' ''' The reference returned points to the actual collection object. ''' This means that as rules are marked broken or unbroken over time, ''' the underlying data will change. Because of this, the UI developer ''' can bind a display directly to this collection to get a dynamic ''' display of the broken rules at all times. ''' ''' A reference to the collection of broken rules. Public Function GetBrokenRules() As RulesCollection Return mBrokenRules End Function ''' ''' Returns the text of all broken rule descriptions, each ''' separated by cr/lf. ''' ''' The text of all broken rule descriptions. Public Overrides Function ToString() As String Dim obj As New System.Text.StringBuilder Dim item As Rule Dim first As Boolean = True For Each item In mBrokenRules If first Then first = False Else obj.Append("|") 'Modified by John to be pipe delimited for separation and localization in UI level End If obj.Append(item.Description) Next Return obj.ToString End Function #End Region End Class