OnIsDirtyChanged throws NullReferenceExceptionOnIsDirtyChanged 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:
- Open the root edit form, call beginedit on the root.
- 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.
- 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.
- Cancel Out of the Root Edit Form.
- UndoChanges is Called on the Root.
- UndoChanges is called on the child collection.
- UndoChanges is called on the Child (I don't understand why we would want this, since it is being removed anyway).
- 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
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:
- Open the root edit form, call beginedit on the
root.
- 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.
- 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.
- Cancel Out of the Root Edit Form.
- UndoChanges is Called on the Root.
- UndoChanges is called on the child
collection.
- UndoChanges is called on the Child (I don't
understand why we would want this, since it is being removed anyway).
- 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
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