OnIsDirtyChanged throws NullReferenceException

OnIsDirtyChanged throws NullReferenceException

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


dcalens posted on Tuesday, July 11, 2006

I have a typical scenario with a root object that contains a child collection of 0 to many objects.  I use a child edit form that is opened from the root edit form, rather than editing directly in a datagrid.  I am also using CSLA 1.53 in a .NET 2.0 UI.

The code that opens the child edit form is similar to examples on this and the old forum:

Dim frm As New ChildEdit

'AddNewChild creates new child with default values, adds to collection, and returns the child.
frm.Child = Root.Children.AddNewChild()

frm.Child.BeginEdit()
Root.BeginEdit()
frm.ShowDialog()
 If frm.DialogResult = Windows.Forms.DialogResult.OK Then
            frm.Child.ApplyEdit()
            Root.ApplyEdit()
Else
          frm.Child.CancelEdit()
          Root.ApplyEdit()
End If

I call root.beginedit when initially opening the root edit form.  If I add a new child, apply the changes to child and root, but cancel out of the root edit screen, the NullReferenceException is thrown from OnIsDirtyChanged.  The "this" is the child with default values.  I can't figure out exactly what is null, but my best guess is the chid is missing a reference since it wasn't originally there.  Am I not calling my BeginEdits/CancelEdits correctly?

Thanks in advance,
David

ajj3085 replied on Tuesday, July 11, 2006

First, what is AddNewChild?  I'd recommend overriding the protected AddNewCore in your Children collection, and then having the UI call AddNew().

Calling root.BeginEdit will automatically call BeginEdit on all instances in Children; that may be part of the problem... try reversing the order of your calls. 

Its hard to say exactly without more code..

In VS, I'd recommend looking at the call stack when the Exception helper appears; that should help you track down exactly where the Null reference is hiding.

HTH
Andy

dcalens replied on Tuesday, July 11, 2006

Andy:

Thanks for the  reply.  I've overridden the collectionbase innerlist.add several different ways:
 
Public Sub Add()
        list.Add(Child.NewChild())
    End Sub

Public Sub Add(ByVal ID As String)
      list.Add(Child.NewChild(ID))
End Sub

The NewChild function is the shared method that just access the private constructor.  I can either provide an ID, or use the default = 0.  I haven't decided which one I will eventually end up using. 

The AddNewChild would more appropriately be called AddNew.  I use it rather than Add because it returns the newly created child :
Public Function AddNewChild() As Child
        Dim Child As Child= Child.NewChild()
        Me.List.Add(Child)
        Return Child
End Function

I switched the order of the beginedits to no avail.  If the root handles the beginedit on all of it's children, do I even need to explicitly call a beginedit on a child?

I'm a rookie and never used the call stack.  I'm uncertain how to get any information out of it, but I'll research it.  Thanks for the tip.

David

RockfordLhotka replied on Tuesday, July 11, 2006

Almost certainly what is happening here is that your code in the collection that adds the child is somehow overriding or bypassing the normal BusinessCollectionBase processing that has to happen with a new child. In particular, BusinessCollectionBase needs to call SetParent on the child, and do some event hooking.

Normally, when not using data binding (in-place grid editing), you would NOT override the add functionality of the base collection. Rather, you'd just add a new method to add the child - like your AddNewChild() method.

Also, I would not have the parent form call BeginEdit/CancelEdit/ApplyEdit. That is poor encapsulation. The child form should be entirely responsible for the child object while it is in use. In other words, the child form should call BeginEdit as it loads, and should call CancelEdit/ApplyEdit as it is closed. The parent form should not be interacting with the child object like it is in your code.

dcalens replied on Tuesday, July 11, 2006

Thanks for the reply.  I am still having issues, but have a little more insight.  Taking your suggestion, here is the code on my parent edit from prior to opening the child edit form:

Dim frm As New ChildEdit
Root.BeginEdit()
frm.Child = Root.Children.AddNew()
frm.ShowDialog()
If frm.DialogResult = Windows.Forms.DialogResult.OK Then
   Root.ApplyEdit()
Else
   Root.CancelEdit()
End If
So I call another beginedit on the root before adding another item to the collection.  As you suggest, the child edit form handles all the begin/apply/cancel for the child edit (not shown above).

Regarding your first comment, I stepped through adding the child and it triggers the oninsert method of businesscollectionbase, which assigns the editleveladded and parent properties, as well as adds the handler for IsDirtyChanged.  So I don't know if I am sidestepping anything here.

I walked through the stack as Andy suggested and it seems as if MarkDirty is called on the object after the undo is finished.  I made a couple of changes in BusinessBase to prevent the error from being thrown, admittedly having no idea what this could affect later.

Change 1:
Comment out the OnIsDirtyChanged in UndoChangesComplete (this is the first place that threw the error):

Protected Overrides Sub UndoChangesComplete()

mBindingEdit = False
AddBusinessRules()
'OnIsDirtyChanged()
MyBase.UndoChangesComplete()

End Sub

Change 2:
Modify the MarkDeleted method in BusinessBase, which was being called after the child was removed from the list:

Protected Sub MarkDeleted()
   mIsDeleted = True
   If Not mIsNew Then
    MarkDirty()
   End If
End Sub
I can't figure out why an object that has been removed from it's parent collection (with the collection restored to it's previous state) is still receiving calls to OnIsDirtyChanged.

dcalens replied on Wednesday, July 12, 2006

I decided to revert CSLA to it's original state (v 1.53) and trace the steps that produced the NullException for anybody that is interested.  I am happy to provide code for any of it:

  1. Open the root edit form, call beginedit on the root.
  2. Before opening the child edit form,  call another BeginEdit on the Root and add a new Child to the Child Collection (this is demonstrated above).  As Rocky suggested,  I removed the Add overrides for the child collection.  Upon inserting the child, the OnInsert method IS called from businesscollectionbase, which adds properties to the new child and apparently hooks the IsDirtyChanged event.
  3. Modify the Child Object in the Edit form and ApplyEdit.  Apply the Edit to the Root.  At this point, the child is at EditLevel  = 0.  The root is at EditLevel = 1.  I *think* that's how it's supposed to be.
  4. Cancel Out of the Root Edit Form.
  5. UndoChanges is Called on the Root.
  6. UndoChanges is called on the child collection.
  7. UndoChanges is called on the Child (I don't understand why we would want this, since it is being removed anyway).
  8. UndoChangesComplete is called, which calls OnIsDirtyChanged, which throws the NullException.  (I also don't understand why OnIsDirtyChanged is called in UndoChangesComplete.  I also don't understand what is Null.  Does the Child object not have the appropriate event handler?)

Removing the OnIsDirtyChanged call from UndoChangesComplete isn't enough.  Say we get through the UndoChanges on the BusinessCollectionBase.  The child is removed, which calls the OnRemove method.  OnRemove first calls DeleteChild, which calls MarkDeleted, which calls MarkDirty.  This is why I had modified the MarkDeleted method.

I ended up making a couple of other changes to businesscollectionbase that seem to make sense to me and prevent the exception.  I'm not entirely comfortable with them and am still hoping to really resolve the issue.  If anyone is interested in the modifications, let me know. 

RockfordLhotka replied on Wednesday, July 12, 2006

Normally what I do is a bit different
 
1) create the root
2) call beginedit on the root
3) allow user to edit root
4) create/add the child
5) display child in modal edit window
5a) child window calls beginedit on child object
5b) user edits child
5c) child window calls either canceledit or applyedit on child
6) main window calls either canceledit or applyedit on root
 
This is the scenario for which n-level undo was designed, and this model should work.
 
My guess is that the null reference exception you are getting is due to data binding. Some UI component is maintaining a reference to your object graph and the IsDirtyChanged event is causing it to refresh - but your object graphi is somehow returning a null value in some property (probably a string property). Data binding doesn't handle null values, so boom. Also, it is hard to trace this, since you don't have access to the data binding processing - which is why the exception seems like it comes from nowhere. At least that's my guess.
 
Rocky


From: dcalens [mailto:cslanet@lhotka.net]
Sent: Wednesday, July 12, 2006 4:46 PM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] OnIsDirtyChanged throws NullReferenceException

I decided to revert CSLA to it's original state (v 1.53) and trace the steps that produced the NullException for anybody that is interested.  I am happy to provide code for any of it:
  1. Open the root edit form, call beginedit on the root.
  2. Before opening the child edit form,  call another BeginEdit on the Root and add a new Child to the Child Collection (this is demonstrated above).  As Rocky suggested,  I removed the Add overrides for the child collection.  Upon inserting the child, the OnInsert method IS called from businesscollectionbase, which adds properties to the new child and apparently hooks the IsDirtyChanged event.
  3. Modify the Child Object in the Edit form and ApplyEdit.  Apply the Edit to the Root.  At this point, the child is at EditLevel  = 0.  The root is at EditLevel = 1.  I *think* that's how it's supposed to be.
  4. Cancel Out of the Root Edit Form.
  5. UndoChanges is Called on the Root.
  6. UndoChanges is called on the child collection.
  7. UndoChanges is called on the Child (I don't understand why we would want this, since it is being removed anyway).
  8. UndoChangesComplete is called, which calls OnIsDirtyChanged, which throws the NullException.  (I also don't understand why OnIsDirtyChanged is called in UndoChangesComplete.  I also don't understand what is Null.  Does the Child object not have the appropriate event handler?)
Removing the OnIsDirtyChanged call from UndoChangesComplete isn't enough.  Say we get through the UndoChanges on the BusinessCollectionBase.  The child is removed, which calls the OnRemove method.  OnRemove first calls DeleteChild, which calls MarkDeleted, which calls MarkDirty.  This is why I had modified the MarkDeleted method.

I ended up making a couple of other changes to businesscollectionbase that seem to make sense to me and prevent the exception.  I'm not entirely comfortable with them and am still hoping to really resolve the issue.  If anyone is interested in the modifications, let me know. 


dcalens replied on Thursday, July 13, 2006

Rocky ~

I think you are right about databinding.  I made my silly little changes and trudged on, only to encounter a nullreferenceexception when adding a child and calling a MarkOld() on it.  In this case, it seems to be related to PropValueChanged and is specific to using ComboBoxes (I removed them and the error went away).  Several of the properties are set by using a combobox whose datasource is a namevaluelist and that is  bound to child properties. 

Do I have to do something special with a databound combobox?  Here is what I have now:

BindField(cbSeam, "SelectedValue", _specItem, "SeamID")

Regards,
David

dcalens replied on Wednesday, July 26, 2006

To follow up, I managed to resolve the issue.  I found it initially be related to databinding to combo boxes.  The confusing thing was that I wasn't having similar problems in other forms.

After implementing Petar's ActiveObjects, it was brought to my attention that I was missing the <Serializable()> attribute for this particular child collection.  Doh!


dcalens replied on Wednesday, July 12, 2006

Sorry for the confusion on my part.  I forgot about IBindingList.AddNew.  Now I am wondering if I am overriding it correctly and if that is causing my problem.  Is it enough to just put an AddNew method in the child collection?

Public Function AddNew() As Child...

Or do I have to specifically override it somehow?

David

RockfordLhotka replied on Wednesday, July 12, 2006

If you aren't overriding AddNewCore() for data binding, then I would suggest that you DO NOT override any add-related methods at all. Just create another "add" method to create/add your child objects.
 
Rocky


From: dcalens [mailto:cslanet@lhotka.net]
Sent: Wednesday, July 12, 2006 4:10 PM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] OnIsDirtyChanged throws NullReferenceException

Sorry for the confusion on my part.  I forgot about IBindingList.AddNew.  Now I am wondering if I am overriding it correctly and if that is causing my problem.  Is it enough to just put an AddNew method in the child collection?

Public Function AddNew() As Child...

Or do I have to specifically override it somehow?

David



dcalens replied on Wednesday, July 12, 2006

Is AddNewCore specific to CSLA 2.0?  I am using CSLA 1.53 with a 2.0 UI.  If that's a bad idea let me know.  I have reasons to do so that are too long to explain.

I got either of the following methods to add child objects to a collection, with no Add overrides:

Public Function AddNew() As Child
        Dim child As Child = Child..NewChild()
        List.Add(child)
        Return(child)
End Function
Public Function AddNew() As Child
    Return CType(CType(Me, IBindingList).AddNew(), SpecificationItem)
End Function
The second methods calls OnAddNew which I have implemented in my child collection.  Either way I still end up with the original issue.  I'm about to post a follow-up, which has some more information regarding this.

Thanks,
David


RockfordLhotka replied on Wednesday, July 12, 2006

Yes, AddNewCore comes from BindingList<T> and is a .NET 2.0 thing.
 
Rocky

Copyright (c) Marimer LLC