How to Disable N-Level undo on Editable Class

How to Disable N-Level undo on Editable Class

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


handaajay15 posted on Monday, June 30, 2008

Hello,

We would like to disable N-Level undo on Editable Class i.e. for class derived from BusinessBase.

Instead of N-Level undo, Can we make it just 1 level undo ? i.e. undo to Original version that was fetched from database.

If the above is not possible, can we disable the undo ? If the user hits cancel, we can always re-fetch the data from the Database.

Another que: We frequently need to compare original value fetched from the database with current value of a property and based on the comparision, we write our business logic. Where and how to store the original value of the object fetched from the database. The old value is required on client as well on server. We do not want to serialize these old values between client and server to avoid the overhead.

Any help in the regard is highly appreciated....

JoeFallon1 replied on Monday, June 30, 2008

1. There is a setting which allows you to not use N level undo. It is discussed here: http://forums.lhotka.net/forums/thread/16652.aspx

2. "The old value is required on client as well on server. We do not want to serialize these old values between client and server."

You can't really have it both ways!

You have to "carry the old value around" if you later want to compare it to the current value. The alternative is hitting the DB with it or pulling a copy of the originally fetched BO from cache. But that really complicates things. It is much simpler to create mOrigValue and compare it to mValue in the same BO. You would only expose OrigValue as a readOnly property (if at all.)

I recall that it is also possibel to extend the new managed fields concept to include metadata like OrigValue and IsDirty at the field level. Rocky does not support this in CSLA itself - but he allows you to extend CSLA to get this behavior if you want to pay that cost.

Joe

tmg4340 replied on Monday, June 30, 2008

Disabling the undo capabilities depends on how you intend to use your objects.  You can disable the IEditableObject support, which would turn off the automatic undo capabilities of your objects.  Once you've disabled that, the amount of undo is totally up to you.  If you only want 1 level, then you only call "BeginEdit" once.

Having said that, there are some potential caveats.  If this is a web app, then you don't have to do anything - you're not getting automatic undo, and if you don't want any undo behavior (and almost every web site I've seen doesn't), just don't call the Begin/End/ApplyEdit methods.

If you're working with a Windows Forms app - well, that becomse more complicated.  Disabling IEditableObject means that the automatic data-binding you get from the Forms architecture doesn't happen, and you're on your own.  Essentially, you would turn your Windows app into a web app, from a data-binding perspective.  You would be required to call the Begin/End/ApplyEdit methods as appropriate.  But you'll also encounter some potentially strange behaviors in certain areas, mostly related to grids.

If you're in a WPF app - well, I can't help you there, as I'm not into WPF yet, so I don't know how that would be affected.

As for your original/current question, there isn't a built-in way to manage that within a CSLA object, so you're pretty much on your own to manage that.  But I'm a little confused about your comment about not wanting to serialize the original values.  I understand the overhead concerns, but you have to get the data to the client somehow, so it has to be serialized the one way.  And once you have the data, why would you then disregard it and re-query the database on the server side?  The server objects are not generally stateful, so there wouldn't be an instance of your business object sitting on the server holding those original values - the server object is completely reconstituted from the byte stream sent to it by the client.  That's the crux of the "mobile object" concept.  You can't mark your original value fields as NonSerialized, because they then wouldn't make it to your client.  So if you want the original values on the client, you'll be sending them back and forth anyway.

HTH

- Scott

handaajay15 replied on Monday, June 30, 2008

Thanks for comments guys.

Platform: WPF/WCF/LINQ

Product: Our product targets wholesale fashion industry with full EDI support. We may have anyway from 100 to 10,000 lines per production order. So, If we keep the original values with in the BO, we are potentially making 20,000 objects travel instead of 10,000. To make things worse, every production line is a matrix of Sizes, Dimension and Colors as grand children.

For Client side, When we have fetched the object from the database, can we store in the client side for reference? If yes, then what is the best way of storing the retreiving the properties on client side?

For Server side, we can re-fetch from the database, i think....

Regarding undo,

On one form, We have 8 collections/lists, where data is moved by user between one collection/list to another. Size of each list is just about 1000 string variables. They are all binding lists though. So Whe we are moving data between collection in UI, its very slow, and after 6,7 attempts the system goes out of memory.

We disabled the undo behaviour as you have suggested, and the system is responding very fast and out of memory problem has gone away, but we are not sure what exactly will stop working.

Again, Thanks for any help.!

 

tmg4340 replied on Monday, June 30, 2008

handaajay15:
Product: Our product targets wholesale fashion industry with full EDI support. We may have anyway from 100 to 10,000 lines per production order. So, If we keep the original values with in the BO, we are potentially making 20,000 objects travel instead of 10,000. To make things worse, every production line is a matrix of Sizes, Dimension and Colors as grand children.

I'm not sure I follow this.  If you keep the original values of your properties in the BO, why does that double the number of objects you send?  Yes, it will make your BO's bigger, but it shouldn't create more of them to send back and forth.  Sending the original values to the client inside your BO is going to be a lot smaller (and faster) then trying to send two copies...

handaajay15:
For Client side, When we have fetched the object from the database, can we store in the client side for reference? If yes, then what is the best way of storing the retreiving the properties on client side?

Would you be storing them on the client in order to create your "original value" BO's?  Sure, you can do that - but with an object graph of that size, you're going to notice the performance hit.  Also, you can't really keep that large an object graph sitting in memory (see your out-of-memory problems), so you'd probably need to serialize it to disk.  But you'd then have to load it back into memory to do your field comparisons, so that doesn't seem to help too much.

handaajay15:
For Server side, we can re-fetch from the database, i think....

Maybe I'm not understanding you, but I don't know why you would re-fetch from the database on the server side.  If you have to have your original database values on the client, then they will be sent in the serialized byte stream to the client, as well as from the client to the server.  There is no need to re-fetch from the database, and doing so will not make your objects smaller as they travel over the wire.

Besides, what's to guarantee that the data you re-fetch from the database is the same as when you initially created your BO's?

handaajay15:
On one form, We have 8 collections/lists, where data is moved by user between one collection/list to another. Size of each list is just about 1000 string variables. They are all binding lists though. So Whe we are moving data between collection in UI, its very slow, and after 6,7 attempts the system goes out of memory.

We disabled the undo behaviour as you have suggested, and the system is responding very fast and out of memory problem has gone away, but we are not sure what exactly will stop working.

I can't say what disabling IEditableObject will do to a WPF app - no experience there.  I have heard that WPF doesn't completely rely on existing data-binding infrastructure, so you may be OK.  I also can't really speak to the performance/memory issue either, at least not without seeing more of how the screen is designed.  8,000 strings is a fair number, and certainly disabling the undo stack will help.  But if you're just moving them from list to list, you shouldn't be gobbling up more and more memory.

HTH

- Scott

RockfordLhotka replied on Tuesday, July 01, 2008

handaajay15:

Platform: WPF/WCF/LINQ

In WPF you don't really need to disable n-level undo, as it isn't used automatically by WPF.

The CslaDataProvider control will automatically use n-level undo if you set ManageObjectLifetime=true - but the default is to not use n-level undo.

In other words, in WPF you shouldn't need to worry about this issue at all - it only gets used if you explicitly use it.

rsbaker0 replied on Monday, June 30, 2008

handaajay15:

Another que: We frequently need to compare original value fetched from the database with current value of a property and based on the comparision, we write our business logic. Where and how to store the original value of the object fetched from the database. The old value is required on client as well on server. We do not want to serialize these old values between client and server to avoid the overhead.

Any help in the regard is highly appreciated....

We implemented a valuecache in our BO's. It's initially empty, but all the property setters will instantiate the cache (if necessary) and save the original value anytime a new value is provided for a property -- it wasn't very hard to implement, but would be tedious without a code generator for your objects.

It really helps to have though. We use it for database concurrency (e.g. detecting collisions), and also being able to tell if a property is "really" dirty (e.g. change a property, then change it back to original value. It's not actually changed any more).

handaajay15 replied on Monday, June 30, 2008

rsbaker0:

We implemented a valuecache in our BO's. It's initially empty, but all the property setters will instantiate the cache (if necessary) and save the original value anytime a new value is provided for a property -- it wasn't very hard to implement, but would be tedious without a code generator for your objects.

It really helps to have though. We use it for database concurrency (e.g. detecting collisions), and also being able to tell if a property is "really" dirty (e.g. change a property, then change it back to original value. It's not actually changed any more).

Yes, I think thats what we need. Any code samples would be highly appreciated to understand how to do it .

 

RockfordLhotka replied on Tuesday, July 01, 2008

rsbaker0:

We implemented a valuecache in our BO's. It's initially empty, but all the property setters will instantiate the cache (if necessary) and save the original value anytime a new value is provided for a property -- it wasn't very hard to implement, but would be tedious without a code generator for your objects.

One way to do this in CSLA 3.5 is to subclass FieldData and PropertyInfo. You can then extend FieldData to store original values or other metadata about the field behind each property.

I didn't do this directly in CSLA because the overhead is obviously quite high (doubling the size of the serialized byte stream to/from the server), but the design of the field manager is such that you should be able to add this functionality in a pretty seamless manner by just extending those two classes through inheritance.

JoeFallon1 replied on Wednesday, July 02, 2008

RockfordLhotka:

One way to do this in CSLA 3.5 is to subclass FieldData and PropertyInfo. You can then extend FieldData to store original values or other metadata about the field behind each property.

Rocky,
Assuming someone chooses to subclass and extend FieldData to contain original values they may also want to modify the way that IsDirty is handled. If the original value is 1 and it is changed to 2 then it IsDirty. But if it is later changed back to 1 then they may consider that to be NOT dirty.

I made a quick attempt at subclassing FieldData and I was not very successful. Could someone please explain how to improve this code? Or are framework changes required?

Issues:
1. I cannot Override the Value or IsDirty property. I used Overloads here to remove the compiler warning.

2. This line of code does not even compile. How can I compare the new value to the Original value.
   If MyBase.Value = _origdata Then 'Operator '=' is not defined for types 'T' and 'T'.

3. How do you make sure the original value is only set once? Will the idea shown below work for Integers?

Imports Csla.Core
Imports Csla.Core.FieldManager

Namespace BO

<Serializable()> _
Public MustInherit Class MyFieldData(Of T)
 
Inherits FieldData(Of T)
 
Implements IFieldData(Of T)

  Private _origdata As T = Nothing

  Public Sub New(ByVal name As String)
   
MyBase.New(name)
 
End Sub

  Public Overloads Property Value() As T
   
Get
     
Return MyBase.Value
   
End Get
   
Set(ByVal value As T)
     
MyBase.Value = value 'sets IsDirty = True

     
If _origdata Is Nothing Then 'only set the original value once. Will this work for non-reference values of type T?
       
_origdata = value
     
End If

      If MyBase.Value = _origdata Then 'Operator '=' is not defined for types 'T' and 'T'.
       
MarkClean() 'sets IsDirty to False
     
End If
   
End Set
 
End Property

  Public ReadOnly Property OrigValue() As T
   
Get
     
Return _origdata
   
End Get
 
End Property

  Public Overloads ReadOnly Property IsDirty() As Boolean
   
Get
     
Dim child As ITrackStatus = TryCast(MyBase.Value, ITrackStatus)
     
If child IsNot Nothing Then
       
Return child.IsDirty
     
Else
       
If MyBase.Value = _origdata Then 'Operator '=' is not defined for types 'T' and 'T'.
         
Return False
       
Else
         
Return MyBase.IsDirty
       
End If
     
End If
   
End Get
 
End Property

 End Class

End Namespace

Joe

sergeyb replied on Wednesday, July 02, 2008

On second issue…. 

This should compile and work:

If MyBase.Value.Equals( _origdata) Then

 

On third issue I would use a variable “HasOrigianlValueBeenSet” because Nothing could be a valid original value, technically…

 

 

 

Sergey Barskiy

Senior Consultant

office: 678.405.0687 | mobile: 404.388.1899

cid:_2_0648EA840648E85C001BBCB886257279
Microsoft Worldwide Partner of the Year | Custom Development Solutions, Technical Innovation

 

From: JoeFallon1 [mailto:cslanet@lhotka.net]
Sent: Wednesday, July 02, 2008 10:21 AM
To: Sergey Barskiy
Subject: Re: [CSLA .NET] How to Disable N-Level undo on Editable Class

 

RockfordLhotka:


One way to do this in CSLA 3.5 is to subclass FieldData and PropertyInfo. You can then extend FieldData to store original values or other metadata about the field behind each property.

Rocky,
Assuming someone chooses to subclass and extend FieldData to contain original values they may also want to modify the way that IsDirty is handled. If the original value is 1 and it is changed to 2 then it IsDirty. But if it is later changed back to 1 then they may consider that to be NOT dirty.

I made a quick attempt at subclassing FieldData and I was not very successful. Could someone please explain how to improve this code? Or are framework changes required?

Issues:
1. I cannot Override the Value or IsDirty property. I used Overloads here to remove the compiler warning.

2. This line of code does not even compile. How can I compare the new value to the Original value.
   If MyBase.Value = _origdata Then 'Operator '=' is not defined for types 'T' and 'T'.

3. How do you make sure the original value is only set once? Will the idea shown below work for Integers?

Imports Csla.Core
Imports Csla.Core.FieldManager

Namespace BO

<Serializable()> _
Public MustInherit Class MyFieldData(Of T)
  Inherits FieldData(Of T)
  Implements IFieldData(Of T)

  Private _origdata As T = Nothing

  Public Sub New(ByVal name As String)
    MyBase.New(name)
  End Sub

  Public Overloads Property Value() As T
    Get
      Return
MyBase.Value
    End Get
    Set
(ByVal value As T)
      MyBase.Value = value 'sets IsDirty = True

     
If _origdata Is Nothing Then 'only set the original value once. Will this work for non-reference values of type T?
       
_origdata = value
      End If

      If MyBase.Value = _origdata Then 'Operator '=' is not defined for types 'T' and 'T'.
       
MarkClean() 'sets IsDirty to False
     
End If
    End
Set
  End
Property

  Public ReadOnly Property OrigValue() As T
    Get
      Return
_origdata
    End Get
  End
Property

  Public Overloads ReadOnly Property IsDirty() As Boolean
    Get
      Dim
child As ITrackStatus = TryCast(MyBase.Value, ITrackStatus)
      If child IsNot Nothing Then
        Return
child.IsDirty
      Else
        If
MyBase.Value = _origdata Then 'Operator '=' is not defined for types 'T' and 'T'.
         
Return False
        Else
          Return
MyBase.IsDirty
        End If
      End
If
    End
Get
  End
Property

End Class

End Namespace

Joe



JoeFallon1 replied on Wednesday, July 02, 2008

Sergey,

Thanks for the quick response. Both ideas worked fine.

Private _hasBeenSet As Boolean = False

Public Overloads Property Value() As T
 
Get
   
Return MyBase.Value
 
End Get
 
Set(ByVal value As T)
   
MyBase.Value = value 'sets IsDirty = True
    
   
If _hasBeenSet = False Then 'only set the original value once.
     
_origdata = value
      _hasBeenSet =
True
   
End If

    If MyBase.Value.Equals(_origdata) Then
     
MarkClean() 'sets IsDirty to False
   
End If
 
End Set
End Property

Public Overloads ReadOnly Property IsDirty() As Boolean
 
Get
   
If MyBase.Value.Equals(_origdata) Then
     
Return False
   
Else
     
Return MyBase.IsDirty
   
End If
 
End Get
End Property

So now all we need is some advice on the Override vs. Overload issue.

Joe

sergeyb replied on Wednesday, July 02, 2008

They this for your overloads problem.  Use Shadows instead of Overloads.  The only problem with this approach is that there is no equivalent to Shadows in C#.

 

  Public Shadows ReadOnly Property IsDirty() As Boolean
    Get
      Dim
child As ITrackStatus = TryCast(MyBase.Value, ITrackStatus)
      If child IsNot Nothing Then
        Return
child.IsDirty
      Else
        If
MyBase.Value = _origdata Then 'Operator '=' is not defined for types 'T' and 'T'.
         
Return False
        Else
          Return
MyBase.IsDirty
        End If
      End
If
    End
Get
  End
Property

 End Class

 

 

Sergey Barskiy

Senior Consultant

office: 678.405.0687 | mobile: 404.388.1899

Magenic ®

Microsoft Worldwide Partner of the Year | Custom Development Solutions, Technical Innovation

 

From: JoeFallon1 [mailto:cslanet@lhotka.net]
Sent: Wednesday, July 02, 2008 10:42 AM
To: Sergey Barskiy
Subject: Re: [CSLA .NET] RE: How to Disable N-Level undo on Editable Class

 

Sergey,

Thanks for the quick response. Both ideas worked fine.

Private _hasBeenSet As Boolean = False

Public Overloads Property Value() As T
  Get
    Return
MyBase.Value
  End Get
  Set
(ByVal value As T)
    MyBase.Value = value 'sets IsDirty = True
    
   
If _hasBeenSet = False Then 'only set the original value once.
     
_origdata = value
      _hasBeenSet = True
    End
If

    If MyBase.Value.Equals(_origdata) Then
     
MarkClean() 'sets IsDirty to False
   
End If
  End
Set
End
Property

So now all we need is some adivce on the Override vs. Overload issue.

Joe



RockfordLhotka replied on Wednesday, July 02, 2008

It could be that those properties should be virtual. I think I erred on the side of caution, figuring I could relax the restriction later.

In the short term you can always implement IFieldData directly - don't subclass at all.

JoeFallon1 replied on Wednesday, July 02, 2008

RockfordLhotka:

It could be that those properties should be virtual. I think I erred on the side of caution, figuring I could relax the restriction later.

In the short term you can always implement IFieldData directly - don't subclass at all.

Yup. That was what I was getting at when I asked if framework changes were required. Any plans to make them Overriddable in 3.5.1?

Joe

 

RockfordLhotka replied on Wednesday, July 02, 2008

I am willing to consider making them overridable.

 

You’ve done more research into this than I have (I just considered it in the design and never prototyped it). Do you think it is sufficient to make those two overridable?

 

Rocky

JoeFallon1 replied on Wednesday, July 02, 2008

Yes. I think that making those 2 Overridable would make it easier/possible to implement the inheritance strategy you recommended. Re-creating the entire class without inheritance would work but a developer should not have to do that.

Joe

 

RockfordLhotka replied on Thursday, July 03, 2008

Yes, changed in svn in 3.5.1.

My only real worry here, is that a subclass will need to detect that it contains a child object and handle that correctly - so it still isn't necessarily trivial to write a subclass.

But then again, your PropertyInfo subclass could make that determination and return a standard FieldData rather than a custom FieldData for child objects.

rsbaker0 replied on Tuesday, July 01, 2008

Here is an excerpt from our value cache implementation. We have two different kinds of objects, one of which tracks changed values by backing field name (long story) and the other which uses the property names, so I kept the same interface for both. This code uses the property names.

To make this work correctly, you have to do two things in your property setter:

(1) Make the setter a "no-op" if the value passed in matches the current property value.

(2) Call "OnPropertyChanging(..)" below with the previous value of the property before updating your backing field with the new value.

 

#region Changed Value Support

// Changed Value Support modeled after WOMapperBusinessBase implementation, except it

// uses property names only and ignores member fieldnames

// Similar OnPropertyChanging() interface kept for consistency/switchability

protected virtual void OnPropertyChanging(string propertyName, object oldValue, string memberfieldName)

{

// Enforce authorization rules

CanWriteProperty(propertyName, true);

// When editing existing object, cache original value on first property change

// for dirty field/concurrency support.

// NOTE: Differs from WORMapperBusinessBase implementation because uses property and not field names

if (!IsNew && !String.IsNullOrEmpty(propertyName))

{

CacheValue(propertyName, oldValue);

}

}

protected void CacheValue(string propertyName, object oldValue)

{

if (!ValueCache.Contains(propertyName))

ValueCache.Add(propertyName, oldValue);

}

protected void ClearConcurrencyCache()

{

if (!ReferenceEquals(_valueCache, null))

_valueCache.Clear();

_valueCache = null;

}

protected System.Collections.Specialized.HybridDictionary _valueCache;

protected System.Collections.Specialized.HybridDictionary ValueCache

{

get

{

if (ReferenceEquals(_valueCache, null))

_valueCache = new System.Collections.Specialized.HybridDictionary();

return _valueCache;

}

}

protected T ReconstructOriginal()

{

T copy = this.Clone();

if (!ReferenceEquals(_valueCache, null) && _valueCache.Count > 0)

{

foreach (object key in _valueCache.Keys)

{

Util.ReflectHelper.SetPropertyValue(

copy, key.ToString(), _valueCache[key]);

}

}

return copy;

}

protected void CopyChangedFields(T target)

{

if (!ReferenceEquals(_valueCache, null) && _valueCache.Count > 0)

{

foreach (object key in _valueCache.Keys)

{

Util.ReflectHelper.SetPropertyValue(

target, key.ToString(), _valueCache[key]);

}

}

}

 

protected bool IsDirtyProperty(string fieldMember)

{

object original;

return IsDirtyProperty(fieldMember, out original);

}

protected bool IsDirtyProperty(string fieldMember, out object original)

{

bool bDirty = false;

original = null;

if (_valueCache != null && _valueCache.Contains(fieldMember))

{

object cachedValue = _valueCache[fieldMember];

object currentValue =

 

Util.ReflectHelper.GetPropertyValue(this, fieldMember);

if (!object.Equals(cachedValue, currentValue))

{

original = cachedValue;

bDirty = true;

}

}

return bDirty;

}

#endregion

Copyright (c) Marimer LLC