True Master-Child Example for CSLA 2.0

True Master-Child Example for CSLA 2.0

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


KickTheSky posted on Tuesday, February 12, 2008

Greetings, still working on a prototype to sell this idea to my company for a business object framework.  Here is the problem I am having.  The project tracker sample uses a strange master-child relation where the child technically already exists when created.  For example, when adding a resource to a project, the resource already exists in the system.

Most of the time, however, that child will not exist.  For example, I am doing a customer example with multiple contacts.  The customer record contains multiple contacts that are created as the customer is created.  The problems I am having are the following...

1) If I want to break the contact info into a seperate form, yet keep it a child object, how can I do this effectively?  Specifically, if the user cancels that form, how do you roll back that child?

2) How is addition and deletion of these objects handled?  In the Project Tracker example, there is an assign and unassign that relys on ID's already existing for the child resources.  IDs will not exist for these contacts until a customer is persisted.

Is there a good example out there of this more traditional view of data?  Am I the only one that thinks the Project Tracker app is sorely lacking as a good example of how to implement the framework?

Inquistive_Mind replied on Tuesday, February 12, 2008

// Business Base Class

// Parent Object that contains a collection of Children Objects

[Serializable()]

public class SomeParent : BusinessBase<SomeParent>

{

#region Constructors

private SomeParent()

{ /* require use of factory methods */ }

#endregion

#region Public Properties

#region SomeParentID

private Int32 _someParentID = 0;

[System.ComponentModel.DataObjectField(true, true, false)]

public Int32 SomeParentID

{

[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]

get

{

CanReadProperty("SomeParentID", true);

return _someParentID;

}

}

#endregion

//Child Object Collection

#region ChildList

private ChildList _childList = ChildList.NewChildList();

public ChildList ChildList

{

get { return _childList; }

}

#endregion

#endregion

#region Business Base Requirements

protected override object GetIdValue()

{

return _someParentID;

}

// The default IsValid and IsDirty properties must be enhanced for all objects that

// subclass BusinessBase and contain child objects

public override bool IsValid

{

get

{

return base.IsValid && _childList.IsValid;

}

}

public override bool IsDirty

{

get

{

return base.IsDirty || _childList.IsDirty;

}

}

#endregion

#region Factory Methods

/// Create a new instance of a SomeParent

public static SomeParent NewParent()

{

if (!CanAddObject())

throw new System.Security.SecurityException(String.Format(ExceptionResources.AddEntityNotAllowed, "SomeParent"));

return DataPortal.Create<SomeParent>();

}

/// Get an Instance of a SomeParent by querying on Default

public static SomeParent Get(Int32 someParentID)

{

if (!CanGetObject())

throw new System.Security.SecurityException(String.Format(ExceptionResources.ViewEntityNotAllowed, "SomeParent"));

SomeParent fetchInstance = null;

try

{

fetchInstance = DataPortal.Fetch<SomeParent>(new Criteria(someParentID));

}

catch (System.Exception ex)

{

Logger.Error(ex.Message);

throw;

}

return fetchInstance;

}

/// Deletes a SomeParent based on its Default

public static void Delete(Int32 someParentID)

{

if (!CanDeleteObject())

throw new System.Security.SecurityException(String.Format(ExceptionResources.DeleteEntityNotAllowed, "SomeParent"));

try

{

DataPortal.Delete(new Criteria(someParentID));

}

catch (System.Exception ex)

{

throw;

}

}

/// Save the current instance of a SomeParent

public override SomeParent Save()

{

if (IsDeleted && !CanDeleteObject())

throw new System.Security.SecurityException(String.Format(ExceptionResources.DeleteEntityNotAllowed, "SomeParent"));

else if (IsNew && !CanAddObject())

throw new System.Security.SecurityException(String.Format(ExceptionResources.AddEntityNotAllowed, "SomeParent"));

else if (!CanEditObject())

throw new System.Security.SecurityException(String.Format(ExceptionResources.UpdateEntityNotAllowed, "SomeParent"));

SomeParent saveInstance = null;

try

{

saveInstance = base.Save();

}

catch (System.Exception ex)

{

throw;

}

return saveInstance;

}

#endregion

#region Criteria Classes

[Serializable()]

private class Criteria

{

#region SomeParentID

private Int32 _someParentID = 0;

public Int32 SomeParentID

{

get

{

return _someParentID;

}

}

#endregion

public Criteria(Int32 someParentID)

{

_someParentID = someParentID;

}

}

#endregion Criteria Classes

#region Data Access

[RunLocal()]

protected override void DataPortal_Create()

{

ValidationRules.CheckRules();

}

private void DataPortal_Fetch(Criteria criteria)

{

using (DbConnection conn = Database.DBConnection)

{

conn.Open();

using (DbCommand cmd = conn.CreateCommand())

{

cmd.CommandType = CommandType.StoredProcedure;

cmd.CommandText = "SomeParentGet";

DbParameter param;

#region @p_SomeParentID

param = cmd.CreateParameter();

param.ParameterName = "@p_SomeParentID";

param.DbType = DbType.Int32;

param.Direction = ParameterDirection.Input;

param.Value = Database.DBNullConvert(criteria.SomeParentID);

cmd.Parameters.Add(param);

#endregion

using (NullableDataReader dr = new NullableDataReader(cmd.ExecuteReader()))

{

if (dr.Read())

{

Fetch(dr);

// Load all the child objects

dr.NextResult();

_childList = ChildList.GetChildList(dr);

}

else

{

throw new Csla.DataPortalException("Criteria Did Not Match any Data", this);

}

}

}

}

}

private void Fetch(NullableDataReader dr)

{

_SomeParentID = dr.GetInt32("SomeParentID");

}

[Transactional(TransactionalTypes.TransactionScope)]

protected override void DataPortal_Update()

{

if (base.IsDirty || _childList.IsDirty)

{

using (DbConnection conn = Database.DBConnection)

{

conn.Open();

using (DbCommand cmd = conn.CreateCommand())

{

cmd.CommandText = "SomeParentUpdate";

cmd.CommandType = CommandType.StoredProcedure;

DbParameter param;

#region @p_SomeParentID

param = cmd.CreateParameter();

param.ParameterName = "@p_SomeParentID";

param.DbType = DbType.Int32;

param.Direction = ParameterDirection.Input;

param.Value = Database.DBNullConvert(_someParentID);

cmd.Parameters.Add(param);

#endregion

int rowsAffected = cmd.ExecuteNonQuery();

_childList.Update(this);

}

}

}

}

#endregion

}

 

Now to the collection object

// Business List Base class

[Serializable()]

public class ChildList : BusinessListBase<ChildList, Child>

{

#region Business Methods

// Add methods and expose it to UI for playing with the collection

public void AddChild(int someParentID, string childItem)

{

Child newItem = Child.NewChild(someParentID, childItem);

if (!Contains(newItem))

this.Add(newItem);

}

public void RemoveChild(int someParentID, string childItem)

{

Child newItem = Child.NewChild(someParentID, childItem);

foreach (Child item in this)

{

if (item.ParentID == someParentID && item.ChildItem == chilItem)

{

Remove(item);

break;

}

}

}

#endregion

#region Factory Methods

// The factory methods are declared as internal scope since they are not for use by the

// UI code,

internal static ChildList NewChildList()

{

//Returns a new instance of the collection object

return new ChildList();

}

internal static ChildList GetChildList(NullableDataReader dr)

{

//Returns collection with the child objects based on the data from the database

return new ChildList(dr);

}

#endregion

#region Constructor

private ChildList()

{

MarkAsChild();

}

private ChildList(NullableDataReader dr)

{

MarkAsChild();

Fetch(dr);

}

#endregion

#region Data Access

//Loading Data

private void Fetch(NullableDataReader dr)

{

RaiseListChangedEvents = false;

while (dr.Read())

this.Add(Child.GetChild(dr));

RaiseListChangedEvents = true;

}

//Updating Data

internal void Update(SomeParent someParent)

{

RaiseListChangedEvents = false;

// Delete the child objects that are removed from the collection

foreach (Child item in DeletedList)

item.DeleteSelf(Convert.ToInt32(item.ParentID), item.ChildItem);

DeletedList.Clear();

// Add or Update any current child objects

foreach (Child item in this)

{

if (item.IsNew)

item.Insert();

else

item.Update();

}

RaiseListChangedEvents = true;

}

#endregion

}

 

Now the Child itself

// Business Base Class

// Child Object

[Serializable()]

public class Child: BusinessBase<Child>

{

#region Public Properties

#region ParentID

private Int32? _parentID = null;

public Int32? ParentID

{

get

{

return _parentID;

}

set

{

if (_parentID != value)

{

_parentID = value;

}

}

}

#endregion

#region ChildItem

private String _childItem = null;

public String ChildItem

{

get

{

return _childItem;

}

set

{

if (_childItem != value)

{

_childItem = value;

}

}

}

#endregion

#endregion

#region BusinessBase Requirements

protected override object GetIdValue()

{

return string.Format("{0}:{1}", _parentID, _childItem);

}

#endregion

#region Constructors

// Default private constructor

private Child()

{

MarkAsChild();

}

// New

private Child(int parentID, string childItem)

{

MarkAsChild();

_parentID = parentID;

_childItem = childItem;

}

// Get

private Child(NullableDataReader dr)

{

MarkAsChild();

_parentID = dr.GetNullableInt32("ParentID");

_childItem = dr.GetNullableString("ChildItem");

MarkOld();

}

#endregion

#region Factory Method

internal static Child NewChild(int parentID, string childItem)

{

return new Child(parentID, childItem);

}

internal static ChildGetChild(NullableDataReader dr)

{

return new Child(dr);

}

#endregion

#region Data Access

internal void Insert()

{

using (DbConnection conn = Database.DBConnection)

{

conn.Open();

using (DbCommand cmd = conn.CreateCommand())

{

cmd.CommandText = "ChildInsert";

cmd.CommandType = CommandType.StoredProcedure;

DbParameter param;

#region @p_ParentID

param = cmd.CreateParameter();

param.ParameterName = "@p_ParentID";

param.DbType = DbType.Int32;

param.Direction = ParameterDirection.Input;

param.Value = Database.DBNullConvert(_parentID);

cmd.Parameters.Add(param);

#endregion

#region @p_ChildItem

param = cmd.CreateParameter();

param.ParameterName = "@p_ChildItem";

param.DbType = DbType.AnsiString;

param.Direction = ParameterDirection.Input;

param.Value = Database.DBNullConvert(_childItem);

cmd.Parameters.Add(param);

#endregion

int rowsAffected = cmd.ExecuteNonQuery();

}

}

}

internal void Update()

{

if (base.IsDirty)

{

using (DbConnection conn = Database.DBConnection)

{

conn.Open();

using (DbCommand cmd = conn.CreateCommand())

{

cmd.CommandText = "ChildUpdate";

cmd.CommandType = CommandType.StoredProcedure;

DbParameter param;

#region @p_ParentID

param = cmd.CreateParameter();

param.ParameterName = "@p_ParentID";

param.DbType = DbType.Int32;

param.Direction = ParameterDirection.Input;

param.Value = Database.DBNullConvert(_parentID);

cmd.Parameters.Add(param);

#endregion

#region @p_childItem

param = cmd.CreateParameter();

param.ParameterName = "@p_childItem";

param.DbType = DbType.AnsiString;

param.Direction = ParameterDirection.Input;

param.Value = Database.DBNullConvert(_childItem);

cmd.Parameters.Add(param);

#endregion

int rowsAffected = cmd.ExecuteNonQuery();

}

}

}

}

internal void DeleteSelf(int ParentID, string childItem)

{

Delete(parentID, childItem);

}

internal void Delete(int parentID, string childItem)

{

using (DbConnection conn = Database.DBConnection)

{

conn.Open();

using (DbCommand cmd = conn.CreateCommand())

{

cmd.CommandType = CommandType.StoredProcedure;

cmd.CommandText = "ChildDelete";

DbParameter param;

#region @p_ParentID

param = cmd.CreateParameter();

param.ParameterName = "@p_ParentID";

param.DbType = DbType.Int32;

param.Direction = ParameterDirection.Input;

param.Value = Database.DBNullConvert(parentID);

cmd.Parameters.Add(param);

#endregion

#region @p_ChildItem

param = cmd.CreateParameter();

param.ParameterName = "@p_ChildItem";

param.DbType = DbType.AnsiString;

param.Direction = ParameterDirection.Input;

param.Value = Database.DBNullConvert(assetUsage);

cmd.Parameters.Add(param);

#endregion

int rowsDeleted = cmd.ExecuteNonQuery();

}

}

}

#endregion

}

Now on the UI

SomeParent someParent = SomeParent.Get(SomeID);//Get the Parent and its children

For adding a child to the collection call the public method AddChild of ChildList object which in turn checks to see whether child already exists and then calls the internal constructor.

someParent.ChildList.AddChild(newID,newChildItem)

For removing a child from the collection call the public method RemoveChild of ChildList object

someParent.ChildList.RemoveChild(somechildID,somechildItem)

Update the Parent someParent.

someParent.Save();

This would mean that all the objects are updated in the same transaction scope hence the child list update doesn not require transactional scope and so the Update is called in the SomeParent transaction scope.This would rollback the whole transaction and would prevent inconsistency.

1) The above example still would work if you want to add the children in a separate form.Get the parent and children(in this case someParent but a empty children list) and use the AddChild() to add and save the Parent.

2)The IDs for the child objects would have to be created when the the factory method AddChild() calls the internal constructors.In my case I had a composite key and hence I pass the parentID and another value(childItem) wchihc forms the key for the child object.

HTH,

Note:I modifed my class so there might be some typos :-(.I created these based on the same example in the text book but instead of the Assign factory method I had to add AddChild and RemoveChild methods.One more issue was configuring the MSDTC to allow distributed transactions.

Cheers

jfreeman replied on Friday, February 15, 2008

Would the BOs layout the same way using the new 3.5 edition of the framework?  Thanks

Jonathan

Inquistive_Mind replied on Friday, February 15, 2008

Hi,

     I am not quite sure about it since we have not moved to that yet.

Cheers

zinovate replied on Wednesday, June 04, 2008

From another Forum post http://www.lhotka.net/weblog/CSLANET35CodeReductionWithChildObjects.aspx

 

It looks like you can still code the child objects in CSLA 2.0 or using the new 3.5 Child_XXX Methods that auto mark the object as childern.

Copyright (c) Marimer LLC