From Rockford Lhotka's Expert C# 2008 and VB 2008 Business Objects books
Hi all
I’m currently implementing an application that uses the party/role concept. For instance I have a Party class, that has a collection of Addresses and a collection of ContactMethods. The Party class can represent either a person or an organization.I have created IPerson and IOrganization interfaces which the Party class implements. For example, the IPerson interface has FirstName, MiddleName and LastName properties whereas the IOrganization interface only has an OrganizationName property.All of these classes and interfaces are put into a separate assembly to make them as reusable as possible.
In my application I have several root classes such as Doctor, School, Institution and so on, that each has a Party object embedded using composition. Each of these classes implement either the IPerson or the IOrganization interface which passes the properties on to the embedded Party object. This is to save the front-end developer from having to worry about whether or not to fill out first-, middle and lastname or organizationname, depending on which type of Party he/she is dealing with.
All this works like a charm. However, if a rule is broken in the Party object or one of it’s Address or ContactMethod objects I can’t figure out how to get to these. They won’t be in the root objects BrokenRulesCollection. I could add an extra property, to the root object, that returns the Party object’s BrokenRulesCollection, but this, kind of, doesn’t “feel” right.I thought of, instead of implementing the IPerson or IOrganization interface directly in my root object, I would have a property that returns the Party object, but still I would like to return it as either one of the interface types and then I’m stuck with the same problem.
Doing some investigation into the depths of the framework I could of course implement an IBrokenRules interface that both the IPerson and IOrganization interfaces inherits from. This actually leads to my question(s). Is this the right way to go or am I missing something entirely? Is it correct of me to go the interface route in this situation or shouldn’t I use interfaces and just return the plain business objects and let it be up the the front-end developer to figure out which properties to set?
Thanks in advanceHenrik
<quote>I could add an extra property, to the root object, that returns the Party object’s BrokenRulesCollection, but this, kind of, doesn’t “feel” right.</quote>
My view is that a master BO which contains other BOs should be able to list out all rules that are broken using a single call. This is why I implemented AllRules in my custom layer that inherits from Csla. It was posted in the old message board and I have been using it for a long time now.
It can be called like this:
=============================================================
AllRules.GetAllBrokenRulesString(mSomeBO)
<Serializable()> _ Public MustInherit Class MyBusinessBase(Of T As MyBusinessBase(Of T)) Inherits BusinessBase(Of T)
#Region " AllRules "
Private Shared BIZ_TYPE As Type = GetType(MyBusinessBase(Of T)) Private Shared COLL_TYPE As Type = GetType(MyBusinessListBase(Of Core.BusinessBase))
Public Function GetAllBrokenRulesString() As String Dim sb As New Text.StringBuilder Return GetBrokenRulesString(Me, sb) End Function
Private Function GetBrokenRulesString(ByVal root As MyBusinessBase(Of T), ByRef sb As Text.StringBuilder) As String Dim j, k As Integer Dim t As Type = root.GetType() Dim obj As MyBusinessBase(Of T) Dim coll As MyBusinessListBase(Of Core.BusinessBase) Dim strg As String = root.BrokenRulesCollection.ToString
If Len(strg) > 0 Then sb.Append("(" & root.Table & ")" & vbCrLf & strg & vbCrLf) End If
Dim fi As System.Reflection.FieldInfo() = t.GetFields((BindingFlags.Public Or BindingFlags.Instance Or BindingFlags.NonPublic)) For j = 0 To fi.Length - 1 If fi(j).FieldType.IsSubclassOf(COLL_TYPE) Then coll = CType(fi(j).GetValue(root), MyBusinessListBase(Of Core.BusinessBase)) If Not coll Is Nothing Then 'this is a child collection, cascade the call For k = 0 To coll.Count - 1 obj = CType(coll(k), MyBusinessBase(Of T)) If Not obj Is Nothing Then GetBrokenRulesString(obj, sb) End If Next End If ElseIf fi(j).FieldType.IsSubclassOf(BIZ_TYPE) Then obj = CType(fi(j).GetValue(root), MyBusinessBase(Of T)) If Not obj Is Nothing Then GetBrokenRulesString(obj, sb) End If End If Next Return sb.ToString End Function
#End Region
End Class
Hello Henrick,
I have to say that I agree with Joe, but I also wanted to mention that we're following the contact module design that you're talking about as well - I've seen different flavors of it but the terms contact mechanism and party roles are unmistakable.
Feel free to contact me if you'd ever like to discuss particular challenging areas (contact mechanisms and the sharing of them is a challenging aspect imo).
Regards,
Chris
Joe
Thank you for your suggestion. I have actually come across your function on the old messageboard, but had forgotten about it. I particularly like that the method is generic and can be implemented in BusinessBase.The method will work perfectly for objects “acting-as” another object, by implementing it’s interface and hiding the actual object from the front-end developer.
For child-objects visible to the front-end developer I’m not sure that returning a single string with all broken rules for all child-objects will solve my problem. Many of my UI’s contains multiple tab pages and I would like to be able to pinpoint the exact fields that is causing validation errors. If I just return a string with all broken rules for the root and all child-objects I cannot distinguish them from each other. In this situation I will still rely on the “GetBrokenRulesCollection” of each child object.
Please see my reply to Joe Fallon for how I'm going to solve my problem.
This is my 6th application where I use the party/role concept. Some of my app's just use the party and it's address and contactmethod collections as simple datacontainers where each person or organization has it's own set og addresses and contactmethods. In only two of my applications have I implemented sharing of contact mechanisms and I agree with you, it's very challenging. It isn't trivial.If I run into other challeging areas of these concepts I will take you up on your offer. You may also contact me, if there is any info I can give you.I picked the party/role concept up from Len Silverston's book "The Data Model Resource Book" volume 1 & 2, which I can highly recommend. He presents a conceptual model og how to deal with people and organizations in a combined party concept, which roles these parties play and how they relate to each other.
/Henrik
Mike,
Turns out this was a tough issue for me too. I finally decided that the use of Generics was incorrect in this code. On a side note you can get COLL_TYPE to compile using this totally unintuitive trick:
Private Shared COLL_TYPE As Type = GetType(MyBusinessListBase(Of , ))It never occured to me that you could *omit* the T, C parameters!
Anyway - it turns out that "generics are NOT polymorphic". Memorize that one! If you want to treat a bunch of various BOs as the same type then you can't use their generic base class - you need to "find" a common interface. That is one reason that Rocky has been adding so many interfaces to Csla 2.1. If you can't "find" one of his then you need to build your own.
I did not find one for collections so I built this one:
Public Interface IBusinessCollection ReadOnly Property Item(ByVal index As Integer, ByVal useInterface As Boolean) As ImyBusinessObject ReadOnly Property IsDirty() As Boolean ReadOnly Property IsValid() As Boolean ReadOnly Property Count() As Integer End Interface
Then in my Base class for BusinessListBase I implemented the interface so that all of my concrete collections have it:
#Region " IBusinessCollection Implementation "
'useInterface is a fake parameter and is only used to change the method signature so we can Overload the Item method. 'It does not use the Boolean value at all. 'This way the Base Item method can still return a strongly typed child BO. 'This Item method returns the Interface version of the child BO as IEditableBusinessObject. Public Overloads ReadOnly Property Item(ByVal index As Integer, ByVal useInterface As Boolean) As IMyBusinessObject Implements IBusinessCollection.Item Get Return MyBase.Item(index) End Get End Property
Public Overloads ReadOnly Property Count() As Integer Implements IBusinessCollection.Count Get Return MyBase.Count End Get End Property
Public Overloads ReadOnly Property IsDirty() As Boolean Implements IBusinessCollection.IsDirty Get Return MyBase.IsDirty End Get End Property
'override CSLA2 code to look like this - no short circuiting involved here. 'run through all the child objects and if any are invalid then the collection is invalid 'call IsValid on all children so that when we examine Broken Rules we get the right set of values. 'Rocky stopped looping after he found the first invalid child. Public Overrides ReadOnly Property IsValid() As Boolean Implements IBusinessCollection.IsValid Get Dim result As Boolean = True
For Each child As C In Me If Not child.IsValid Then result = False End If Next
Return result End Get End Property
I did something similar for Root BOs.
Public Interface IMyBusinessObject Inherits IEditableBusinessObject ReadOnly Property BrokenRulesCollection() As Validation.BrokenRulesCollection Property Table() As String Property IsOwner() As Boolean ReadOnly Property OwnedObjects() As IList ReadOnly Property OwnedCollections() As IList End Interface
#Region " IMyBusinessObject Implementation "
Public Overrides ReadOnly Property BrokenRulesCollection() As Validation.BrokenRulesCollection Implements IMyBusinessObject.BrokenRulesCollection Get Return MyBase.BrokenRulesCollection End Get End Property
Public Property Table() As String Implements IMyBusinessObject.Table Get Return mTable End Get Set(ByVal value As String) mTable = value End Set End Property
'set to True if the derived BO contains other BOs. 'Allows the derived classes to skip overriding of IsDirty and IsValid. Public Property IsOwner() As Boolean Implements IMyBusinessObject.IsOwner Get Return mIsOwner End Get Set(ByVal value As Boolean) mIsOwner = value End Set End Property
'Extract just the directly owned objects Protected ReadOnly Property OwnedObjects() As IList Implements IMyBusinessObject.OwnedObjects Get If mOwnedObjects Is Nothing Then GetOwnedFields() End If
Return mOwnedObjects End Get End Property
'Extract just the directly owned collections Protected ReadOnly Property OwnedCollections() As IList Implements IMyIBusinessObject.OwnedCollections Get If mOwnedCollections Is Nothing Then GetOwnedFields() End If
Return mOwnedCollections End Get End Property
'fill the 2 array lists once with the Root BOs and Editable Collections and use many times in Property gets above. Private Sub GetOwnedFields() mOwnedCollections = New ArrayList mOwnedObjects = New ArrayList
Dim fields As FieldInfo() = Me.GetType().GetFields(BindingFlags.Instance Or BindingFlags.Public Or BindingFlags.NonPublic) Dim field As FieldInfo
For Each field In fields If GetType(IBusinessCollection).IsAssignableFrom(field.FieldType) Then mOwnedCollections.Add(field.GetValue(Me)) ElseIf GetType(IMyBusinessObject).IsAssignableFrom(field.FieldType) Then mOwnedObjects.Add(field.GetValue(Me)) End If Next End Sub
Then I re-wrote the code for GetBrokenRulesString to use the 2 new interfaces instead of the generic code which did not work.
Public Overrides ReadOnly Property IsDirty() As Boolean Get If mIsOwner Then Return MyBase.IsDirty OrElse CheckDirty(Me) Else Return MyBase.IsDirty End If End Get End Property
Private Shared Function CheckDirty(ByVal root As MyBusinessBase(Of T)) As Boolean Dim coll As IBusinessCollection
For Each coll In root.OwnedCollections If Not coll Is Nothing Then If coll.IsDirty Then Return True End If End If Next
Dim obj As IMyBusinessObject
For Each obj In root.OwnedObjects If Not obj Is Nothing Then If obj.IsDirty Then Return True End If End If Next
Return False End Function
'we always want to know all broken rules so do not bail out of the logic early if the parent container object is invalid. 'Do NOT change the "And" to "AndAlso". 'Many BOs have code in IsValid to make a final decision on whether or not a rule is broken. If the code never gets called 'then when we list out AllRules.GetAllBrokenRulesString we may not be correct. Public Overrides ReadOnly Property IsValid() As Boolean Get If mIsOwner Then 'Do NOT change the "And" to "AndAlso". Return MyBase.IsValid And CheckValid(Me) Else Return MyBase.IsValid End If End Get End Property
Private Shared Function CheckValid(ByVal root As MyBusinessBase(Of T)) As Boolean Dim coll As IBusinessCollection
For Each coll In root.OwnedCollections If Not coll Is Nothing Then If Not coll.IsValid Then Return False End If End If Next
For Each obj In root.OwnedObjects If Not obj Is Nothing Then If Not obj.IsValid Then Return False End If End If Next Return True End Function
Private Function GetBrokenRulesString(ByVal root As IMyBusinessObject, ByRef sb As Text.StringBuilder) As String Dim i As Integer Dim obj As IMyBusinessObject Dim coll As IBusinessCollection Dim brokenRules As String = root.BrokenRulesCollection.ToString
If Len(brokenRules) > 0 Then sb.Append("(" & root.Table & ")" & vbCrLf & brokenRules & vbCrLf) End If
For Each coll In root.OwnedCollections 'this is a child collection, cascade the call For i = 0 To coll.Count - 1 obj = coll.Item(i, True) If Not obj Is Nothing Then GetBrokenRulesString(obj, sb) End If Next Next
For Each obj In root.OwnedObjects If Not obj Is Nothing Then GetBrokenRulesString(obj, sb) End If Next
Return sb.ToString End Function
When displaying a message to the user about a broken child, I use this linq code which loops through the child properties picking up the invalid business bases within the business list bases (will need a similar query for child business base within currentroot and possibily recursive if more than 2 levels):
var invalidobjs = from prop in CurrentRoot.GetType().GetProperties() where ( (prop.PropertyType.GetInterface(typeof (Csla.Core.ITrackStatus).Name) != null) && (prop.PropertyType.GetInterface(typeof (IQueryable<CHILDOBJ>).Name) != null) ) let value = (Csla.Core.ITrackStatus) prop.GetValue(CurrentRoot, null) where ((!value.IsValid) && (value != null)) let obj = (IQueryable<CHILDOBJ>)value from b in obj where !b.IsValid select b; foreach (var invalidobj in invalidobjs) { var busobj = invalidobj as Csla.Core.BusinessBase; if (busobj == null) continue; View.DisplayBrokenRules(busobj.BrokenRulesCollection); // first invalid child break; }