CSLA 3.5 PropertyInfo and Undoable

CSLA 3.5 PropertyInfo and Undoable

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


ErikPhilips posted on Tuesday, April 22, 2008

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.

RockfordLhotka replied on Wednesday, April 23, 2008

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.

ErikPhilips replied on Wednesday, April 23, 2008

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";
      }
   }
}

RockfordLhotka replied on Wednesday, April 23, 2008

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.

ErikPhilips replied on Thursday, April 24, 2008

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).

RockfordLhotka replied on Thursday, April 24, 2008

_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).



ErikPhilips replied on Thursday, April 24, 2008

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...   

RockfordLhotka replied on Thursday, April 24, 2008

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

ErikPhilips replied on Thursday, April 24, 2008

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.

RockfordLhotka replied on Thursday, April 24, 2008

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

 

ErikPhilips replied on Thursday, April 24, 2008

I assume similar code for CopyState and AcceptChanges in UndoableBase.cs...

RockfordLhotka replied on Saturday, April 26, 2008

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...



ErikPhilips replied on Saturday, April 26, 2008

 

My sample application works properly, no negative impact I can see.  My real application also is performing normally.

ErikPhilips replied on Sunday, April 27, 2008

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";
      }
   }

RockfordLhotka replied on Sunday, April 27, 2008

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

ErikPhilips replied on Sunday, April 27, 2008

I didn't do a good job of presenting my code at 2:45am my time Wink [;)].  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.

ErikPhilips replied on Sunday, April 27, 2008

I also want to metion that the Listbox EndEdit is only a problem if you are not using a global Form-Level Cancel.  Only direct binding Cancel and N-Level undo does this problem exist.

ErikPhilips replied on Monday, April 28, 2008

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