views:

715

answers:

13

For the sake of argument assume that I have a webform that allows a user to edit order details. User can perform the following functions:

  • Change shipping/payment details (all simple text/dropdowns)
  • Add/Remove/Edit products in the order - this is done with a grid
  • Add/Remove attachments

Products and attachments are stored in separate DB tables with foreign key to the order.

Entity Framework (4.0) is used as ORM.

I want to allow the users to make whatever changes they want to the order and only when they hit 'Save' do I want to commit the changes to the database. This is not a problem with textboxes/checkboxes etc. as I can just rely on ViewState to get the required information. However the grid is presenting a much larger problem for me as I can't figure out a nice and easy way to persist the changes the user made without committing the changes to the database. Storing the Order object tree in Session/ViewState is not really an option I'd like to go with as the objects could get very large.

So the question is - how can I go about preserving the changes the user made until ready to 'Save'.

Quick note - I have searched SO to try to find a solution, however all I found were suggestions to use Session and/or ViewState - both of which I would rather not use due to potential size of my object trees

+1  A: 

Have you considered storing the information in a JavaScript object and then sending that information to your server once the user hits save?

Jeremy Bandini
I suppose a cookie could be used to persist across different pages.
Jeremy Bandini
I have and tried, however, due to the complexity of the objects (this is for actually Purchase Requisition workflow) it's an extremely cumbersome and time consuming way. Eventually I gave up on the idea because of maintenance implications
Marek Karbarz
+1  A: 

If using the Session is not your preferred solution, which is probably wise, the best possible solution would be to create your own temporary database tables (or as others have mentioned, add a temporary flag to your existing database tables) and persist the data there, storing a single identifier in the Session (or in a cookie) for later retrieval.

Jason Berkan
+2  A: 

First, you may want to segregate your specific state management implementation into it's own class so that you don't have to replicate it throughout your systems.

Second, you may want to consider a hybrid approach - use session state (or cache) for a short time to avoid unnecessary trips to a DB or other external store. After some amount of inactivity, write the cached state out to disk or DB. The simplest way to do this, is to serialize your objects to text (using either serialization or a library like proto-buffers). This helps allow you to avoid creating redundant or duplicate data structure to capture the in-progress data relationally. If you don't need to query the content of this data - it's a reasonable approach.

As an aside, in the database world, the problem you describe is called a long running transaction. You essentially want to avoid making changes to the data until you reach a user-defined commit point. There are techniques you can use in the database layer, like hypothetical views and instead-of triggers to encapsulate the behavior that you aren't actually committing the change. The data is in the DB (in the real tables), but is only visible to the user operating on it. This is probably a more complicated implementation than you may be willing to undertake, and requires intrusive changes to your persistence layer and data model - but allows the application to be ignorant of the issue.

LBushkin
using session/cache for this is only reasonable If the amount of data * users doing it at the same time is relatively small. Also note that there is not much value gained if the application is deployed to multiple servers, as the data needs to be put in a shared store.
eglasius
A: 

You should be able to create a temp file and serialize the object to that, then save only the temp file name to the viewstate. Once they successfully save the record back to the database then you could remove the temp file.

Graham Powell
This would not work well for web farms.
Jacob
Mmm, good point, my thinking was single-server centric. Would still work if saving to a virtual directory that's actually on a remote file share, but there's probably a better way.
Graham Powell
A: 

Single server: serialize to the filesystem. This also allows you to let the user resume later. Multiple server: serialize it but store the serialized value in the db.

This is something that's for that specific user, so when you persist it to the db you don't really need all the relational stuff for it.

Alternatively, if the set of data is v. large and the amount of changes is usually small, you can store the history of changes done by the user instead. With this you can also show the change history + support undo.

eglasius
+4  A: 

If you have control over the schema of the database and the other applications that utilize order data, you could add a flag or status column to the orders table that differentiates between temporary and finalized orders. Then, you can simply store your intermediate changes to the database. There are other benefits as well; for example, a user that had a browser crash could return to the application and be able to resume the order process.

I think sticking to the database for storing data is the only reliable way to persist data, even temporary data. Using session state, control state, cookies, temporary files, etc., can introduce a lot of things that can go wrong, especially if your application resides in a web farm.

Jacob
+1 "sticking to the database for storing data is the only reliable way". Session and ViewState are flaky and users don't like losing an order they've spent time preparing. Just store it in the db with a status (that's what a db is for!). Set the order status to committed/saved when the user clicks on save. If a user comes back later after interrupting a session, use your own business logic to decide whether to continue or discard any uncommitted order in the database.
Joe
This gets a little more complicated in my actual situation where an order can me modified across multiple sessions/users (non-concurrent). So in order to use a database I need to make a copy of the order and sync it when the user commits - this is the way I'm leaning towards currently, but it's just not as clean and seamless as I'd hope it would be
Marek Karbarz
Which is why using an event sourcing approach works better as the record / replay sementics are incredibly simple to manage.
Neal
+1  A: 

Use domain events to capture the users actions and then replay those actions over the snapshot of the order model ( effectively the current state of the order before the user started changing it).

Store each change as a series of events e.g. UserChangedShippingAddress, UserAlteredLineItem, UserDeletedLineItem, UserAddedLineItem.

These events can be saved after each postback and only need a link to the related order. Rebuilding the current state of the order is then as simple as replaying the events over the currently stored order objects.

When the user clicks save, you can replay the events and persist the updated order model to the database.

You are using the database - no session or viewstate is required therefore you can significantly reduce page-weight and server memory load at the expense of some page performance ( if you choose to rebuild the model on each postback ).

Maintenance is incredibly simple as due to the ease with which you can implement domain object, automated testing is easily used to ensure the system behaves as you expect it to (while also documenting your intentions for other developers).

Because you are leveraging the database, the solution scales well across multiple web servers.

Using this approach does not require any alterations to your existing domain model, therefore the impact on existing code is minimal. Biggest downside is getting your head around the concept of domain events and how they are used and abused =)

This is effectively the same approach as described by Freddy Rios, with a little more detail about how and some nice keyword for you to search with =)

http://jasondentler.com/blog/2009/11/simple-domain-events/ and http://www.udidahan.com/2009/06/14/domain-events-salvation/ are some good background reading about domain events. You may also want to read up on event sourcing as this is essentially what you would be doing ( snapshot object, record events, replay events, snapshot object again).

Neal
+1  A: 

how about serializing your Domain object (contents of your grid/shopping cart) to JSON and storing it in a hidden variable ? Scottgu has a nice article on how to serialize objects to JSON. Scalable across a server farm and guess it would not add much payload to your page. May be you can write your own JSON serializer to do a "compact serialization" (you would not need product name,product ID, SKU id, etc, may be you can just "serialize" productID and quantity)

ram
A: 

2 approaches - create a complex AJAX application that stores everything on the client and only submits the entire package of changes to the server. I did this once a few years ago with moderate success. The applicaiton is not something I would want to maintain though. You have a hard time syncing your client code with your server code and passing fields that are added/deleted/changed is nightmarish.

2nd approach is to store changes in the data base in a temp table or "pending" mode. Advantage is your code is more maintainable. Disadvantage is you have to have a way to clean up abandonded changes due to session timeout, power failures, other crashes. I would take this approach for any new development. You can have separate tables for "pending" and "committed" changes that opens up a whole new level of features you can add. What if? What changed? etc.

No Refunds No Returns
A: 

I would go for viewstate, regardless of what you've said before. If you only store the stuff you need, like { id: XX, numberOfProducts: 3 }, and ditch every item that is not selected by the user at this point; the viewstate size will hardly be an issue as long as you aren't storing the whole object tree.

When storing attachments, put them in a temporary storing location, and reference the filename in your viewstate. You can have a scheduled task that cleans the temp folder for every file that was last saved over 1 day ago or something.

This is basically the approach we use for storing information when users are adding floorplan information and attachments in our backend.

Jan Jongboom
A: 

Are the end-users internal or external clients? If your clients are internal users, it may be worthwhile to look at an alternate set of technologies. Instead of webforms, consider using a platform like Silverlight and implementing a rich GUI there.

You could then store complex business objects within the applet, provide persistant "in progress" edit tracking across multiple sessions via offline storage and easily integrate with back-end services that providing saving / processing of the finalised order. All whilst maintaining access via the web (albeit closing out most *nix clients).

Alternatives include Adobe Flex or AJAX, depending on resources and needs.

Gareth Saul
While the clients are internal rewrite for Silverlight etc is not an option (at least at the moment) - cost of such rewrite would outweigh any benefits for us
Marek Karbarz
+1  A: 

Have you considered using a User Profile? .Net comes with SqlProfileProvider right out of the box. This would allow you to, for each user, grab their profile and save the temporary data as a variable off in the profile. Unfortunately, I think this does require your "Order" to be serializable, but I believe all of the options except Session thus far would require the same.

The advantage of this is it would persist through crashes, sessions, server down time, etc and it's fairly easy to set up. Here's a site that runs through an example. Once you set it up, you may also find it useful for storing other user information such as preferences, favorites, watched items, etc.

Jeff Wight
A: 

How large do you consider large? If you are talking sessions-state (so it doesn't go back/fore to the actual user, like view-state) then state is often a pretty good option. Everything except the in-process state provider uses serialization, but you can influence how it is serialized. For example, I would tend to create a local model that represents just the state I care about (plus any id/rowversion information) for that operation (rather than the full domain entities, which may have extra overhead).

To reduce the serialization overhead further, I would consider using something like protobuf-net; this can be used as the implementation for ISerializable, allowing very light-weight serialized objects (generally much smaller than BinaryFormatter, XmlSerializer, etc), that are cheap to reconstruct at page requests.

When the page is finally saved, I would update my domain entities from the local model and submit the changes.

For info, to use a protobuf-net attributed object with the state serializers (typically BinaryFormatter), you can use:

// a simple, sessions-state friendly light-weight UI model object
[ProtoContract]
public class MyType {
    [ProtoMember(1)]
    public int Id {get;set;}

    [ProtoMember(2)]
    public string Name {get;set;}

    [ProtoMember(3)]
    public double Value {get;set;}
    // etc

    void ISerializable.GetObjectData(
        SerializationInfo info,StreamingContext context)
    {
        Serializer.Serialize(info, this);
    }

    public MyType() {} // default constructor

    protected MyType(SerializationInfo info, StreamingContext context)
    {
        Serializer.Merge(info, this);
    }
}
Marc Gravell