Thursday, October 06, 2005
« Windows Forms binding issue: A component... | Main | ASP.NET 2.0 Data Binding Frustration »

Here's a "basic" CSLA .NET 2.0 editable root class. It pretty much illustrates all the things you can do in a class under the upcoming version of the framework. In particular note the way validation rules are handled and all the transactional options in the DataPortal_XYZ methods (presumably you'd pick one for your app :) ).

A CustomerTypes class is also included at the bottom, illustrating how to implement a name-value list that works with data binding.

Imports CSLA

 

<Serializable()> _

Public Class Customer

  Inherits BusinessBase(Of Customer)

 

#Region " Business Methods "

 

  Private mID As Integer

  Private mLastName As String = ""

  Private mFirstName As String = ""

  Private mLastActivity As SmartDate

  Private mType As Integer

  Private mCity As String = ""

 

  Private Shared mCustomerTypes As CustomerTypes

 

  Public Property City() As String

    Get

      If CanReadProperty() Then

        Return mCity

      Else

        Throw New System.Security.SecurityException( _

          "Property read not allowed")

      End If

    End Get

    Set(ByVal value As String)

      If CanWriteProperty() Then

        If Not mCity.Equals(value) Then

          mCity = value

          PropertyHasChanged()

        End If

      Else

        Throw New System.Security.SecurityException( _

          "Property write not allowed")

      End If

    End Set

  End Property

 

  Public Shared ReadOnly Property CustomerTypes() As CustomerTypes

    Get

      If mCustomerTypes Is Nothing Then

        mCustomerTypes = Library.CustomerTypes.GetCustomerTypes

      End If

      Return mCustomerTypes

    End Get

  End Property

 

  Public ReadOnly Property ID() As Integer

    Get

      Return mID

    End Get

  End Property

 

  Public Property LastName() As String

    Get

      If CanReadProperty() Then

        Return mLastName

      Else

        Throw New System.Security.SecurityException( _

          "Property read not allowed")

      End If

    End Get

    Set(ByVal value As String)

      If CanWriteProperty() Then

        If mLastName <> value Then

          mLastName = value.ToUpper

          PropertyHasChanged()

        End If

      Else

        Throw New System.Security.SecurityException( _

          "Property write not allowed")

      End If

    End Set

  End Property

 

  Public Property FirstName() As String

    Get

      If CanReadProperty() Then

        Return mFirstName

      Else

        Throw New System.Security.SecurityException( _

          "Property read not allowed")

      End If

    End Get

    Set(ByVal value As String)

      If CanWriteProperty() Then

        If mFirstName <> value Then

          mFirstName = value

          PropertyHasChanged()

        End If

      Else

        Throw New System.Security.SecurityException( _

          "Property write not allowed")

      End If

    End Set

  End Property

 

  Public Property LastActivity() As String

    Get

      If CanReadProperty() Then

        Return mLastActivity.Text

      Else

        Throw New System.Security.SecurityException( _

          "Property read not allowed")

      End If

    End Get

    Set(ByVal value As String)

      If CanWriteProperty() Then

        If mLastActivity <> value Then

          mLastActivity.Text = value

          PropertyHasChanged()

        End If

      Else

        Throw New System.Security.SecurityException( _

          "Property write not allowed")

      End If

    End Set

  End Property

 

  Public Property CustomerType() As Integer

    Get

      If CanReadProperty() Then

        Return mType

      Else

        Throw New System.Security.SecurityException( _

          "Property read not allowed")

      End If

    End Get

    Set(ByVal value As Integer)

      If CanWriteProperty() Then

        If mType <> value Then

          mType = value

          PropertyHasChanged()

        End If

      Else

        Throw New System.Security.SecurityException( _

          "Property write not allowed")

      End If

    End Set

  End Property

 

  Public ReadOnly Property CustomerTypeText() As String

    Get

      If CanReadProperty() Then

        Return CustomerTypes.Value(mType)

      Else

        Throw New System.Security.SecurityException( _

          "Property read not allowed")

      End If

    End Get

  End Property

 

#End Region

 

#Region " Business Rules "

 

  Protected Overrides Sub AddBusinessRules()

    AddRule(AddressOf Validation.CommonRules.StringRequired, _

      "FirstName")

    AddRule(AddressOf Validation.CommonRules.StringRequired, _

      "LastName")

  End Sub

 

#End Region

 

#Region " Object ID Value "

 

  Protected Overrides Function GetIdValue() As Object

    Return mID

  End Function

 

#End Region

 

#Region " Constructors "

 

  Private Sub New()

 

    ' don't allow a Guest to see or change city data

    AuthorizationRules.DenyRead("City", "Guest")

    AuthorizationRules.DenyWrite("City", "Guest")

 

  End Sub

 

#End Region

 

#Region " Factory Methods "

 

  Public Shared Function NewCustomer() As Customer

    Return DataPortal.Create(Of Customer)(Nothing)

  End Function

 

  Public Shared Function GetCustomer(ByVal id As Integer) As Customer

    Return DataPortal.Fetch(Of Customer)(New Criteria(id))

  End Function

 

  Public Shared Sub DeleteCustomer(ByVal id As Integer)

    DataPortal.Delete(New Criteria(id))

  End Sub

 

#End Region

 

#Region " Criteria "

 

  <Serializable()> _

  Private Class Criteria

    Public ID As Integer

 

    Public Sub New(ByVal id As Integer)

      Me.ID = id

    End Sub

  End Class

 

#End Region

 

#Region " Data Access "

 

  ' Forced to run on client

  <RunLocal()> _

  Protected Overrides Sub DataPortal_Create(ByVal criteria As Object)

 

    ' get default values from db

    ' we get here via DataPortal.Create()

 

    mID = New System.Random().Next(100, 999)

    mType = CustomerTypes.DefaultKey

    CheckRules()

 

  End Sub

 

  ' Runs on client or server based on DataPortal config

  Protected Overrides Sub DataPortal_Fetch(ByVal criteria As Object)

 

    ' get data from db

    ' we get here via DataPortal.Fetch()

 

    Dim crit As Criteria = DirectCast(criteria, Criteria)

    mID = crit.ID

    mType = CustomerTypes.Key("Hybrid")

    mLastName = "Lhotka"

    mFirstName = "Rocky"

    CheckRules()

 

  End Sub

 

  ' COM+ transactions

  <Transactional()> _

  Protected Overrides Sub DataPortal_Insert()

 

    ' insert data into db

    ' we get here via obj.Save()

 

  End Sub

 

  ' System.Transactions namespace

  Protected Overrides Sub DataPortal_Update()

 

    ' update data in db

    ' we get here via obj.Save()

 

    Using tr As New System.Transactions.TransactionScope

      ' do updates

      tr.Complete()

    End Using

 

  End Sub

 

  ' ADO.NET transactions

  Protected Overrides Sub DataPortal_DeleteSelf()

 

    ' do deferred delete of self

    ' we get here via obj.Save()

 

    Using cn As New SqlClient.SqlConnection

      Using tr As SqlClient.SqlTransaction = cn.BeginTransaction

        ' do delete

      End Using

    End Using

 

  End Sub

 

  ' No transactions

  Protected Overrides Sub DataPortal_Delete(ByVal criteria As Object)

 

    Dim crit As Criteria = DirectCast(criteria, Criteria)

 

    ' do immediate delete based on criteria

    ' we get here via DataPortal.Delete()

 

  End Sub

 

#End Region

 

End Class

 

======================================================

Imports CSLA

 

Public Class CustomerTypes

  Inherits NameValueListBase(Of Integer, String)

 

  Public Function DefaultKey() As Integer

    Return 1

  End Function

 

  Public Function DefaultValue() As String

    Return Value(DefaultKey)

  End Function

 

  Private Sub New()

 

  End Sub

 

  Public Shared Function GetCustomerTypes() As CustomerTypes

    Return DataPortal.Fetch(Of CustomerTypes) _

      (New Criteria(GetType(CustomerTypes)))

  End Function

 

  Protected Overrides Sub DataPortal_Fetch(ByVal criteria As Object)

    Me.IsReadOnly = False

    Add(New NameValuePair(1, "Domestic"))

    Add(New NameValuePair(2, "International"))

    Add(New NameValuePair(3, "Hybrid"))

    Me.IsReadOnly = True

  End Sub

 

End Class

 

Thursday, October 06, 2005 3:20:31 PM (Central Standard Time, UTC-06:00)
Is there any reason why the callback methods from DataPortal are not genericized?
Thursday, October 06, 2005 4:26:30 PM (Central Standard Time, UTC-06:00)
You mean the criteria parameter? While it could be strongly typed, it is totally realistic to have multiple Criteria classes for a given object - especially with collections. I don't know of a way to declare essentially a paramarray of types and have it cascade to n different method implementations.

Still, I'll have to think on that a bit. The DataPortal actually uses reflection to invoke DataPortal_Fetch for instance. Due to that fact, it seems quite possible that you could strongly type the method. Of course that would preclude default implementations in the base class and/or the use of an interface...

Difficult choices all.
Saturday, October 08, 2005 2:48:18 AM (Central Standard Time, UTC-06:00)
A couple of quick questions about the parameter set routine.

1. Are you jumping through any weird System.Diagnostics hoops to find out which property changed in the CanWriteProperty/PropertyHasChanged() routines or are they completely generic and don't know which property has changed?

2. Is there any reason (like performance, better generic understanding) for everything in a two large nested if s as opposed to using a flatter Bail-out structure like
Set(ByVal value As String)
Bail-outs
If CanWriteProperty() = False Then
Throw New System.Security.SecurityException( _
"Property write not allowed")
End If
If mCity.Equals(value) Then
Exit Property
End If

mCity = value
PropertyHasChanged()
End Set

And lastly, not directly related to the post.
Please think about dividing the BusinessBase class into a CSLA.BusinessBase (with Rules and Authorization) and a CSLA.UnValidatedBusinessBase.

This really wont make much difference at the final code level but should allow end-users to stay conform to the generic codebase without conforming to a nonremotable (is that a word?) Rule structure.
Ben Cline
Wednesday, October 12, 2005 11:08:26 PM (Central Standard Time, UTC-06:00)
1. Yes - though I don't think they are wierd :) And they are optional. If you want to manually provide a string literal for the property name and bypass the System.Diagnostics call that's up to you. I let you trade maintainability of code against performance as you choose.

2. You can write your set block as you see fit - as long as the semantic meaning remains consistent it doesn't matter to me or the framework how you do the coding. Honestly though, my goal is to standardize property declarations to the point that you'd be insane not to code-gen them. Then you can just choose how your code-gen tool creates the code :)

3. You can always create your own base classes. There's nothing stopping you from inheriting directly from BindableBase or UndoableBase and bypassing all the behaviors in BusinessBase. As with 1.x, the 2.0 code is designed so you incur virtually no overhead if you inherit from BusinessBase and don't use n-level undo, or valiation rules or authorization rules. You can just ignore those features and pay nothing, or use them and enjoy the benefits.

But if you really don't want them there, create your own base class as you see fit. My ultimate goal isn't to write a framework, but rather to illustrate the issues you must face when creating a framework along with a set of possible solutions.

Rocky
Saturday, October 15, 2005 10:16:05 AM (Central Standard Time, UTC-06:00)
Some questions on RulesManager. My objective is to minimize changes and ease the transition to version 2.0 of the framework.

1) In CSLA 1.5x, there are 3 overloads for the AddRule() method:
- AddRule(handler, ruleName)
- AddRule(handler, string ruleName, string propertyName)
- AddRule(handler, string ruleName, RuleArgs args)
In version 2.0, the semantic of AddRule() is somewhat different, e.g. as shown above, it takes a property name instead. How does this change in semantic affects the transition to version 2.0 from 1.5x? For instance, do you recommend to use 'property name' instead of 'rule name' on the ruleName argument using the 1st overload in version 1.5x to assist smooth transition? Side effects?

2) The PropertyHasChanged() method has another overload that takes a property name parameter. Again, this is somewhat change in semantic compared with CheckRules() in version 1.5x, which takes a rule name. Does it equivalent to do the following in version 1.5x that yields the similar behavior as PropertyHasChanged() in version 2.0?

public class MyBusinessBase : BusinessBase
{
protected void PropertyHasChanged(string propertyName)
{
MarkDirty();
BrokenRules.CheckRules(
propertyName // This is the 'ruleName' argument
);
}
}

public class Customer : MyBusinessBase
{
...
public string FirstName
{
...
set
{
if (_firstName != value)
{
_firstName = value;
PropertyHasChanged(
"FirstName" // This is a rule name, in fact.
);
}
}
}
}
William
Sunday, October 16, 2005 2:17:42 AM (Central Standard Time, UTC-06:00)
I saw an old discussion on BrokenRules.AddRule() at:
http://www.searchcsla.com/Details.aspx?msn_id=3824

My interpretation is correct that the "rule name" parameter in AddRule() of CSLA 1.5x is merely a grouping mechanism. Thus, my concern still exists that the new signature of AddRule() in version 2.0 breaks this semantic?

William
William
Monday, October 17, 2005 3:07:10 PM (Central Standard Time, UTC-06:00)
Rocky,
I think the new code looks great.

Looking at it I couldn't help but notice that every property has the If CanRead/CanWrite error code. As a way to streamline this the security checks can be broken out into CSLA provided subs. The subs could be overloaded to allow the user to use a default exception message or pass in their own.

The end code could look like this:
Public Property LastName() As String
Get
CheckCanReadProperty("Optional message")
return mLastName
End Get
Set(ByVal value As String)
CheckCanWriteProperty("Optional message")
If mLastName <> value Then
mLastName = value.ToUpper
PropertyHasChanged()
End If
End Property





Jeremy
Comments are closed.