Web Project - Collecting User Actions (Create, Update, Delete) and persisting on "Save" button click

Posts   
 
    
KastroNYC
User
Posts: 96
Joined: 23-Jan-2006
# Posted on: 14-Feb-2007 18:50:33   

I've tried to ask this question once before and i've seen others asking as well but I haven't seen any definitive answer. I'm using latest version of Adapter.

Entity Structure:

Unit | 1 - * | Room Room | 1 - * | RoomAmenitie

Problem: I want to allow the user to create/update a Unit with X number of RoomEntity along with selected RoomAmenitie and keep these RoomEntity with relations in viewstate until the user clicks "Save".

I've seen others say to use UnitOfWork (UnitOfWork2) instead and place this in the viewstate. That sounds great but how can you display the changes to this UnitOfWork? I can't databind to a UnitOfWork so there doesn't seem to be any way of showing the changes to the user without keeping some EntityCollection around. This is fine I guess if you just have one collection but in my case there are many more collections and i've simplified the example for clarity but with a lot of collections, this just doesn't seem feasible.

So again problem is that UnitOfWork is as far as I know the chosen answer for solving the "collect actions then commit" problem however what is the solution for displaying these changes to the UnitOfWork to the user?

Example:

User adds 5 rooms - (UnitOfWork.AddForSave or UnitOfWork.AddCollectionForSave) ??? Display updates and shows 5 rooms ??? - (how do i do this ? ) User edits room 1 and adds 2 RoomAmenitie (?? how do I do this to the UnitOfWork) ??? Display updates and shows 5 rooms one with 2 RoomAmenitie ??? - (how do I do this) User clicks save - (UnitOfWork.Commit)

I know asked this question in another thread and the point which I'm stuck at without using UnitOfWork, when I am using a RoomCollection property (EntityCollection<RoomEntity>), is when I receive an EntityOutOfSync exception. I have created a Unit Test which I hope should clarify the problem:



[Test]
        public void RoomAmenitiesShouldAllowForRemovalTest()
        {
            //Precondition
            //fetch some Location - Location has 1 - n with Unit
            LocationEntity loc = LocationManager.Fetch(213);



            //create a new Room Collection - this mimics the 
            //ViewState held RoomCollection property on a RoomManager control
            EntityCollection<RoomEntity> rooms = new EntityCollection<RoomEntity>();
            
            //simulate user clicks Create Room Buttom - creates a single room with two amenties selected
            RoomEntity room1 = RoomManager.Create((byte) ResidentialRoomTypeEnum.Bedroom);
            room1.Length = 20;
            room1.Height = 20;
            room1.Width = 20;
            room1.RoomAmenitie.Add(RoomAmenitieManager.Create(room1, AmenitieManager.Fetch(12)));
            room1.RoomAmenitie.Add(RoomAmenitieManager.Create(room1, AmenitieManager.Fetch(11)));
            //room is added to the collection
            rooms.Add(room1);

            //simulate user Clicks Save button
            //begin by creating some new unit
            UnitEntity unit = UnitManager.Create();  //set default DateCreated, some other defaults

            //associate all the rooms in the collection with the unit
            foreach (RoomEntity room in rooms)
            {
                room.Unit = unit;
            }

            //associate the unit with its parent
            unit.Location = loc;

            //save the Unit - all newly created related entities are saved successfully
            // up until this point everything works as expected
            UnitManager.Save(unit, true);

            //unitId - used to refetch the entity with all needed prefetches
            int unitId = unit.UnitId;

            //reset the entities i've been using to null for clarity 
            unit = null;
            room1 = null;

            //simulate user selects this unit for Edit
            //refetch the Unit with all needed prefetches - Details Shown Below
            unit = UnitManager.FetchWithPropertyManagementPrefetches(unitId);
            
            // simulate the RoomManager control being bound to the unit that is selected
            rooms = unit.Room;

            //simulate user selects Room for edit
            room1 = rooms[0];
            int RoomId = room1.RoomId;

            //user deletes an amenitie - since the only other way to track this would be with a seperate
            //collection of deleted entities and since this Room's parent unit is real (is in the db)
            //we can delete it from the db now
            RoomAmenitieManager.Delete(room1.RoomAmenitie[0]);

            //refetch the room - with all needed relations - Details Below
            room1 = RoomManager.FetchWithNeededRelations(RoomId);

            //now when the RoomCollection is attempted at being bound
            //the OutOfSyncException is being thrown
            Assert.IsTrue(room1.UnitId > 0);

        }


Fetch with Prefetches for Unit


public static UnitEntity FetchWithPropertyManagementPrefetches(int unitId)
        {
            PrefetchPath2 path = new PrefetchPath2((int)EntityType.UnitEntity);
            path.Add(UnitEntity.PrefetchPathRoom);
            path[0].SubPath.Add(RoomEntity.PrefetchPathAmenitieCollectionViaRoomAmenitie);
            path[0].SubPath.Add(RoomEntity.PrefetchPathPhotoCollectionViaRoomPhoto);
            path[0].SubPath.Add(RoomEntity.PrefetchPathRoomAmenitie);
            path[0].SubPath[2].SubPath.Add(RoomAmenitieEntity.PrefetchPathAmenitie);
            path[0].SubPath.Add(RoomEntity.PrefetchPathRoomPhoto);
            path.Add(UnitEntity.PrefetchPathAmenitieCollectionViaUnitAmenitie);
            path.Add(UnitEntity.PrefetchPathUnitAmenitie);
            path[2].SubPath.Add(UnitAmenitieEntity.PrefetchPathAmenitie);
            path.Add(UnitEntity.PrefetchPathPhotoCollectionViaUnitPhoto);
            path.Add(UnitEntity.PrefetchPathUnitPhoto);

            
            UnitEntity ent = new UnitEntity(unitId);
            using (DataAccessAdapter da = new DataAccessAdapter())
            {
                da.FetchEntity(ent, path);
            }
            return ent;
        }

Fetch with Prefetches for Room


public static RoomEntity FetchWithNeededRelations(int id)
        {
            RoomEntity room = new RoomEntity();

            PrefetchPath2 path = new PrefetchPath2((int)EntityType.RoomEntity);
            path.Add(RoomEntity.PrefetchPathAmenitieCollectionViaRoomAmenitie);
            path.Add(RoomEntity.PrefetchPathPhotoCollectionViaRoomPhoto);
            path.Add(RoomEntity.PrefetchPathRoomAmenitie);
            path.Add(RoomEntity.PrefetchPathRoomPhoto);
            path[2].SubPath.Add(RoomAmenitieEntity.PrefetchPathAmenitie);
            path[3].SubPath.Add(RoomPhotoEntity.PrefetchPathPhoto);
            path.Add(RoomEntity.PrefetchPathUnit);

            using (DataAccessAdapter da = new DataAccessAdapter())
            {
                da.FetchEntity(room, path);
            }

            return room;
        }

-Kas

jmeckley
User
Posts: 403
Joined: 05-Jul-2006
# Posted on: 14-Feb-2007 21:47:00   

A UOW wraps the update/save into a single transaction and reduces the amount of code you need to write for begingin, rolling back and comitting transctions.

Your right you can't bind to a UOW since it can contain multiple collections and graphs. The collections are stored in UOWElement lists.

If you know your UOW only contains a collection Units (with their respective children) then you can extract the collection from the first UOWElement. then list out all the dirty entities and there dirty children and show what informatin will change using current value and dbvalue from the entities field objects.

This makes sense in theory although I haven't implemented anything like this before.

psandler
User
Posts: 540
Joined: 22-Feb-2005
# Posted on: 14-Feb-2007 23:33:10   

I'm not sure I 100% understand your test code, but I think I get the gist.

I would recommend that you keep the full object graph in memory (viewstate or session). In addition to this graph, keep an entity collection to store deleted entities. If an entity gets deleted, remove it from the graph and add it to the deleteCollection (in lieu of calling the delete function directly). If an entity that the user is trying to delete is a new entity (i.e it doesn't exist in the database yet), just remove if from the graph and don't add it to the deleteCollection.

Then when the user saves, add both the graph and the deleteCollection to a UOW object, then commit it.

As jmeckley said, you could also use the UOW object as a single container to hold the data in memory, and then inspect its contents to get your graph and deleteCollection back, but I think that will make your code more complex.

Phil

El Barto
User
Posts: 64
Joined: 09-Nov-2006
# Posted on: 15-Feb-2007 12:29:34   

I'm working on a wizard of 5 steps. Each change is added to a UnitOfWork object. The UnitOfWork object is saved in viewstate. In some steps in the wizard I display a repeater. This repeater also shows the in memory changes that the user made earlier. I think this is what you want to accomplish more or less.

I used a LLBLGenDataSource2 to bind the repeater to. I've set the LiverPersistence property to false and hanled the OnPerformSelect event of the datasource. In the eventhandler I filled the ContainedCollection and after that changed that collection according to what is in the UnitOfWork.


    protected void dsTipNode_PerformSelect(object sender, PerformSelectEventArgs2 e) {
        using (DataAccessAdapter adap = new DataAccessAdapter()) {
            adap.FetchEntityCollection(e.ContainedCollection, e.Filter, 0, e.Sorter, e.PrefetchPath);
        }
        foreach (UnitOfWorkElement2 uow in UnitOfWork.GetEntityElementsToDelete()) {
            if (uow.Entity.GetType() == typeof(TipNodeEntity)) {
                e.ContainedCollection.Remove(uow.Entity);
            }
        }
        foreach (UnitOfWorkElement2 uow in UnitOfWork.GetEntityElementsToInsert()) {
            if (uow.Entity.GetType() == typeof(TipNodeEntity)) {
                e.ContainedCollection.Add(uow.Entity);
            }
        }
        lblNoNodesSelected.Visible = (e.ContainedCollection.Count < 1);
    }


In this example the enities in the collection can only be added or removed.

KastroNYC
User
Posts: 96
Joined: 23-Jan-2006
# Posted on: 15-Feb-2007 19:34:59   

Thanks for the help guys. As for the particular problem I was having it seems my Manager method was a bit flawed so once i resolved that I was able to get the scenario to at least work by checking if the edited item is new (entity.Id > 0) and if so just making the change permanent (to the db) immediately then re-fetching with prefetches.

I like the idea of persisting the UOW and not that there is anything wrong with it, but it seems a bit weird to walk through a UOW's collection and check the type and have a collection of "if" statements, so I think for now I'll just stick with making changes on edit permanent.

Also the way my app works, each input is in a seperate web control (so for instance I have a Room control who has a property SelectedRoomEntity which is responsible for packaging up the web controls values and placing them in a RoomEntity and returning it to the parent web control which would be in this case a RoomManager which contains the collection) so I really was hoping to implement the solution in a similar way to an insert where I can build up a RoomEntity with any relations and then just pass it up the chain.

The code ends up being very sleek and simple for inserting and although I definitely understand why EntityCollection.RemoveAt(x) doesn't instruct a delete on the db I wish there were I flag I could set which would make it work that way on deletion when instructed so I could maintain the same level of code simplicity. Anyone know of way to alter the templates to produce such a property?

jmeckley
User
Posts: 403
Joined: 05-Jul-2006
# Posted on: 15-Feb-2007 19:36:49   

you can also check the entity.IsNew property for T/F to determine if it's an insert or update instead of using the Id field.

Posts: 14
Joined: 16-Mar-2007
# Posted on: 19-Mar-2007 15:13:58   

Elbarto...can you share the code for the 5 steps you have created.

It will help me

I'm working on a wizard of 5 steps. Each change is added to a UnitOfWork object. The UnitOfWork object is saved in viewstate. In some steps in the wizard I display a repeater. This repeater also shows the in memory changes that the user made earlier. I think this is what you want to accomplish more or less.

I used a LLBLGenDataSource2 to bind the repeater to. I've set the LiverPersistence property to false and hanled the OnPerformSelect event of the datasource. In the eventhandler I filled the ContainedCollection and after that changed that collection according to what is in the UnitOfWork.

Code:

protected void dsTipNode_PerformSelect(object sender, PerformSelectEventArgs2 e) {
    using (DataAccessAdapter adap = new DataAccessAdapter()) {
        adap.FetchEntityCollection(e.ContainedCollection, e.Filter, 0, e.Sorter, e.PrefetchPath);
    }
    foreach (UnitOfWorkElement2 uow in UnitOfWork.GetEntityElementsToDelete()) {
        if (uow.Entity.GetType() == typeof(TipNodeEntity)) {
            e.ContainedCollection.Remove(uow.Entity);
        }
    }
    foreach (UnitOfWorkElement2 uow in UnitOfWork.GetEntityElementsToInsert()) {
        if (uow.Entity.GetType() == typeof(TipNodeEntity)) {
            e.ContainedCollection.Add(uow.Entity);
        }
    }
    lblNoNodesSelected.Visible = (e.ContainedCollection.Count < 1);
}

In this example the enities in the collection can only be added or removed.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 20-Mar-2007 12:00:48   

Why would you want to store the UoW in the viewstate of a page if the UoW is used in multiple pages? Isn't it better to store it in the seesion then?

Frans Bouma | Lead developer LLBLGen Pro
Posts: 14
Joined: 16-Mar-2007
# Posted on: 20-Mar-2007 13:34:56   

Pls close this thread as i hv opened a new thread

Otis wrote:

Why would you want to store the UoW in the viewstate of a page if the UoW is used in multiple pages? Isn't it better to store it in the seesion then?