EditLevel Mismatch ?

EditLevel Mismatch ?

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


Wal972 posted on Monday, July 16, 2007

Hi

I got this error, can someone please explain/ assist.

scenario: A business object which has no properties of its own, but a different child objects.

ie. an Employee with a child of Personal Details.

DataPortal.Update failed (Csla.Core.UndoException: Edit level mismatch in CopyState
   at Csla.Core.UndoableBase.CopyState(Int32 parentEditLevel)
   at Csla.Core.UndoableBase.CopyState(Int32 parentEditLevel)
   at Csla.Core.BusinessBase.BeginEdit()
   at Csla.Core.BusinessBase.IEditableObject_BeginEdit()
   at System.Windows.Forms.CurrencyManager.OnCurrentChanged(EventArgs e)
   at System.Windows.Forms.CurrencyManager.ChangeRecordState(Int32 newPosition, Boolean validating, Boolean endCurrentEdit, Boolean firePositionChange, Boolean pullData)
   at System.Windows.Forms.CurrencyManager.List_ListChanged(Object sender, ListChangedEventArgs e)
   at System.Windows.Forms.BindingSource.OnListChanged(ListChangedEventArgs e)
   at System.Windows.Forms.BindingSource.InnerList_ListChanged(Object sender, ListChangedEventArgs e)
   at System.ComponentModel.BindingList`1.OnListChanged(ListChangedEventArgs e)
   at System.ComponentModel.BindingList`1.Child_PropertyChanged(Object sender, PropertyChangedEventArgs e)
   at Csla.Core.BindableBase.raise_PropertyChanged(Object sender, PropertyChangedEventArgs e)
   at Csla.Core.BindableBase.OnUnknownPropertyChanged()
   at Csla.Core.BusinessBase.MarkClean()
   at Csla.Core.BusinessBase.MarkOld()
   at HumanResources.Library.Employee.DataPortal_Update() in C:\Source\KISS\HumanResources.Library\Employee\Employee.vb:line 151)

Thanks

Ellie

ajj3085 replied on Monday, July 16, 2007

This is a new feature of the framework designed to help find data binding errors.

Basically you need to ensure that you call EndEdit on your binding sources before calling update.  Check out the newest ProjectTracker sample, ProjectEdit.cs, to see how to handle the situtation properly.

MrPogo replied on Wednesday, July 18, 2007

When I change to CSLA 3.0 and I have a BusinessListBase as a root and bind it to a datagrid and do an edit in the grid on a object in the blb and I get the EditLevel Mismatch once I come of the field or row.  Everything else works.

It seems to be a grandchild not getting an editlevel increase when the parent does.

Thanks in advance.

John

Sean Walsh replied on Thursday, July 19, 2007

Non-Hierarchical Object Graph causes EditLevel problems

I’m surprised more people don’t experience this problem.  One scenario where it happens is when the problem domain is not a strict hierarchy.  We have an application where we would like to have properties of objects that reference other properties of the same object.

In our case, we’re working in criminal justice and we have a case with various offence codes – it’s a collection of child objects (ECOffenceCodes) within the parent (ERCase).  However, in addition, we also want to work with the Primary Offence Code which is one of the objects in the collection.

The trouble is that CSLA expects all Parent-Child relationships to be a hierarchy so upping the EditLevel of a parent recursively ups the EditLevel of all of its children (and their children and so on).  However, what if two of its child objects at whatever point in the hierarchy point to the SAME object instance within the object graph?  Blam!  EditLevel mismatch in CopyState.

I have produced a minimal set of code to illustrate the problem.  Create yourself a Windows application project with a form and a button with the following click event…

using System;
using System.Windows.Forms;

namespace EditLevelProblem
{
    public class frmMain : Form
    {
        public frmMain()
        {
            InitializeComponent();
        }

        private void btnProblem_Click(object sender, EventArgs e)
        {
            // Create a parent object
            //
            ERParent erParent = ERParent.New();

            // Add 2 children to its collection
            //
            for (int i = 0; i < 2; i++)
            {
                erParent.ChildCollection.Add(ECChild.New());
            }

            // Here's the nub of the problem
            // Point to a child via ANOTHER property of the parent
            //
            erParent.PrimaryChild = erParent.ChildCollection[1];

            // Light blue touch paper and retire...
            //
            erParent.BeginEdit();
        }
    }
}
 

BusinessObjects.cs – a minimal set of CSLA Objects.  I’ve put all 3 business objects – ERParent, ECChild and EcChildCollection into one source module to keep things simple.  No data access, no validation, authorisation etc. This is about as bare as I could get it (actually the getter for ERParent.PrimaryChild isn’t used but it keeps ReSharper quiet!) but it illustrates the problem well…

using System;
using Csla;

namespace EditLevelProblem
{
    // ERParent - Parent Class
    //
    [Serializable()]
    public class ERParent : BusinessBase<ERParent>
    {
        private ECChild _primaryChild = ECChild.New();
        private ECChildCollection _childCollection = ECChildCollection.New();

        protected override object GetIdValue()
        {
            return 0;
        }

        public ECChild PrimaryChild
        {
            get { return _primaryChild; }
            set { _primaryChild = value; }
        }

        public ECChildCollection ChildCollection
        {
            get { return _childCollection; }
        }

        public static ERParent New()
        {
            return new ERParent();
        }

        private ERParent()
        {
        }
    }

    // ECChild - Child Class
    //
    [Serializable()]
    public class ECChild : BusinessBase<ECChild>
    {
        protected override object GetIdValue()
        {
            return 0;
        }

        public static ECChild New()
        {
            return new ECChild();
        }

        private ECChild()
        {
            MarkAsChild();
        }
    }

    // ECChildCollection - Collection of Child Objects
    //
    [Serializable()]
    public class ECChildCollection : BusinessListBase<ECChildCollection, ECChild>
    {
        public static ECChildCollection New()
        {
            return new ECChildCollection();
        }

        private ECChildCollection()
        {
            MarkAsChild();
        }
    }
}

Maybe what is required is for CopyState to maintain a hashtable (or some such) of objects visited during the recursive CopyState and only perform CopyState ONCE per object instance.?

Is it too radical to suggest that everywhere an if statement of the form:

      if (this.EditLevel + 1 > parentEditLevel)
        throw new UndoException(string.Format(Resources.EditLevelMismatchException, "CopyState"));

 appears – i.e.:

UndoableBase.CopyState, UndoChanges, AcceptChanges

BusinessListBase.CopyState, UndoChanges, AcceptChanges

it should simply read:

      if (this.EditLevel + 1 > parentEditLevel)
          return;

In other words, no child’s EditLevel is ever allowed to get out of step with its parent’s?

I think there is a sort of precedent for this sort of behaviour in the comment in UndoableBase.UndoChanges that reads:

      // if we are a child object we might be asked to
      // undo below the level of stacked states,
      // so just do nothing in that case
      if (EditLevel > 0)
         {

Or is there some really nasty DataBinding bug I’m encouraging here?
Does _stateStack.Count help?
O
r are we using CSLA incorrectly?
This is my first post so I hope I’m not wasting anyone’s time here…


MrPogo replied on Thursday, July 19, 2007

This seems to be my problem I have  a blb -bo-child and on the UI its a parent datagrid with a datagrids with the child below it.  When I edit the childern datagrids I have no problems with a save but when I edit the parent grid I get an error.  The objects are Users-user-userrole

Edititing the user is where the edit level mismatch occurs.

Sean Walsh replied on Thursday, July 19, 2007

yeah

I'm re-reading pp 50-54 of Rocky's book to see if i've got the design wrong - i.e. we're being too data-oriented and not behaviour-oriented enough...

RockfordLhotka replied on Thursday, July 19, 2007

Sean Walsh:

Non-Hierarchical Object Graph causes EditLevel problems

I’m surprised more people don’t experience this problem.  One scenario where it happens is when the problem domain is not a strict hierarchy.  We have an application where we would like to have properties of objects that reference other properties of the same object.

In our case, we’re working in criminal justice and we have a case with various offence codes – it’s a collection of child objects (ECOffenceCodes) within the parent (ERCase).  However, in addition, we also want to work with the Primary Offence Code which is one of the objects in the collection.

The trouble is that CSLA expects all Parent-Child relationships to be a hierarchy so upping the EditLevel of a parent recursively ups the EditLevel of all of its children (and their children and so on).  However, what if two of its child objects at whatever point in the hierarchy point to the SAME object instance within the object graph?  Blam!  EditLevel mismatch in CopyState.

If I understand you correctly, what you are describing is a using relationship, not a parent-child relationship. You must code using relationships differently, there's no real way around that.

This is one of the more confusing elements of OO design though: confusion between using and forms of containment.

An Order has LineItems, and each LineItem has a Product.

That seems like an innocent statement that would lead to the belief that Product is somehow a child of LineItem. Very clearly however, Product is its own concept. A LineItem merely uses a Product - and in reality probably doesn't use a Product object at all - just some product data.

For convenience of navigation (if your use case requires it), LineItem might implement a GetProduct() method that returns a Product object. Honestly though, even that is a form of coupling that is often best avoided (though I do such things sometimes, because it makes the object model more intuitive).

The key here, is that LineItem never maintains a reference to a Product. Even if it implements a GetProduct() method, that method looks like this:

Public Function GetProduct()
  Return Product.GetProduct(_productId)
End Function

It is merely a shortcut for something the UI could have done itself - again for purposes of making the object model and object relationships more intuitive and perhaps more explicit.

But Product is not a child of LineItem.

Getting the basic object design wrong, especially on a point like this, will cause all sorts of nasty headaches - including some with data binding and edit levels.

Sean Walsh replied on Thursday, July 19, 2007

Thanks Rocky
I'll consider this.  Even at first reading what you say feels right.  I think there are several instances in our object hierarchy where we're using a parent-child relationship in an inappropriate fashion...

Wal972 replied on Friday, July 20, 2007

Thats fine. But what if you have a parent object which has no direct properties, instead it has a series of children which contain a variety of information. ie like the example at the beginning.

Employee (No Properties)

   Personal Details

   Address Details

   Tax Details

   etc.

All the children are and should be contained within the parent. How does the Editlevel mismatch issue get resolved.

 

Thanks

Ellie

Sean Walsh replied on Friday, July 20, 2007

Are you using Data Binding?  If so, are you using any 3rd party components?

Jon replied on Friday, January 23, 2009

***** CSLA VERSION *****
3.0.2.0


***** PARENT COLLECTION *****
public class EventActions : BusinessListBase-EventActions, EventAction-


***** CHILD COLLECTION ITEM *****
public class EventAction : BusinessBase-EventAction-
{
private string _name;
private EventActionType _eventActionType;

public string Name
{
get { return _name; }
set { _name = value; PropertyHasChanged(); }
}

public EventActionType EventActionType
{
get { return _eventActionType; }
set { _eventActionType = value; }
}
}


***** CHILD OF CHILD COLLECTION ITEM *****
EventActionType : BusinessBase-EventActionType-


***** WPF WINDOW CODE *****
private void EditEventAction()
{
myEventActions.BeginEdit();

AddEditEventActionWindow window = new AddEditEventActionWindow(myEventActions[0]);
bool? windowResult = window.ShowDialog();
if (windowResult.HasValue && windowResult.Value)
{
myEventActions.ApplyEdit();
myEventActions = myEventActions.Save();
}
else
myEventActions.CancelEdit();

myEventActions.ResetBindings();
}



When editing the myEventActions[0] ...
... if I change the Name property of EventAction, the myEventActions.ApplyEdit() works great.
... if I change the EventActionType property (in essence, replacing an old child with a different child) I get the "Edit level mismatch in AcceptChanges" error during myEventActions.ApplyEdit().

I don't think this is a "using vs. parent-child" relationship issue. I DO "maintain a reference" to the EventActionType in EventAction. As a matter of fact, I am replacing the reference.

AFTER replacing the EventActionType in EventAction, the EventAction has an EditLevel of 1 and the EventActionType has an EditLevel of 0 (because its a replacement, not an edit of what was there).


I handled this by applying the [NotUndoable()] to the EventActionType field in the EventAction. Having done so, I will need to directly apply BeginEdit() / ApplyEdit() to the EventActionType when editing IT (not replacing).


SO, after that long winded explanation (I am really sry about that too), my question is, did I handle that right, or am i missing something in the design that would have allowed me to not HAVE to apply [NotUndoable()] to EventActionType field in EventAction?

ajj3085 replied on Friday, January 23, 2009

You may want to consider upgrading to 3.0.5, as there are some bug fixes which may affect you.  Check the change log to be sure.

Jon replied on Monday, January 26, 2009

I've upgraded to 3.5.2 (almost seamlessly, thank you for that).

I’m still getting the same error.
BUT, I think I know what’s wrong.

The Hierarchy is:
EventActions -> EventAction -> EventActionType

The scenario is as follows:
• Fetch EventActions (RootParent) which loads all the EventAction (Parent / Child) which loads for each EventAction an EventActionType (Child).
• Call BeginEdit() on EventActions which sets the EditLevel for EventActions and all child objects to 1.
• Change the EventActionType IN EventAction. The replacement EventActionType has an EditLevel of 0 because it was NOT part of the EventAction when EventActions.BeginEdit() was called.
• Call EventActions.ApplyEdit() which loops through all the child objects eventually getting to the replaced EventActionType and throwing the EditLevel Mismatch error because it has an EditLevel of 0.


The following SEEMS to work.

From within EventAction:

public EventActionType EventActionType
{
set{ _eventActionType = EventActionType. FetchChildEventActionType(value.EventActionTypeID, EditLevel); }
}


From within EventActionType:

internal static EventActionType FetchChildEventActionType(short eventActionTypeID, int parentEditLevel)
{

ChildCriteria criteria = new ChildCriteria(ChildCriteria.Actions.ByEventActionTypeID);
criteria.EventActionTypeID = eventActionTypeID;
criteria.ParentEditLevel = parentEditLevel;

return DataPortal.Fetch(criteria);
}



private void DataPortal_Fetch(ChildCriteria criteria)
{
// load up values

if(criteria.ParentEditLevel > 0)
CopyState(criteria.ParentEditLevel);
}



Is this solution a good idea or am I going to be bit by some consequence I don't see?

ajj3085 replied on Tuesday, January 27, 2009

Just to be clear, is EventActionType a child of the root object EventAction?  I'm not sure, so please don't take my word on this, but I'm not sure that's a supported scenario with ERLB.  Hopefully someone else here can chime in with better knowledge than I have.

Jon replied on Tuesday, January 27, 2009

Yes, EventActionType IS a child of EventAction AND EventAction is a child in the RootCollection EventActions (plural).

If I call BeginEdit() on a BusinessListBase, then call BusinessListBase.AddNew(), the new Child takes on an EditLevel appropriate to the Parent.

However, if I change a child object to a different Child (the Parent is not a collection, but a BusinessBase - like its Child), then the newly referenced Child's EditLevel is zero (because it was NOT the child at the time BeginEdit was called on the Parent).

So, my solution calls CopyState on the newly referenced Child to bring it into synch with it's Parent.

The only place I have access to CopyState is from within the Child object itself ... which is why I am calling CopyState inside the DataPortal_Fetch(ChildCriteria criteria) method, and which is why there is a method DataPortal_Fetch which accepts a ChildCriteria (indicating that it is a child, and thus needs to call CopyState).

Is this scenario supported, but in a different manner?

Ultimately my question is: How DO you change a Child BusinessBase for a Parent BusinessBase after calling BeginEdit() on the Parent BusinessBase?

RockfordLhotka replied on Tuesday, January 27, 2009

This is prior to CSLA 3.5? In CSLA 3.5+ the field manager takes care of
these details. But in 3.0 or older, you are responsible for synchronizing
the edit level of any new child object added into an existing object graph.

To do this, use a double while loop to raise and/or lower the edit level of
the new child to match the parent. In 3.0 you should be able to use the
Csla.Core.IUndoableObject interface to gain access to the CopyState() and
related methods.

Also, you can look at the 3.6 code to see how it does this - but it sounds
like you already have the concept figured out.

Rocky

Jon replied on Wednesday, January 28, 2009

Thanks Rocky for … well … developing the CSLA framework and taking the time to support it.

I have upgraded to 3.6 and am still unclear about a couple things.

I have TWO Parent Child relationships:

FIRST Parent Child relationship (as described earlier):
EventActions -> EventAction -> EventActionType
EventActions = BusinessListBase which is the RootParent collection containing Child EventAction objects
EventAction = BusinessBase which is a Child of RootParent collection AND Parent to EventActionType BusinessBase
EventActionType = BusinessBase which is a Child of EventAction.

SECOND Parent Child relationship:
EventActionTypes -> EventActionType
EventActionTypes = BuisnessListBase which is the RootParent collection containing Child EventAcitonType objects.
EventActionType = BuisnessBase which is the Child of RootParent collection.

CODE:
EventActions myEventActions = EventActions.GetEventActions();
EventActionTypes myEventActionTypes = EventActionTypes.GetEventActionTypes();

myEventActions.BeginEdit();
// CHANGE the EventActionType for one of the EventAction objects to a different EventActionType
myEventActions[0].EventActionType = myEventActionTypes[1];

if(“button ok clicked”)
myEventActions.ApplyEdit();
else
myEventActions.CancelEdit();




The lynch-pin is in the EventAction.EventActionType property.
What should the setter DO?

POSSIBILITY ONE:
set {
_eventActionType = value;
}

In this case, value is originally a Child of one Parent and is being assigned as a child of another Parent. It is unaware of this fact and thus has an EditLevel of its original Parent (that being zero).
So, this fails.

POSSIBILITY TWO:
set {
_eventActionType = EventActionType.FetchChildEventActionType(value.EventActionTypeID, EditLevel);
}

This scenario is described in my previous post. It syncs up the EditLevel by calling CopyState() with the current EventAciton.EditLevel.

This could be tweaked to call (Core.IUndoableObject) _eventActionType).CopyState(EditLevel) after setting _eventActionType on the client side, instead of calling the above on the Server Side in the DataPortal_Fetch method. This would eliminate the need to transfere the copied state across Applicaton Domains.

However, what happens when myEventActions.CancelEdit() is called? The OLD EventActionType DID have an EditLevel of 1 (it was a Grandchild of EventActions when myEventActions.BeginEdit() was called), but the reference to THAT EventActionType has since been replaced.

In order to support this scenario, I would need to keep a list of replaced EventActionType Children in EventAction, override UndoingChanges method in EventAction and ReReplace the reference to _eventActionType to the one corresponding to the rolledback EditLevel.


Am I correct about the above? Is there a better way? Any advice is appreciated.

Thanks Rocky

Jon replied on Friday, January 30, 2009

To All interested parties, the following is what I ended up with.

As stated in my previous post, my dilemma was that I was replacing a Child BusinessBase class with a different Child BusinessBase class from a different Parent AFTER calling BeginEdit on the RootParent of the original Parent.
(see previous posts for details)

I ended up with the following:

Code in EventAction (Parent BusinessBase class):


private Stack-EventActionType- replacedEventActionTypes;

private EventActionType _eventActionType;
public EventActionType EventActionType
{
get
{
return _eventActionType;
}
set
{
if (_eventActionType != null)
{
replacedEventActionTypes.Push(_eventActionType);
}

_eventActionType = EventActionType.GetChildEventActionType(value.EventActionTypeID);
}


protected override void AcceptingChanges()
{
if ((_eventActionType as Csla.Core.IUndoableObject).EditLevel != EditLevel)
(_eventActionType as Csla.Core.IUndoableObject).CopyState(EditLevel, BindingEdit);
base.AcceptingChanges();
}
protected override void UndoingChanges()
{
if ((_eventActionType as Csla.Core.IUndoableObject).EditLevel != EditLevel)
{
EventActionType replacedEventActionType = null;
while(replacedEventActionTypes.Count > 0)
{
replacedEventActionType = replacedEventActionTypes.Pop();

if ((replacedEventActionType as Csla.Core.IUndoableObject).EditLevel == EditLevel)
{
_eventActionType = replacedEventActionType;
break;
}
}
}

base.UndoingChanges();
}




In essence …

I kept a list of replaced Child objects to support UndoingChanges. The Child object was ReReplaced in the overriden UndoingChanges method based on the EditLevel of the Parent.

I called CopyState in the overriden AcceptingChanges method to sync the current Child object’s EditLevel to that of the Parent.


My only concern for the above solution is that a trip across application domains is occuring every time the EventActionType is changed even though I HAVE an EventActionType I am passing in to the setter. This is necessary because the passed in EventActionType has a different Parent. It would be ideal if I could CHANGE the EventActionType’s Parent from EventActionTypes (the original Parent) to EventAction (the new Parent).

RockfordLhotka replied on Friday, July 20, 2007

Wal972:

DataPortal.Update failed (Csla.Core.UndoException: Edit level mismatch in CopyState

Why are you invoking n-level undo in your DataPortal_XYZ methods?

Not that this is a totally invalid scenario, but it is not a common scenario either, and so it is suspicious. It may indicate that you are doing something odd, and it can sometimes be challenging to get odd things working just right.

Wal972 replied on Friday, July 20, 2007

I'm not intentially invoking n-undo. I am using the same code as in the previous 2.x framework. So any assistance would be appreciated. I not sure of how this is happening.

Thanks

Ellie

RockfordLhotka replied on Friday, July 20, 2007

My guess is that you are using a local data portal, and that you are not cloning the object before saving it. This means that the object is still connected to the UI through data binding during the save and there’s probably some event-loopback issues going on.

 

Try saving the object as shown in the current ProjectTracker\PTWin code, where the object is cloned before saving, and see if that helps.

 

Rocky

 

 

From: Wal972 [mailto:cslanet@lhotka.net]
Sent: Friday, July 20, 2007 9:46 PM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] EditLevel Mismatch ?

 

I'm not intentially invoking n-undo. I am using the same code as in the previous 2.x framework. So any assistance would be appreciated. I not sure of how this is happening.

Thanks

Ellie



Wal972 replied on Friday, July 20, 2007

Thanks that did the trick. Why does local data portal need the cloning ?

Ellie

RockfordLhotka replied on Saturday, July 21, 2007

It doesn’t technically need the cloning, but the cloning is good for a couple reasons – check Chapter 9 for some discussion.

 

In short, saving the clone does two big things: eliminates data binding (since the original is bound, not the clone), and provides a way to return to the original object graph in case of an exception during the save.

 

When using a remote data portal the object is cloned across the network, so this is a non-issue. But in the local data portal the object itself is what’s being saved, and that can be a problem.

 

One item on my wish list is to automatically do a clone when using the local data portal. This would clearly be a breaking change, but I’ll probably do it in version 3.5 because it would make this whole issue fade nicely into history.

 

Rocky

 

 

From: Wal972 [mailto:cslanet@lhotka.net]
Sent: Friday, July 20, 2007 11:26 PM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] RE: EditLevel Mismatch ?

 

Thanks that did the trick. Why does local data portal need the cloning ?

Ellie



AzStan replied on Friday, November 02, 2007

Rocky:

I notice that you are not cloning the business object when it is being saved as an element of an EditablRootListBase.   Is there a reason for this?   Such objects will often be bound to the UI, with the Save being initiated by an 'EndEdit' call on a BindingSource.  The following is from EditablRootListBase:

public virtual void SaveItem(int index)

{

bool raisingEvents = this.RaiseListChangedEvents;

this.RaiseListChangedEvents = false;

_activelySaving = true;

T item = this[index];

int editLevel = item.EditLevel;

// commit all changes

for (int tmp = 1; tmp <= editLevel; tmp++)

item.AcceptChanges(editLevel - tmp);

try

{

// do the save

this[index] = (T)item.Save();

}

finally

{

// restore edit level to previous level

for (int tmp = 1; tmp <= editLevel; tmp++)

item.CopyState(tmp);

_activelySaving = false;

this.RaiseListChangedEvents = raisingEvents;

}

this.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemChanged, index));

}

 

Stan

RockfordLhotka replied on Sunday, November 04, 2007

Not cloning the object in ERLB was an "oversight", which has been fixed in current versions of the framework (3.0.2 and 3.5).

I put oversight in quotes, because I knew about the issue, but hadn't come up with an elegant solution. The problem is a bit complex, because you only want to do the cloning if the data portal is running in local mode, not when it is using a remote server.

So in 3.0.2 I added an AutoCloneOnUpdate config setting, so the data portal can actually just take care of this whole issue by itself. In 3.0.2 this defaults to false, and in 3.5 it will default to true.

Now the ERLB does the clone itself (always) if that config setting is false - which makes the local data portal happy, but incurs unneeded overhead with a remote data portal. But if the flag is true, then it allows the data portal to just do the right (most efficient) thing.

See the Using CSLA .NET 3.0 ebook for more details.

Copyright (c) Marimer LLC