I'm not sure if I'm implementing something wrong but when I create a property on an inherited BusinessBase using PropertyInfo, if I do not use a private field to store property data, the property is not undoable. I just want to make sure this is by design.
It should be undoable.
In ProjectTracker, the Project class uses managed backing fields and the Cancel button works fine to undo changes to those properties.
Ok i'm trying a very limited set of code that I believe should mimic the ProjectTracker's editing capabilities of the Project class. The only thing not implemented is the security context. The following code does not work for the Name property and does work for the Color property.
When I trace the code for an undo call (UndoableBase.sc #199 protected internal void UndoChanges(int parentEditlevel), I trace to a "reflect" call of "fields = currentType.GetFields(...);". After the call fields contains 1 field named "_name". So i'm not sure how this undoes managed fields.
using System; using System.Windows.Forms; using Csla; using Csla.Core; namespace WindowsFormsApplication3 { public partial class Form1: Form { public Form1() { InitializeComponent(); this.Load += new System.EventHandler(this.Form1_Load); } Dog d = new Dog(); TextBox t = new TextBox(); TextBox t2 = new TextBox(); Button bCancel = new Button(); Button bApply = new Button(); BindingSource bs = new BindingSource(); public void Form1_Load(object sender, EventArgs e) { t.Left = 10; t.Top = 50; Controls.Add(t); t2.Left = 120; t2.Top = 50; Controls.Add(t2); bCancel.Text = "Cancel Edit"; bCancel.Left = 10; bCancel.Top = 90; Controls.Add(bCancel); bCancel.Click += new EventHandler(bCancel_Click); bApply.Text = "Apply Edit"; bApply.Left = 90; bApply.Top = 90; Controls.Add(bApply); bApply.Click += new EventHandler(bApply_Click); d.BeginEdit(); bs.DataSource = d; t.DataBindings.Add("Text", bs, "Name"); t2.DataBindings.Add("Text", bs, "Color"); } void bCancel_Click(object sender, EventArgs e) { t.DataBindings.Clear(); t2.DataBindings.Clear(); bs.DataSource = null; d.CancelEdit(); d.BeginEdit(); bs.DataSource = d; t.DataBindings.Add("Text", bs, "Name"); t2.DataBindings.Add("Text", bs, "Color"); } void bApply_Click(object sender, EventArgs e) { t.DataBindings.Clear(); t2.DataBindings.Clear(); bs.DataSource = null; d.ApplyEdit(); d.BeginEdit(); bs.DataSource = d; t.DataBindings.Add("Text", bs, "Name"); t2.DataBindings.Add("Text", bs, "Color"); } } [Serializable()] public class Dog: BusinessBase { private static PropertyInfo _nameProp = RegisterProperty(typeof(Dog), new PropertyInfo("Name")); public string Name { get { return GetProperty(_nameProp); } set { SetProperty(_nameProp, value); } } private string _color; public string Color { get { return _color; } set { _color = value; } } public Dog() { LoadProperty(_nameProp, "Lucky"); _color = "Red"; } } }
You know, I've never tried using managed properties with the non-generic version of BusinessBase - maybe that's the problem.
You have Dog : BusinessBase rather than Dog : BusinessBase<Dog> - is that accurate?
The answer to your question about undo of managed fields is that the managed fields are stored in a FieldDataManager object, and it implements IUndoableObject, and so it takes a snapshot of the values and restores them within that object. So your business object should have a field (declared by BusinessBase) called _fieldManager (if I remember right), and UndoableBase should hit that field and cascade the call to that FieldDataManager object.
When I pasted the code in above, somehow the <Dog> Generic param was removed (it exists in my code). I do see the _fieldManager in the non-generic Csla.Core.BusinessBase which the generic BusinessBase inherits from. Why my compiled version only sees the field _name is beyond me. I have traced to the fields again and manually execute "FieldInfo f = currentType.GetField("_fieldManager");", f is null. I'm going to try this on another machine and see if I get the same results. I would assume that GetFields should return plenty more private fields then just the one i have created (assuming you have created some in the inherited classes like the _fieldManager).
_fieldManager is created in a lazy manner, so it could be null.
But if it is null, that indicates that nothing has triggered its creation –
meaning that there’s been no access to managed fields in your object
(none have been set or retrieved).
And yes, it is true that there are other fields that would be
undone – like _isDeleted, _isNew, etc.
Rocky
From: ErikPhilips
[mailto:cslanet@lhotka.net]
Sent: Thursday, April 24, 2008 12:32 AM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] CSLA 3.5 PropertyInfo and Undoable
When I pasted the code in above, somehow the <Dog> Generic
param was removed (it exists in my code). I do see the _fieldManager in
the non-generic Csla.Core.BusinessBase which the generic BusinessBase inherits
from. Why my compiled version only sees the field _name is beyond
me. I have traced to the fields again and manually execute
"FieldInfo f = currentType.GetField("_fieldManager");", f
is null. I'm going to try this on another machine and see if I get the same
results. I would assume that GetFields should return plenty more private
fields then just the one i have created (assuming you have created some in the
inherited classes like the _fieldManager).
Alright I just spent lots of time tracing the ProjectTracker program. Here is the run down I traced.
Form loads
Dataloaded
BeginEdit called on _project [EditLevel = 1]
TextBox modified.
Cancel button clicked. [EditLevel=2 (either on text change or tab, i didn't look doesn matter)]
Call to UnbingBindingSource [EditLevel = 2, _bindingEdit=true]
Set BindingSource.Datasource = null
Calls CancelEdit on the "Current" of the BindingSource
Calls CancelEdit on the current ProjectResource object as an IEditableObject
Calls CancelEdit on the current ProjectResource object as a BusinessBase
Calls UndoChanges
At this point many fields are "undone" except fieldManager(s) because of bindingEdit [EditLevel=1, _bindingEdit=true]
UndoChangesComplete [EditLevel=1, _bindingEdit=false]
Call to CancelEdit on _project (becomes CancelEdit on _project as BusinessBase) [EditLevel=1, _bindingEdit=false]
At this point many fields are "undone" including fieldManager(s) [EditLevel=0, _bindingEdit=false]
So my question is why does project tracker do double duty on undo's per single edit? Secondly, why can't a fieldManager be "undone" while _bindingEdit = true?
Time to go read the help file...
It is possible there’s a bug lurking in CSLA.
However, before we got there, let’s make sure this isn’t
a data binding misunderstanding (because that is quite likely).
PTWin is (intentionally) a complex implementation, where I’m
using data binding AND I want a top-level Cancel button on the form.
If you ONLY use data binding (no form-level Cancel button) then
things are simpler.
If you ONLY use a form-level Cancel button (disable IEditableObject
in the object) then things are simpler.
But PTWin has it all turned on – using all the features
together.
1.
BEFORE binding the object to the UI an explicit BeginEdit() is
called.
2.
Then the object is data bound, which elevates the edit level again,
but under data binding’s control
3.
Child objects might have their edit levels elevated even more by
data binding
4.
Data binding will decrement the edit levels too, as the user
moves around the UI/object model
5.
The edit level will never go below 1 though, because that’s
where data binding started
6.
AFTER unbinding the object from the UI, an explicit CancelEdit()
or ApplyEdit() is called – and that gets the edit level back to 0
If you want to have a form-level Cancel button you must follow
this approach. Nothing else works.
Well, the only other alternative is to disable IEditableObject
(which you can do) but then the user won’t get normal in-place editing
behaviors that come from data binding, because your objects will be ignoring
all data binding requests in terms of undo.
Rocky
Alright it all sound great. I guess my confusion was that the BeginEdit before binding was completely optional. More I look into it, there is the except "unless you want a form-level Cancel". Makes perfect sense. I'll do that when needed.
However, regardless if you are or are not using a form level, it very much appears that if the object was ever edited by a Binding->BindingSource, the _bindingEdit will be true and you cannot undo either N-Level fieldManager fields, nor simple non-form level Cancel because of _bindingEdit. This makes me wonder what your thought process was behind line 144 in UndoableBase.cs.
Ahh, well, that line is there (and this may now be a bug with
FieldDataManager) because in the context of data binding it turns out that you
can’t cascade the call to child objects. That’s not how the DataSet
works, and data binding doesn’t work right if a parent cascades a data
binding BeginEdit() call to its children (even though a manual
BeginEdit() should cascade).
But I suspect that you are correct – FDM isn’t a real
child – it is just a state-holder for the current object, and so the call
should cascade to the FDM.
So that line should probably read:
// this is a child
object, cascade the call
if
(!_bindingEdit || value is FieldManager.FieldDataManager)
((Core.IUndoableObject)value).CopyState(this.EditLevel + 1);
Rocky
I assume similar code for CopyState and AcceptChanges in UndoableBase.cs...
I think you are probably quite right.
Is it possible for you to make these changes and see if they
have any positive (or negative) impact?
Thanks!
Rocky
From: ErikPhilips
[mailto:cslanet@lhotka.net]
Sent: Thursday, April 24, 2008 11:21 PM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] RE: RE: RE: CSLA 3.5 PropertyInfo and Undoable
I assume similar code for CopyState and AcceptChanges in UndoableBase.cs...
My sample application works properly, no negative impact I can see. My real application also is performing normally.
This is really kicking my butt. Yes there is still some type of bug. EndEdit is being called for the object from a bound ListBox. It appears that LoadPropertyValue calls PropertyHasChanged which calls BindableBase.OnPropertyChanged this in turn goes through .Net calls down to EndEdit for the current object. This only occurs for FieldManager values. Here is a stacktrace of a single change to the name of a dog:
> Csla.dll!Csla.Core.BusinessBase.System.ComponentModel.IEditableObject.EndEdit() Line 890 C#
(External Calls)
System.Windows.Forms.dll!System.Windows.Forms.CurrencyManager.EndCurrentEdit() + 0x77 bytes
System.Windows.Forms.dll!System.Windows.Forms.CurrencyManager.ChangeRecordState(int newPosition = 0, bool validating = false, bool endCurrentEdit, bool firePositionChange = true, bool pullData = false) + 0xa0 bytes
System.Windows.Forms.dll!System.Windows.Forms.CurrencyManager.Position.set(int value) + 0x3e bytes
System.Windows.Forms.dll!System.Windows.Forms.ListBox.OnSelectedIndexChanged(System.EventArgs e = {System.EventArgs}) + 0x78 bytes
System.Windows.Forms.dll!System.Windows.Forms.ListBox.NativeRemoveAt(int index) + 0x67 bytes
System.Windows.Forms.dll!System.Windows.Forms.ListBox.ObjectCollection.SetItemInternal(int index, object value = {WindowsFormsApplication3.Dog}) + 0x182 bytes
System.Windows.Forms.dll!System.Windows.Forms.ListBox.SetItemCore(int index, object value) + 0x37 bytes
System.Windows.Forms.dll!System.Windows.Forms.ListControl.DataManager_ItemChanged(object sender, System.Windows.Forms.ItemChangedEventArgs e) + 0x6c bytes
System.Windows.Forms.dll!System.Windows.Forms.CurrencyManager.OnItemChanged(System.Windows.Forms.ItemChangedEventArgs e) + 0x67 bytes
System.Windows.Forms.dll!System.Windows.Forms.CurrencyManager.List_ListChanged(object sender, System.ComponentModel.ListChangedEventArgs e) + 0x3cf bytes
System.Windows.Forms.dll!System.Windows.Forms.BindingSource.OnListChanged(System.ComponentModel.ListChangedEventArgs e) + 0x7b bytes
System.Windows.Forms.dll!System.Windows.Forms.BindingSource.InnerList_ListChanged(object sender, System.ComponentModel.ListChangedEventArgs e) + 0x2e bytes
System.dll!System.ComponentModel.BindingList<System.__Canon>.OnListChanged(System.ComponentModel.ListChangedEventArgs e) + 0x17 bytes
System.dll!System.ComponentModel.BindingList<WindowsFormsApplication3.Dog>.Child_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + 0x1f7 bytes
[External Code]
Csla.dll!Csla.Core.BindableBase.OnPropertyChanged(string propertyName = "Name") Line 102 + 0x29 bytes C#
Csla.dll!Csla.Core.BusinessBase.PropertyHasChanged(string propertyName = "Name") Line 285 + 0x9 bytes C#
Csla.dll!Csla.Core.BusinessBase.LoadPropertyValue<string>(Csla.PropertyInfo<string> propertyInfo = {Csla.PropertyInfo<string>}, string oldValue = "Lucky", string newValue = "Luckya", bool markDirty = true) Line 2313 + 0x29 bytes C#
Csla.dll!Csla.Core.BusinessBase.SetProperty<string>(Csla.PropertyInfo<string> propertyInfo = {Csla.PropertyInfo<string>}, string newValue = "Luckya", Csla.Security.NoAccessBehavior noAccess = ThrowException) Line 2116 + 0x39 bytes C#
Csla.dll!Csla.Core.BusinessBase.SetProperty<string>(Csla.PropertyInfo<string> propertyInfo = {Csla.PropertyInfo<string>}, string newValue = "Luckya") Line 2017 + 0x35 bytes C#
WindowsFormsApplication3.exe!WindowsFormsApplication3.Dog.Name.set(string value = "Luckya") Line 93 + 0x14 bytes C#
[External Code]
WindowsFormsApplication3.exe!WindowsFormsApplication3.Program.Main() Line 18 + 0x1a bytes C#
[External Code]
public partial class Form1: Form { public Form1() { InitializeComponent(); this.Load += new System.EventHandler(this.Form1_Load); } DogList dl = new DogList(); TextBox t = new TextBox(); TextBox t2 = new TextBox(); ListBox l = new ListBox(); Button bCancel = new Button(); Button bApply = new Button(); BindingSource bs = new BindingSource(); public void Form1_Load(object sender, EventArgs e) { for ( int i = 0; i < 5; i++ ) { Dog d = new Dog(); dl.Add(d); } t.Left = 150; t.Top = 50; Controls.Add(t); t2.Left = 270; t2.Top = 50; Controls.Add(t2); l.Left = 10;l.Top = 50;Controls.Add(l);l.Height = 200; bCancel.Text = "Cancel Edit"; bCancel.Left = 150; bCancel.Top = 90; Controls.Add(bCancel); bCancel.Click += new EventHandler(bCancel_Click); bApply.Text = "Apply Edit"; bApply.Left = 230; bApply.Top = 90; Controls.Add(bApply); bApply.Click += new EventHandler(bApply_Click); bind(); } void bCancel_Click(object sender, EventArgs e) { //unbind(); bs.CancelEdit(); //bind(); } void bApply_Click(object sender, EventArgs e) { //unbind(); bs.ApplyEdit(); //bind(); } void unbind() { t.DataBindings.Clear(); t2.DataBindings.Clear(); l.DataBindings.Clear(); bs.DataSource = null; } void bind() { bs.DataSource = dl; Binding tb = t.DataBindings.Add("Text", bs, "Name"); tb.DataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged; Binding tb2 = t2.DataBindings.Add("Text", bs, "Color"); tb2.DataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged; l.DataSource = bs; l.DisplayMember = "Name"; } } [Serializable()] public class DogList: BusinessListBase<DogList, Dog> { } [Serializable()] public class Dog: BusinessBase<Dog> { private static PropertyInfo _nameProp = RegisterProperty(typeof(Dog), new PropertyInfo("Name")); public string Name { get { return GetProperty(_nameProp); } set { SetProperty(_nameProp, value); } } private string _color; public string Color { get { return _color; } set { _color = value; } } public Dog() { LoadProperty(_nameProp, "Lucky"); _color = "Red"; } }
You do realize that unbinding isn’t as simple as you have
it in your code?
Look at the PTWin code or the Windows Forms chapter in the Using
CSLA .NET 3.0 ebook to see how to safely unbind from a bindingsource.
Rocky
I didn't do a good job of presenting my code at 2:45am my time . I was only unbinding controls from the source for testing, which isn't even used in the code I presented. Also notice I'm using the CancelEdit and ApplyEdit on the BindingSource not the BusinessListBase.
But none of that really matters, as EndEdit gets called as soon as a single character is typed in the Dog.Name TextBox (DataSourceUpdateMode.OnPropertyChanged), that single character causes a chain of events that eventually calls EndEdit on the current object. No other code that is incorrect after binding makes any difference if I still can't Undo because EndEdit is called anytime a SetProperty (calling LoadProperty) causes EndEdit. As soon as the ListBox datasource is not bound to DogList, EndEdit is not called. Saving to a Database or whatever with the actual BusinessList or Item doesn't make a difference if the effects before it are in error.
I re-created my entire demo from scratch, with no copying-pasting, and it worked. The only thing I did differently was allowed the IDE to create the user controls. So I went through and found the only difference between my original code I posted here and the new code was the following line:
Listbox1.FormattingEnabled = true;
I certainly have no problem saying I don't completely understand the inner workings of binding, but why does FormattingEnabled prevent or cause EndEdit to be called? To be perfectly clear, this is not a bug in Csla. If I create my own non-Csla base with INotifyPropertyChanged, and IEditableObject, then create multiple instances and store them in a BindingList, EndEdit is called if FormattingEnabled = false on any listbox the BindingList is bound too.Copyright (c) Marimer LLC