Referential Integrity - Disable child deletion via Validation Rule?

Referential Integrity - Disable child deletion via Validation Rule?

Old forum URL: forums.lhotka.net/forums/t/9387.aspx


alex.williamson@grampianfasteners.com posted on Tuesday, August 17, 2010


This is a long post with source, my apologies but I wanted to be clear with my issue and existing code. I'm sure the answer will be simple and not a case of just catching an exception.

Background:

An IIR (EditableRoot) has a SourceId property (Integer) which must exist. This is enabled through the use of Validation Rules in the IIR:

        Protected Overrides Sub AddBusinessRules()
            ...other rules
            ValidationRules.AddRule(Of IIR)(AddressOf IIR.SourceIdMustExistRule, SourceIdProperty)
            ...other rules
        End SubListB is an EditableRootList which has child items ObjectB. 

 
The rule works fine and goes along the lines of:
 
        Private Shared Function SourceIdMustExistRule(Of T As IIR)(ByVal target As T, ByVal e As Csla.Validation.RuleArgs) As Boolean
            Dim SourceInfoListInstance As SourceKeyValueCollection = SourceKeyValueCollection.GetIIRSourceKeyValueCollection
            If SourceInfoListInstance.ContainsKey(target.ReadProperty(SourceIdProperty)) = True Then
                Return True
            Else
                e.Description = "A Source with that Id does not exist (Id: " & target.ReadProperty(SourceIdProperty) & ")"
                Return False
            End If
        End Function
 

SourceKeyValueCollection is currently a non-cached (will be cached soon) read-only key-value collection of SourceId -> SourceName items.

This all works fine.


Problem:

I want to allow the users to edit the list of sources, but I don't want them to be able to delete any sources if they're in use in an IIR. This rule should be easy to implement in the Validation Rules of an editable Source item. If the source isn't used it can be deleted.

I created a SourceList (BusinessListBase) with child SourceListItems (BusinessBase) as per p179 in BO 2008.

CRUD works well and the items work as expected, except limiting the ability to cancel delete if the SourceListItem's Id is in use in an IIR.

SourceList Implementation:

 

Imports Csla.Data
Imports System
Imports System.Collections.Generic
Imports Csla
Imports Csla.Security
 
<Serializable()> _
Public Class SourceList
    Inherits BusinessListBase(Of SourceList, SourceListItem)
 
#Region "Authorization Rules"
 
    Protected Overrides Function AddNewCore() As Object
        Dim obj As SourceListItem = SourceListItem.NewSourceListItem
        Me.Add(obj)
        Return obj
    End Function
 
    Public Function GetDefault() As SourceListItem
        For Each Item As SourceListItem In Me
            If Item.IsDefault = True Then Return Item
        Next
        Return Nothing
    End Function
 
    Protected Shared Sub AddObjectAuthorizationRules()
        Csla.Security.AuthorizationRules.AllowEdit(GetType(SourceList), My.Resources.QAManagers, "Administrators")
    End Sub
 
#End Region
 
#Region "Factory Methods"
 
    Public Shared Function NewSourceList() As SourceList
        Return DataPortal.Create(Of SourceList)()
    End Function
 
    Public Shared Function GetSourceList() As SourceList
        Return DataPortal.Fetch(Of SourceList)()
    End Function
 
    ' Require use of factory methods 
    Private Sub New()
        AllowNew = True
    End Sub
 
#End Region
 
#Region "Data Access"
 
    Private Overloads Sub DataPortal_Fetch()
        RaiseListChangedEvents = False
        Using ctx = ContextManager(Of LinqDAL.IIRTrackerDBLinqDataContext).GetManager(LinqDAL.Database.IIRData)
            Dim data = (From o In ctx.DataContext.IIRSources Select o Order By o.Name Ascending).ToArray
            For Each obj As LinqDAL.IIRSource In data
                Me.Add(SourceListItem.GetSourceListItem(obj))
            Next
        End Using
        RaiseListChangedEvents = True
    End Sub
 
    Protected Overloads Sub DataPortal_Update()
        Me.Child_Update()
    End Sub
 
#End Region
End Class
 

 

SourceListItem Implementation:

 

Imports Csla.Security
Imports Csla
Imports Csla.Data
 
<Serializable()> _
Public Class SourceListItem
    Inherits BusinessBase(Of SourceListItem)
 
#Region " Business Methods "
 
    Private Shared IdProperty As PropertyInfo(Of Integer) = RegisterProperty(New PropertyInfo(Of Integer)("Id", "Id"))
    ''' <Summary>
    ''' Gets and sets the Id value.
    ''' </Summary>
    Public Property Id() As Integer
        Get
            Return GetProperty(IdProperty)
        End Get
        Set(ByVal value As Integer)
            SetProperty(IdProperty, value)
        End Set
    End Property
 
    Private Shared NameProperty As PropertyInfo(Of String) = RegisterProperty(New PropertyInfo(Of String)("Name", "Name"))
    ''' <Summary>
    ''' Gets and sets the Name value.
    ''' </Summary>
    Public Property Name() As String
        Get
            Return GetProperty(NameProperty)
        End Get
        Set(ByVal value As String)
            SetProperty(NameProperty, value)
        End Set
    End Property
 
    Private Shared IsDefaultProperty As PropertyInfo(Of Boolean) = RegisterProperty(New PropertyInfo(Of Boolean)("IsDefault", "IsDefault"))
    ''' <Summary>
    ''' Gets and sets the IsDefault value.
    ''' </Summary>
    Public Property IsDefault() As Boolean
        Get
            Return GetProperty(IsDefaultProperty)
        End Get
        Set(ByVal value As Boolean)
            SetProperty(IsDefaultProperty, value)
        End Set
    End Property
 
#End Region
 
#Region " Validation Rules "
 
    Protected Overrides Sub AddBusinessRules()
        ValidationRules.AddRule(Of SourceListItem)(AddressOf Csla.Validation.CommonRules.StringRequired, NameProperty)
        ValidationRules.AddRule(Of SourceListItem)(AddressOf OnlyOneDefaultSourceListItem, IsDefaultProperty)
        ValidationRules.AddRule(Of SourceListItem)(AddressOf UniqueNameRule, NameProperty)
    End Sub
 
    Private Shared Function UniqueNameRule(Of T As SourceListItem)(ByVal target As T, ByVal e As Csla.Validation.RuleArgs) As Boolean
        Dim Parent As SourceList = DirectCast(target.Parent, SourceList)
        If Parent IsNot Nothing Then
            For Each item As SourceListItem In Parent
                If item.Name = target.ReadProperty(NameProperty) AndAlso Not (ReferenceEquals(item, target)) Then
                    e.Description = "There is another IIR Source List Item with that Name (Current: " & target.Name & ", In Database: " & item.Name & "). Please choose another Id"
                    Return False
                End If
            Next
        End If
        Return True
    End Function
 
    Private Shared Function OnlyOneDefaultSourceListItem(Of T As SourceListItem)(ByVal target As T, ByVal e As Csla.Validation.RuleArgs) As Boolean
        Dim Parent As SourceList = DirectCast(target.Parent, SourceList)
        If Parent IsNot Nothing Then
            If target.IsDefault = True Then
                For Each item As SourceListItem In Parent
                    If item.IsDefault = True AndAlso Not (ReferenceEquals(item, target)) Then
                        e.Description = "There is another IIR Source set as default"
                        Return False
                    End If
                Next
            End If
        End If
        Return True
    End Function
 
#End Region
 
#Region " Authorization Rules "
 
    Protected Overrides Sub AddAuthorizationRules()
        AuthorizationRules.AllowWrite(IdProperty, "IIR_QAManagers", "Administrators")
        AuthorizationRules.AllowWrite(NameProperty, "IIR_QAManagers", "Administrators")
        AuthorizationRules.AllowWrite(IsDefaultProperty, "IIR_QAManagers", "Administrators")
    End Sub
 
    Protected Shared Sub AddObjectAuthorizationRules()
        Csla.Security.AuthorizationRules.AllowEdit(GetType(SourceListItem), "IIR_QAManagers", "Administrators")
    End Sub
 
#End Region
 
#Region " Factory Methods "
 
    Friend Shared Function NewSourceListItem() As SourceListItem
        Return DataPortal.CreateChild(Of SourceListItem)()
    End Function
 
    Friend Shared Function GetSourceListItem(ByVal childData As LinqDAL.IIRSource) As SourceListItem
        Return DataPortal.FetchChild(Of SourceListItem)(childData)
    End Function
 
    Private Sub New()
        'Require use of factory methods
    End Sub
 
#End Region
 
#Region " Data Access "
 
    Protected Overrides Sub Child_Create()
        'TODO: load default values
        'omit this override if you have no defaults to set
        MyBase.Child_Create()
    End Sub
 
    Private Sub Child_Fetch(ByVal childData As LinqDAL.IIRSource)
        With childData
            LoadProperty(IdProperty, .Id)
            LoadProperty(NameProperty, .Name)
            LoadProperty(IsDefaultProperty, .IsDefault)
        End With
    End Sub
 
    Private Sub Child_Insert()
        Using ctx = ContextManager(Of LinqDAL.IIRTrackerDBLinqDataContext).GetManager(LinqDAL.Database.IIRData)
            Dim data = New LinqDAL.IIRSource
            With data
                .Name = ReadProperty(NameProperty)
                .IsDefault = ReadProperty(IsDefaultProperty)
            End With
            ctx.DataContext.IIRSources.InsertOnSubmit(data)
            ctx.DataContext.SubmitChanges()
            LoadProperty(IdProperty, data.Id)
        End Using
    End Sub
 
    Private Sub Child_Update()
        Using ctx = ContextManager(Of LinqDAL.IIRTrackerDBLinqDataContext).GetManager(LinqDAL.Database.IIRData)
            Dim data = (From o In ctx.DataContext.IIRSources Where o.Id = ReadProperty(IdProperty) Select o).Single
            With data
                .Name = ReadProperty(NameProperty)
                .IsDefault = ReadProperty(IsDefaultProperty)
            End With
            ctx.DataContext.SubmitChanges()
        End Using
    End Sub
 
    Friend Sub Child_DeleteSelf()
        Using ctx = ContextManager(Of LinqDAL.IIRTrackerDBLinqDataContext).GetManager(LinqDAL.Database.IIRData)
            Dim data = (From o In ctx.DataContext.IIRSources Where o.Id = ReadProperty(IdProperty) Select o).Single
            ctx.DataContext.IIRSources.DeleteOnSubmit(data)
            ctx.DataContext.SubmitChanges()
        End Using
    End Sub
 
#End Region
 
End Class
 

What I've tried so far:

  • I couldn't see anything in the book for allowing optional deleting of child items.  
  • I first thought I could override Remove(object) on the parent SourceList, but the only thing I can do is override RemoveItem(integer) which isn't the same.  
  • Then I wondered if I could override Delete() on the child SourceListItem, but this method is never called:

 

    Public Overrides Sub Delete()
        CanExecuteMethod("Delete", True)     
        MyBase.Delete()

    End Sub

for completeness here is the CanExecuteMethod:

    Public Overrides Function CanExecuteMethod(ByVal methodName As String) As Boolean
        If methodName = "Delete" Then
            Return False    ' just to get it to work ;-)
        End If
        Return MyBase.CanExecuteMethod(methodName)
    End Function
 
  • I then tried adding a validation rule which I thought would run when the item is saved:

        Private Shared Function CanDeleteRule(Of T As SourceListItem)(ByVal target As T, ByVal e As Csla.Validation.RuleArgs) As Boolean
            If target.IsDeleted AndAlso SourceUsed.SourceUsed(target.ReadProperty(IdProperty)) Then
                e.Description = "Source used in at least one IIR and cannot be deleted."
                Return False
            End If
            Return True
        End Function

    but what property do I bind it to? I tried binding it to IsDeleted which didn't work and then tried calling CheckRules() in an overloaded Save() but that doesn't work either.   :(  
  • The only thing that has worked to far is to do:
    Friend Sub Child_DeleteSelf()
        If SourceUsed.SourceUsed(ReadProperty(IdProperty)) Then Throw New InvalidOperationException("Cannot delete a source if it used elsewhere.")
 
SourceUsed is a working and tested CommandBase:
 
Imports Csla
Imports Csla.Data
 
<Serializable()> _
Public Class SourceUsed
    Inherits CommandBase
 
#Region " Authorization Rules "
 
    Public Shared Function CanExecuteCommand() As Boolean
        Return True
    End Function
 
    Private _id As Integer = 0
    Private _used As Boolean = False
 
    Public ReadOnly Property Used As Boolean
        Get
            Return _used
        End Get
    End Property
 
#End Region
 
#Region " Factory Methods "
 
    Public Shared Function SourceUsed(ByVal id As Integer) As Boolean
        Dim result As SourceUsed = DataPortal.Execute(Of SourceUsed)(New SourceUsed(id))
        Return result.Used
    End Function
 
    Private Sub New(ByVal id As Integer)
        _id = id
    End Sub
 
#End Region
 
#Region " Server-side Code "
 
    Protected Overrides Sub DataPortal_Execute()
        Using ctx = ContextManager(Of LinqDAL.IIRTrackerDBLinqDataContext).GetManager(LinqDAL.Database.IIRData)
            Dim data = (From o In ctx.DataContext.IIRs Where o.SourceId = _id)
            If data.Count > 0 Then _used = True
        End Using
    End Sub
 
#End Region
 
End Class
 

Other forum posts I tried however gave no joy:
 
Old topic on Referential Integrity which uses an ugly exception method - not nice:
http://forums.lhotka.net/forums/p/4943/24055.aspx

This got close, suggesting a CanExecuteMethod which asks if it can be deleted (just for the UI developer - not implemented in object rules), but doesn't really do a Validation Rule - is this really the best way?
http://forums.lhotka.net/forums/p/7134/34125.aspx

 

 

alex.williamson@grampianfasteners.com replied on Friday, August 20, 2010

We went with Overriding the RemoveAt(index) on the list and throwing an exception there.

We also added a CanRemoveItem(throwonfalse) to the list so it can be queried by the UI dev.

ajj3085 replied on Friday, August 20, 2010

Hi, yes, that's how I probably would have recommended you handle this anyway, if a late opinion helps at all. Smile

alex.williamson@grampianfasteners.com replied on Monday, August 23, 2010

Andy

Hi, yes, that's how I probably would have recommended you handle this anyway, if a late opinion helps at all. Smile

Yep that's good - thanks for replying. Had to make sure I'm going at least some way in the right direction!

Copyright (c) Marimer LLC