Wednesday, March 01, 2006

A nice pattern for databinding domain objects

I'm currently working on a windows form application. We've tried to follow good OO principles in designing our business domain classes; encapsulating the business rules, only allowing valid instances of a domain class to be created. We also really like windows form databinding. It's so much easier than writing lots of code to shunt your object's properties back and forth to the form's controls. So, what's the problem. Well, say we have a person class with a name property. The business rules say that the name property can't be an empty string, but how do we create a new person if we can't first bind a blank name to a text box? I've got a nice solution with the use of the 'Builder' pattern. Each domain class has a nested Builder class that is bound to the form instead of an instance of the class itself. The builder allows empty values for required properties so that initially the form has blank fields to be filled in. When the user clicks 'OK', the builder's CreatePerson method is called that returns an instance of the domain class. Because the builder is a nested type it has access to private shared methods that can contain business logic in the domain class, this enforces encapsulation. Validation of the entered fields can be done on the CreateInstance method. Here's the Person class with the nested Builder. Note that the DateOfBirth and Age properties are related via a business rule and that the business rule is also used in the Builder:
Public Class Person

    Private m_name As String
    Private m_age As Integer
    Private m_dateOfBirth As DateTime

    Public Sub New(ByVal name As String, ByVal dateOfBirth As DateTime)
        Me.Name = name
        Me.DateOfBirth = dateOfBirth
    End Sub

    Public Property Name() As String
        Get
            Return m_name
        End Get
        Set(ByVal Value As String)
            If Value = String.Empty Then
                Throw New ValidationException("Name cannot be an empty string")
            End If
            m_name = Value
        End Set
    End Property

    Public Property DateOfBirth() As DateTime
        Get
            Return m_dateOfBirth
        End Get
        Set(ByVal Value As DateTime)
            m_dateOfBirth = Value
            m_age = CalculateAgeFromDob(m_dateOfBirth)
        End Set
    End Property

    Private Shared Function CalculateAgeFromDob(ByVal dateOfBirth As DateTime) As Integer
        Return ((DateTime.Now().Subtract(dateOfBirth).TotalDays) / 360) - 1
    End Function

    Private Shared Function CalculateDobFromAge(ByVal age As Integer, ByVal dateOfBirth As DateTime) As DateTime
        Return New DateTime(DateTime.Now.Year - age, dateOfBirth.Month, dateOfBirth.Day)
    End Function

    Public Property Age() As Integer
        Get
            Return m_age
        End Get
        Set(ByVal Value As Integer)
            m_age = Value
            m_dateOfBirth = CalculateDobFromAge(m_age, m_dateOfBirth)
        End Set
    End Property

    Public Overrides Function ToString() As String
        Return m_name & " " & m_age.ToString() & " " & m_dateOfBirth.ToShortDateString()
    End Function

    Public Class Builder

        Private m_name As String
        Private m_age As Integer
        Private m_dateOfBirth As DateTime

        Public Sub New()
            '
            ' initialise to starting values
            '
            m_name = ""
            m_age = 0
            m_dateOfBirth = DateTime.Now
        End Sub

        Public Function GetPerson() As Person
            Return New Person(m_name, m_dateOfBirth)
        End Function

        Public Property Name() As String
            Get
                Return m_name
            End Get
            Set(ByVal Value As String)
                m_name = Value
            End Set
        End Property

        Public Property DateOfBirth() As DateTime
            Get
                Return m_dateOfBirth
            End Get
            Set(ByVal Value As DateTime)
                m_dateOfBirth = Value
                m_age = CalculateAgeFromDob(m_dateOfBirth)
            End Set
        End Property

        Public Property Age() As Integer
            Get
                Return m_age
            End Get
            Set(ByVal Value As Integer)
                m_age = Value
                m_dateOfBirth = CalculateDobFromAge(m_age, m_dateOfBirth)
            End Set
        End Property

    End Class

End Class

And here's the interesting bits of the form...
Public Class MainForm
    Inherits System.Windows.Forms.Form

    Private m_person As Person
    Private m_personBuilder As Person.Builder

    Private Sub BindPerson(ByVal person As Person)

        m_person = person
        Me.m_nameTextBox.DataBindings.Add("Text", m_person, "Name")
        Me.m_dateOfBirthTextBox.DataBindings.Add("Text", m_person, "DateOfBirth")
        Me.m_ageTextBox.DataBindings.Add("Text", m_person, "Age")

    End Sub

    Private Sub BindPersonBuilder(ByVal personBuilder As Person.Builder)

        m_personBuilder = personBuilder
        Me.m_nameTextBox.DataBindings.Add("Text", m_personBuilder, "Name")
        Me.m_dateOfBirthTextBox.DataBindings.Add("Text", m_personBuilder, "DateOfBirth")
        Me.m_ageTextBox.DataBindings.Add("Text", m_personBuilder, "Age")

    End Sub

#Region " Windows Form Designer generated code "

    Public Sub New(ByVal personBuilder As Person.Builder)
        MyBase.New()
        InitializeComponent()
        BindPersonBuilder(personBuilder)
    End Sub

    Public Sub New(ByVal person As Person)
        MyBase.New()

        'This call is required by the Windows Form Designer.
        InitializeComponent()

        'Add any initialization after the InitializeComponent() call
        BindPerson(person)
    End Sub

    ....

#End Region

    Private Sub m_showPersonButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles m_showPersonButton.Click

        Try
            m_person = m_personBuilder.GetPerson()
            MessageBox.Show(m_person.ToString())
        Catch ex As ValidationException
            MessageBox.Show(ex.Message)
        End Try

    End Sub

End Class

No comments: