partial save & resolving original context item state

Developer
Nov 29, 2010 at 8:43 PM
Edited Dec 17, 2010 at 12:39 AM

I have a post on the Ria Services Forum, here, which goes into the background of my question.

Another user suggested using the extract/apply state extensions of this project to serve my purpose and this seems like a great solution...

...However, I'm curious what the full-circle implementation would be?

Meaning, once I create a temp context, copy the state from my original to my temp entity, then save the temp context, how do i then update my existing context to show the original item as not dirty, already saved, and also fire all the appropriate events so my UI can react accordingly?

Thanks,

Joe

Coordinator
Nov 29, 2010 at 11:19 PM
Edited Nov 29, 2010 at 11:20 PM

You then export the entities from the temp context back to the original. If you are using GUID keys then the original entities will automatically get refreshed. If you are using identities you will need to manually detach any entites in "new" state before doing the import.

Deletes are also a manual operarion. You can export deletes but you can't import them.

Developer
Nov 30, 2010 at 12:51 AM

When you say manually detach any 'new' entities, do you mean new to the original context but that I have saved with the temp context, or any new entity in the original context, even if they weren't saved with the new context? (My entities are keyed by a string, unfortunately)

 

recap:

  1. new
    1. newEntity gets added to the masterContext
    2. the user makes changes, Saves
    3. on save -> create tempContext & tempEntity
    4. tempContext.Entities.Add(tempEntity)
    5. tempEntity.ApplyState(newEntity.ExtractState(..))
    6. tempContext.SubmitChanges()
    7. in SubmitChangesComplete() ...
    8. newEntity.EntityState = EntityState.Detached
    9. newEntity.ApplyState(tempEntity.ExtractState(..))
    10. newEntity.EntityState = EntityState.Unchanged
    11. voila?
  2. existing 
    1. entries in masterContext are edited by the user & user saves
    2. on save -> same as above (newEntity is editedEntity)
    3. steps 3-7 as above
    4. skip step 8
    5. steps 9-11 as above


Or are the Manual procedures only required for new items? meaning, if it already exists, do i merely have to:

  1. copy existing entity to tempEntity
  2. tempContext.SubmitChanges()
  3. copy tempEntity back to existing
  4. voila?
Coordinator
Nov 30, 2010 at 2:25 AM

Manually detach the entities that were new on the original context and have just been saved. If the string key was created on the client, then you don't need to worry about it. It is only if the server created the key that you need to do the detach. The problem is that since the key is changed by the server, it can't automatically synch back up with the original context.

On your steps, use the Export/Import methods as they automatically do the state management work.

Dec 13, 2010 at 10:28 AM
Edited Dec 20, 2010 at 9:19 AM

you can Submit single entity changes with this  code:

yourDomainContext.SubmitSingleChanges(yourEntity)

the ShowPop function can be commented, in my case I  shows a childwindow with the exception details, or you can throw here the exception (see my next post)

Developer
Dec 13, 2010 at 3:39 PM
Edited Dec 13, 2010 at 4:08 PM

Paulo

Thanks for your reply - I like your approach, however this code doesn't seem to work: The changeset is never submitted!

Could you elaborate on your design a bit? What is the idea behind the Add method as user state?

Developer
Dec 13, 2010 at 7:13 PM

Colin,

I'm seeing that no matter what I do, the update method is NEVER called on the DomainService.

Assuming EntitySet.Add() automatically switches the EntityState to New, I've tried changing this to Modified pre-Submit. 
But that has no effect: Insert, not update is still called on the DomainService.

Ideas?

Coordinator
Dec 13, 2010 at 7:36 PM

Cloning an update is a three step process.

Step 1: Create a detached Entity with a clone of the entities original state
Step 2: Attach the cloned Entity to the EntitySet
Step 3: Apply the modified state

Step 2 is where you are going wrong, you need to use the Attach, not the add. Attach puts the entity in the EntitySet in Unmodified state instead of New state. Then when you apply the modified values that changes the state to Modified so that an update will occur.

The ApplyState in RIA Services Contrib uses a modified version of the above steps as it assumes that the Entity is already attached

Step 1: Apply original state to the entity (if the entity was in new state, it is still in new state. If the entity was in Unmodified, it is now in Modfied state).
Step 2: Cast the entity to IChangeTracking and call AcceptChanges (whatever the previous status of the entity, it is now Unmodified)
Step 3: Apply the modified state (entity is now in Modified state as long as the two states were not equal.)

The qualifier on step 3 is very important, for updates RIA Services does check the original values against the current values to determine if an update is needed or not. If all of the original values match the current values, then the entity is Unmodified.

Developer
Dec 13, 2010 at 8:27 PM

ah hahhh

here's my finalized implementation that works (and doesn't break databinding in my listview/control either! - somehow before it was breaking it)

	MyContext tempContext = new MyContext();
         MyEntity tempEntity;

         if (this.MyEntity.EntityState == EntityState.Modified)
         {
             tempEntity = new MyEntity();
             tempEntity.ApplyState(this.MyEntity.ExtractState(ExtractType.OriginalState), null);
             tempContext.MyEntities.Attach(tempEntity);
             tempEntity.ApplyState(
                 tempEntity.ExtractState(ExtractType.OriginalState), 
                 this.MyEntity.ExtractState(ExtractType.ModifiedState));
         }
         else
         {
             tempEntity = this.MyEntity.Clone();
             tempContext.MyEntities.Add(tempEntity);
         }

         tempContext.SubmitChanges(
             a =>
             {
                 MyEntity saved = null;
                 if (a.ChangeSet.AddedEntities.Count == 1)
                 {
                     saved = a.ChangeSet.AddedEntities.FirstOrDefault() as MyEntity;
                 }
                 else if (a.ChangeSet.ModifiedEntities.Count == 1)
                 {
                     saved = a.ChangeSet.ModifiedEntities.FirstOrDefault() as MyEntity;
                 }
                 else
                     MessageBox.Show("no entities added or modified?");

                 if (saved != null)
                 {
                     this.MyEntity.ApplyState(
                         saved.ExtractState(ExtractType.OriginalState), 
                         saved.ExtractState(ExtractType.ModifiedState));
                 }
             },
             null);

	
Colin, thanks so much for your help through this. 
Developer
Dec 13, 2010 at 8:28 PM

forgot to mention - that Clone method in there is PauloVilla's

Coordinator
Dec 13, 2010 at 9:13 PM

PauloVilla's clone method is using reflection to create a new entity for no good reason. It is just wasting cycles. Here is a slightly modified version of what you just posted. Also, for a "new" entity you shouldn't be setting both a new and modified state. Because you are using ApplyState anyway, that extra ApplyState in the Modified section is probably not needed. You might find me doing that kind of thing because I am worried that adding a "blank" entity to someone else's EntityContainer could cause problems. In your case, I don't think that will be a problem so you could save some processing time and remove that line.

 

            MyContext tempContext = new MyContext();
            MyEntity tempEntity = new MyEntity();

            if (this.MyEntity.EntityState == EntityState.Modified)
            {
                //This line could be removed
                tempEntity.ApplyState(this.MyEntity.ExtractState(ExtractType.OriginalState), null);
                tempContext.MyEntities.Attach(tempEntity);
                tempEntity.ApplyState(
                    tempEntity.ExtractState(ExtractType.OriginalState),
                    this.MyEntity.ExtractState(ExtractType.ModifiedState));
            }
            else
            {
                tempEntity.ApplyState(this.MyEntity.ExtractState(ExtractType.ModifiedState), null);
                tempContext.MyEntities.Add(tempEntity);
            }

            tempContext.SubmitChanges(
                a =>
                {
                    MyEntity saved = null;
                    if (a.ChangeSet.AddedEntities.Count == 1)
                    {
                        saved = a.ChangeSet.AddedEntities.FirstOrDefault() as MyEntity;
                    }
                    else if (a.ChangeSet.ModifiedEntities.Count == 1)
                    {
                        saved = a.ChangeSet.ModifiedEntities.FirstOrDefault() as MyEntity;
                    }
                    else
                        MessageBox.Show("no entities added or modified?");

                    if (saved != null)
                    {
                        this.MyEntity.ApplyState(
                            saved.ExtractState(ExtractType.OriginalState),
                            saved.ExtractState(ExtractType.ModifiedState));
                    }
                },
                null);

 

Dec 16, 2010 at 7:27 PM
Edited Dec 21, 2010 at 3:26 PM

This code can be used for add a modify an entity in a domain context.

using System;
using System.ServiceModel.DomainServices.Client;
using RiaServicesContrib;
using RiaServicesContrib.Extensions;

namespace MSL.Controls
{
    public class SubmitPartialOperation<T> where T : Entity
    {
        public SubmitOperation InnerSubmitOperation { getprivate set; }
        public SubmitPartialOperation(SubmitOperation so)
        {
            InnerSubmitOperation = so;
            SubmittedEntity = so.UserState as T;
        }
        public T SubmittedEntity { getprivate set; }
    }

    public static partial class SubmitExtensions
    {
        public static void SubmitPartialChanges<T>(this DomainContext dc, T ent) where T : Entitynew()
        {
            dc.SubmitPartialChanges(ent, null);
        }

        public static void SubmitPartialChanges<T>(this DomainContext dc, T ent, Action<SubmitPartialOperation<T>> ended) where T : Entitynew()
        {
            var tdm = (DomainContext)Activator.CreateInstance(dc.GetType());
            var v = tdm.EntityContainer.GetEntitySet<T>();
            tdm.SubmitChanges(w =>
            {
                ent.Sincro(w);
                if (ended != null)
                    ended(new SubmitPartialOperation<T>(w));

            }
            , Agregar(v, ent));
        }
        public static void Sincro<T>(this T ent, SubmitOperation obj) where T : Entity
        {
            if (obj.Error != nullComun.ShowPop(obj.Error);
            var entActual = obj.UserState as T;
            ent.Copiar(entActual);
        }
        public static T Agregar<T>(this EntitySet<T> col, T entActual) where T : Entitynew()
        {
            var entTemp = new T();
            if (entActual == nullreturn null;
            switch (entActual.EntityState)
            {
                case EntityState.Modified:
                    {
                        entTemp.ApplyState(entActual.ExtractState(ExtractType.OriginalState), null);
                        col.Attach(entTemp);
                        entTemp.ApplyState(
                            entTemp.ExtractState(ExtractType.OriginalState),
                            entActual.ExtractState(ExtractType.ModifiedState));
                        break;
                    }
                case EntityState.New:
                    {
                        entTemp.Copiar(entActual);
                        col.Add(entTemp);
                        break;
                    }
                case EntityState.Unmodified:
                    {
                        col.Add(entTemp);
                        entTemp.Copiar(entActual);
                        break;
                    }

                default:
                    throw new NotImplementedException();
            }
            return entTemp;
        }
        public static T Clonar<T>(this T origen) where T : Entitynew()
        {
            var n = new T();
            n.ApplyState(origen.ExtractState(ExtractType.OriginalState),
                origen.ExtractState(ExtractType.ModifiedState));
            return n;
        }
        public static void Copiar<T>(this T ent, T actual) where T : Entity
        {
            ent.ApplyState(
                   actual.ExtractState(ExtractType.OriginalState),
                        actual.ExtractState(ExtractType.ModifiedState));
        }
    }
}
Coordinator
Dec 16, 2010 at 10:51 PM

Nothing wrong with your code paulovila, just the one chunk of code that Joe was using didn't make any sense. It would be interesting if you added the DomainContext type to the generic of your method, that way your code would be usable against any DomainContext.

Developer
Dec 16, 2010 at 11:00 PM
Edited Dec 17, 2010 at 12:40 AM
ColinBlair wrote:

... It would be interesting if you added the DomainContext type to the generic of your method, that way your code would be usable against any DomainContext.

That's actually what I ended up doing, as I will probably need to do some dynamic xap loading down the road.
I also added onError & onComplete actions that get passed around.

(Though I did nix the Add method, instead combining it into the top-level method. I felt as though outside the context of the single changes, it didn't really make sense seeing it on an EntitySet: it's functiona is kinda more like CloneEntityInto or something similar. I just don't foresee needing to do that is all - otherwise I would keep it :D)

Dec 17, 2010 at 6:15 AM
Edited Dec 18, 2010 at 9:29 AM

Ok, in order to avoid using the custom domain type just add these methods: [it can be called like this:  MyDomainContext.SubmitSingleChanges(myEntity)]

(see previous reply)

Coordinator
Dec 17, 2010 at 6:39 AM

If you include the DomainContext in the generic you can skip the activator. Activator.CreateInstance (and reflection in general) does have a performance penalty that should be avoided if possible.

To get the EntitySet you just need to call tdm.EntityContainer.GetEntitySet<T>(), you don't need to use reflection there either.

One of the many reasons that a new version of Contrib has been delayed is that I am trying to work around the performance penalty that all of the reflection is causing, that is making me hyper-sensitive right now to reflection when it isn't needed.

Dec 17, 2010 at 7:09 AM

If I do SubmitChanges with the current domain context, wont it include any other previous changes in that domain context?, that is why I create another DomainContext instance.

The usage of tdm.EntityContainer.GetEntitySet<T>() is very elegant.

 

Coordinator
Dec 17, 2010 at 12:47 PM

I meant if you add the DomainContext type to the generic definition:

public static void SubmitSingleChanges<T, C>(this T ent) where C:DomainContext, new() where T : Entitynew()

var tdm = new C();

That would remove the need for the current DomainContext to be passed in at all. You can have SubmitSingleChanges be at the Entity level, but since an Entity doesn't know what DomainContext it belongs to you need to provide it as part of the generic.

 

Dec 18, 2010 at 9:15 AM
Edited Dec 18, 2010 at 9:34 AM

I thought about doing it like you say, but:
If the domain context is specified in the generic part the 'SubmitSingleChanges' like:

public static void SubmitSingleChanges<T, TC>(this T ent, Action<SubmitOperation> ended)
    where T : Entity, new()
    where TC : DomainContext, new()
{
    var tdm = new TC();
    var v = tdm.EntityContainer.GetEntitySet<T>();
    tdm.SubmitChanges(w =>
    {
        ent.Sincro(w);
        if (ended != null)
            ended(w);
    }
    , Agregar(v, ent));
}
 

the usage wolud become uncomfortable because you may have to specify both, the entity type and the domain context type.
In order to call this method it would be similar to:
   MyEntity.SubmitSingleChanges<MyEntityType,MyDomainContextType>(OnEnded)
it is easier to type:
   MyContext.SubmitSingleCahnges(MyEntity,OnEnded)
 
overusing a language feature like generics, leads to more complex usage.
Finally, consider that settig the extension method at the domain context level is because there
will allways be a context instance around the code that will finally hold the changes.

Developer
Dec 18, 2010 at 2:43 PM

My solution to this was to an EntityType-specific extension method. 

public static class DerivedEntityExtensions

public static void SubmitSingleChanges<T>(this T derivedEntityToSave, Action<T> onComplete, Action<Exception> onError)
       where T: DerivedEntityType, new()
{
       derivedEntityToSave.SubmitSingleChanges<T, MyDomainContext>(onComplete, onError);
}  

}

Dec 18, 2010 at 5:23 PM
Edited Dec 20, 2010 at 9:23 AM

In your case you don't need  to constraint to the derived entity type, but to Entity.

       where T: Entity, new()

In this way you can use it with all of your domain entities

If you see the code in my previous post, the use of Action<SubmitSingleOperation> is because it contains more information: Added, modified and deleted entities and a general error, if it were, all in one method.

Developer
Dec 30, 2010 at 5:57 PM

OK, next concept: object hierarchies.

If my Entity has a child property: List<AnotherEntity>, which need to be persisted also ... I'm going to have to modify all this to use the EntityGraphs, right?

Developer
Dec 30, 2010 at 6:04 PM

new thread: http://riaservicescontrib.codeplex.com/Thread/View.aspx?ThreadId=240034

Developer
Nov 28, 2011 at 3:32 PM

With my recent additions to EntityGraph, doing a partial save has become pretty straight-forward. Checkout this link for a step-by-step guide. Don't forget to also read the disclaimer though.