Monday, October 31, 2005
I recently gave the keynote address at the Heartland Developer's Conference. While there, I was interviewed by PodcastStudio.net and the interview is now online for listening. At the moment it is here, and I'm sure after that it will move to the archive.
Monday, October 31, 2005 10:14:17 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  | 
 Thursday, October 13, 2005

ASP.NET 2.0 has bi-directional data binding, which is a big step forward. This means you can bind the UI to a data source (DataTable, object, etc.) and not only display the data, but allow the user to do “in-place” editing that updates the data source when the page is posted back.

 

The end result is very cool, since it radically reduces the amount of code required for many common data-oriented web pages.

 

Unfortunately the data binding implementation isn’t very flexible when it comes to objects. The base assumption is that “objects” aren’t intelligent. In fact, the assumption is that you are binding to “data objects” – commonly known as Data Transfer Objects or DTOs. They have properties, but no logic.

 

In my case I’m working with CSLA .NET 2.0 objects – and they do have logic. Lots of it, validation, authorization and so forth. They are “real” objects in that they are behavior-based, not data-based. And this causes a bit of an issue.

 

There are two key constraints that are problematic.

 

First, ASP.NET insists on creating instances of your objects at will – using a default constructor. CSLA .NET follows a factory method model where objects are always created through a factory method, providing for more control, abstraction and flexibility in object design.

 

Second, when updating data (the user has edited existing data and clicked Update), ASP.NET creates an instance of your object, sets its properties and calls an Update method. CSLA .NET objects are aware of whether they are new or old – whether they contain a primary key value that matches a value in the database or not. This means that the object knows whether to do an INSERT or UPDATE automatically. But when a CSLA .NET object is created out of thin air it obviously thinks it is new – yet in the ASP.NET case the object is actually old, but has no way of knowing that.

 

The easiest way to overcome the first problem is to make business objects have a public default constructor. Then they play nicely with ASP.NET data binding. The drawback to this is that anyone can then bypass the factory methods and incorrectly create the objects with the New keyword. That is very sad, since it means your object design can’t count on being created the correct way, and a developer consuming the object might use the New keyword rather than the factory method and really mess things up. Yet at present this is how I’m solving issue number one.

 

I am currently solving issue number two through an overloaded Save method in BusinessBase: Save(forceUpdate) where forceUpdate is a Boolean. Set it to True and the business object forces its IsNew flag to False, thus ensuring that an update operation occurs as desired. This solution works wonderfully for the web scenario, but again opens up the door to abuse in other settings. Yet again a consumer of the object could introduce bugs into their app by calling this overload when it isn’t appropriate.

 

The only way out of this mess that I can see is to create my own ASP.NET data control that understands how CSLA .NET objects work. I haven’t really researched that yet, so I don’t know how complex it is to write such a control. I did try to subclass the current Object control, but they don’t provide extensibility points like I need, so that doesn’t work. The only answer is probably to subclass the base data control class itself…

Thursday, October 13, 2005 6:17:12 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  | 
 Thursday, October 06, 2005

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 2:18:16 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  | 
 Wednesday, October 05, 2005
Bill McCarthy has come up with a nice extender control that wraps the solution to the data binding issue I've been discussing. Thanks Bill!!
Wednesday, October 05, 2005 3:09:52 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [0]  |