locked
Attach modified version of an Entity on client RRS feed

  • Question

  • Is this something that was overlooked or did i just not find the way to do it?

    My need is to be able to work on an entity detached from a context and at a precise point, attach the modified entity to the context and have it show as modified.

    Example scenario:

    Load a list of Customers into a DataGrid. For each row, i have an Edit button that allows me to edit the Customer into a ChildWindow. While i edit the child, i do not want modifications to show in the grid until i hit the Accept button. I was thiniking of using 1 context for the list and 1 context for the ChildWindow. Then i thought i could detach the Customer from context 2 and attach to context 1 and have it show as modified. Part of my goal is to collect changes until a Save All button is hit and then i would SaveChanges() on context 1.

    I'd like to avoid having to copy properties from 1 entity to the other entity if possible.

    Maybe someone has a better way to handle that kind of scenario ?

    Wednesday, August 12, 2009 3:01 PM

Answers

  •  

    Guess i should have posted how to use my cloning code listed previously:

    Neat, I like that top level interface. I am not supporting deep cloning in my code at the moment but I am also working at a bit lower level than your code. Would you be interested at all in getting added to RIA Services Contrib? I created that project to be a clearing house for Very Useful Code like this and I would love to get more contributers to it.

    What I have are a set of extension methods to the entities that mirror the existing (but private) ApplyState and ExtractState methods. The main difference is that my ApplyState takes in two dictionaries, the original state and the modified state, and my ExtractState can either extract the original state or the modified state. So, for example, if you wanted to clone an entity using my API it would look like this:

    Foo CloneFoo(Foo entity)
    {
         Foo newEntity = new Foo();
         newEntity.ApplyState(null, entity.ExtractState(ExtractType.Modified));
         newEntity.Id = Guid.NewGuid();
         Context.Foos.Add(tempEntity);
    }

    Tuesday, August 18, 2009 9:12 AM

All replies

  • The only way to do that is to make a new copy of the Entity.

    Easiest way is to use a helper class or write a clone method which serializes / deserializes a new instance.

    Wednesday, August 12, 2009 3:13 PM
  • An entity is always attached in a unchanged state. All change tracking info is lost.

    Follow some links that may help you:

    http://silverlight.net/forums/t/117380.aspx

    And also:

    RIA does not support what you want to do but you can try my suggestion below:

    http://silverlight.net/forums/t/113064.aspx

     

    Wednesday, August 12, 2009 3:22 PM
  • Or you could remove it from the collection, do your processing on it then re-insert it when you're finished. Bit finicky but another way to do it.

    Wednesday, August 12, 2009 4:09 PM
  • Yes, there is no good way for doing this.

    Wednesday, August 12, 2009 4:38 PM
  • Wouldnt it be nice if there was something like:


    EntityList<T>.MergeEntity(Entity ModifiedEntity)

    This could then find the original entity using the EntityKey, see if there are any changes, put the current as Original and put the ModifiedEntity as current. Didnt Domain Service handle that type of situation before? (i might be confusing things with EF)

    Wednesday, August 12, 2009 5:22 PM
  • One of the problems is that entities have dependencies on each other and moving an entity from one context to another would require

    moving the whole tree.

    Wednesday, August 12, 2009 5:38 PM
  • Yeah, you're right, I was not thinking deep enough at all of what it implied.

    Guess i'll have to work out something by myself !

     Anyone know if the RIA client classes generating process can be modified ?

    Not sure what i would do yet, but i'm leaning towards subclassing something in there and having templates use my subclasses. Even if just to fall from higher :-)

    Thursday, August 13, 2009 8:04 AM
  • Yes, you can plug into the codegen and make modifications but generally speaking there isn't much you can change in codegen that you can't do through partial classes. About the only thing that can only be done through codegen is changing the base type of your entities but even then your new type still needs to inherit the Entity class.

    The code that is causing you problems is in the dlls, it isn't accessible from generated code.

    Thursday, August 13, 2009 10:42 AM
  • After fiddling around a bit more, looking at RIA in reflector and such, i came up with a nice startup for integrating off context editing.

    I have Cloning working nicely with optional inclusion of members:

    1) Cloning non Entity data members (Always)
    2) Entity data members (optional and i'm thinking having a string array for choosing what to sub-clone)
    3) EntityCollections ... in progress, still have to decide how to select what to sub clone, maybe in string array as 'member.member...' style. Each element is cloned and added to the cloned element collection.

    One aspect i havent looked at is Validation. I will also want to clone to another instance of the DataContext.

    Next step will be doing the merge back ! :-)

    Here is an example of using my system with cloning happening on the client:

    Shipment c = ship1.Shipments[0];
    Shipment c1 = Utility.CloneEntity<Shipment>(c);

    I plan to post the full source code later on if i feel i was able to succeed.

    P.S: I hope not to get into troubles for getting my inspiration from code in the RIA source code using Reflector.

    Thursday, August 13, 2009 12:27 PM
  • After fiddling around a bit more, looking at RIA in reflector and such, i came up with a nice startup for integrating off context editing. I have Cloning working nicely with optional inclusion of members

    It would be nice if there was a generic implementation of cloning as well as mapping one object tree to another tree. The latter can be useful server side, when you want to map a server side tree into a RIA tree of entities. I have a feeling these bits must be somewhere already in all these .Net libraries...

    The other part of the story is of course the missing client API to merge entities like discussed before.

    Thursday, August 13, 2009 12:49 PM
  • Well,

    The other part of the story is of course the missing client API to merge entities like discussed before.

    That is the gap i'm trying to fill Big Smile

    Thursday, August 13, 2009 1:03 PM
  • P.S: I hope not to get into troubles for getting my inspiration from code in the RIA source code using Reflector.

    Oh no, now you can't work on the mono project! Stick out tongue

    Seriously, using Reflector is the only way to get anything done with the current state of documentation.

    Thursday, August 13, 2009 1:20 PM
  • Since you are poking around the code with Reflector anyway, I will point out the methods that, IMHO, we need public versions of:

    Entity.ApplyState (the public version should take in an OriginalValues dictionary and a ModifiedValues dictionary)

    Entity.ExtractState (the public version should have an enum variable to choose original or modified)

    Entity.Merge

    I don't think getting public access to the setter of EntityState is a good idea. If on ApplyState we provide a non-null ModifiedValues then the EntityState is Modified.

    Thursday, August 13, 2009 2:19 PM
  • Ok, been fiddling a lot but i believe i have the Clone part working. Feel free to try an break it so it gets stronger Big Smile

    I'll be working on merging back portion. If anyone has suggestions on what to-do, not-do with merge back, i'd be interested to hear them.

    Here is a server side Query: 

    public Shipment GetShipment(int id)
    {
        return this.Context.Shipments.Include("Customer").Include("Location").Include("ShipDetails")
    .Include("Documents").Include("ShipDetails.Service").Include("ShipDetails.Status")
    .Where(sh => sh.IdShipment == id).FirstOrDefault(); }

    my Client side cloning usage: 

    Shipment c = ship1.Shipments[0];
    Shipment c1 = Utility.CloneEntity<Shipment>(c, "Customer", "Location","Documents","ShipDetails","ShipDetails.Service","ShipDetails.Status");



    and finally, my utility class: 
    using System;
    using System.Linq;
    using System.Reflection;
    using System.Windows.Ria.Data;
    
    namespace RiaUtils
    {
    	internal static class Utility
    	{
    		private static PropertyInfo[] GetAllDataMembers(Entity entity)
    		{
    			(BindingFlags bindingAttr = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
    			return entity.GetType().GetProperties(bindingAttr)
    				.Where<PropertyInfo>
    
    				(
    					delegate(PropertyInfo p) { return (((p.GetGetMethod() != null) && ((p.GetSetMethod() != null) || p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(EntityCollection<>)))); }
    				)
    				.OrderBy<PropertyInfo, string>(delegate(PropertyInfo p)
    				{
    					return p.Name;
    				})
    				.ToArray<PropertyInfo>();
    		}
    
    		private static Entity CloneEntity(Entity entity, string path, string[] includes)
    		{
    			Entity newentity = entity.GetType().GetConstructor(Type.EmptyTypes).Invoke(null) as Entity;
    
    			foreach (PropertyInfo info in GetAllDataMembers(entity))
    			{
    				Type type = info.PropertyType;
    				if (typeof(Entity).IsAssignableFrom(type))
    				{
    					if (includes.Contains(path + info.Name))
    					{
    						Entity member = (Entity)info.GetValue(entity, null);
    						if (member != null)
    							info.SetValue(newentity, CloneEntity(member, string.Format("{0}{1}.", path, info.Name), includes), null);
    					}
    				}
    				else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(EntityCollection<>))
    				{
    					if (includes.Contains(path + info.Name))
    					{
    						// First, get a reference to both collections.
    						object srcitems = info.GetValue(entity, null);
    						object dstitems = info.GetValue(newentity, null);
    
    						Type srctype = srcitems.GetType();
    						// get source collection count
    						PropertyInfo p = srctype.GetProperty("Count");
    						int count = (int)p.GetValue(srcitems, null);
    						// Find the Indexing property (Usually named Items, but is customizable)
    						String indexername = ((DefaultMemberAttribute)srctype.GetCustomAttributes(typeof(DefaultMemberAttribute), true)[0]).MemberName;
    						PropertyInfo itmpi = srctype.GetProperty(indexername);
    						// Find the Add Method
    						MethodInfo addmethod = srctype.GetMethod("Add");
    
    						object[] idx = new object[] { 0 };
    						for (int i = 0; i < count; i++)
    						{
    							idx[0] = i;
    							Entity srcitem = itmpi.GetValue(srcitems, idx) as Entity;
    							Entity newitem = CloneEntity(srcitem, string.Format("{0}{1}.", path, info.Name), includes);
    							// We actually add using the Add method
    							addmethod.Invoke(dstitems, new object[] { newitem });
    						}
    					}
    				}
    				else
    					info.SetValue(newentity, info.GetValue(entity, null), null);
    
    			}
    			return newentity;
    		}
    
    		public static T CloneEntity<T>(T entity, params string [] includes) where T : Entity, new()
    		{
    			return (T) CloneEntity(entity, "", includes);
    		}
    	}
    }
    
    Friday, August 14, 2009 9:52 AM
  • Check out this post, apparently the EntityContainer has a merge function already that nobody knew about.

    Monday, August 17, 2009 2:02 PM
  • Don't confuse Backing up a full datacontext and restoring it with what merge is supposed to be.

    AFAIK, i'm working on selective full tree merge of off-context entities that came from a previous context or another context.

    if you know how to read between the lines, they're using reflection, which is what i'm also doing but with selectivity.

    Tuesday, August 18, 2009 7:10 AM
  • Don't confuse Backing up a full datacontext and restoring it with what merge is supposed to be.

    AFAIK, i'm working on selective full tree merge of off-context entities that came from a previous context or another context.

    if you know how to read between the lines, they're using reflection, which is what i'm also doing but with selectivity.

    When I posted the link to this thread the other thread had just talked about EntityList.LoadEntities which basically does the same merge that DomainContext.Load does. I am interested in knowing what you mean by selectivity. I have my own library that I am adding to RIA Services Contrib some time today, now that Mathew has said how they are changing LoadState I feel more comfortable rewriting my own. I don't mind looking at the code through Reflector but I don't like publically releasing code that uses that decompiled code.

    Tuesday, August 18, 2009 7:38 AM
  • Don't confuse Backing up a full datacontext and restoring it with what merge is supposed to be.

    AFAIK, i'm working on selective full tree merge of off-context entities that came from a previous context or another context.

    if you know how to read between the lines, they're using reflection, which is what i'm also doing but with selectivity.

    When I posted the link to this thread the other thread had just talked about EntityList.LoadEntities which basically does the same merge that DomainContext.Load does. I am interested in knowing what you mean by selectivity. I have my own library that I am adding to RIA Services Contrib some time today, now that Mathew has said how they are changing LoadState I feel more comfortable rewriting my own. I don't mind looking at the code through Reflector but I don't like publically releasing code that uses that decompiled code.

    Tuesday, August 18, 2009 7:38 AM
  • Guess i should have posted how to use my cloning code listed previously:

    private void TryTest1(object sender, System.Windows.RoutedEventArgs e)
    {
    	var q = ship1.GetShipmentQuery(1140);
    	var lop = ship1.Load<Shipment>(q, ShipmentLoaded, null);
    }
    
    private void ShipmentLoaded(LoadOperation<;Shipment> lo)
    {
    	if (!lo.HasError)
    	{
    		Shipment c = lo.Entities.FirstOrDefault();
    		if (c != null)
    		{
    			Shipment c1 = Utility.CloneEntity<Shipment>(c, "Customer", "Location", "Documents", "ShipDetails", "ShipDetails.Service", "ShipDetails.Status");
    		}
    	}
    	else
    	{
    		string s = lo.Error.Message;
    	}
    }

     

     

    As you can see from that sample code, i can select which Entites or EntityCollections i want cloned with my main clone. It works a bit like Include() for L2SQL.

     

    Tuesday, August 18, 2009 8:06 AM
  •  

    Guess i should have posted how to use my cloning code listed previously:

    Neat, I like that top level interface. I am not supporting deep cloning in my code at the moment but I am also working at a bit lower level than your code. Would you be interested at all in getting added to RIA Services Contrib? I created that project to be a clearing house for Very Useful Code like this and I would love to get more contributers to it.

    What I have are a set of extension methods to the entities that mirror the existing (but private) ApplyState and ExtractState methods. The main difference is that my ApplyState takes in two dictionaries, the original state and the modified state, and my ExtractState can either extract the original state or the modified state. So, for example, if you wanted to clone an entity using my API it would look like this:

    Foo CloneFoo(Foo entity)
    {
         Foo newEntity = new Foo();
         newEntity.ApplyState(null, entity.ExtractState(ExtractType.Modified));
         newEntity.Id = Guid.NewGuid();
         Context.Foos.Add(tempEntity);
    }

    Tuesday, August 18, 2009 9:12 AM
  • Here is my code:

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.Serialization;
    using System.Windows.Ria.Data;


    namespace RiaContrib.Extensions
    {
        public static class RiaExtensions
        {
            public enum ExtractType
            {
                OriginalState, ModifiedState
            };

            public static IDictionary<string, object> ExtractState(this Entity entity, ExtractType extractType)
            {
                Entity extractEntity;
                if (extractType == ExtractType.OriginalState && entity.HasChanges)
                    extractEntity = entity.GetOriginal();
                else
                    extractEntity = entity;

                Dictionary<string, object> returnDictionary = new Dictionary<string, object>();
                foreach (PropertyInfo currentPropertyInfo in GetDataMembers(extractEntity))
                {
                    object currentObject = currentPropertyInfo.GetValue(extractEntity, null);
                    returnDictionary[currentPropertyInfo.Name] = currentObject;
                }
                return returnDictionary;
            }

            public static void ApplyState(this Entity entity, IDictionary<string,object> originalState, IDictionary<string,object> modifiedState)
            {
                if (entity.EntityState != EntityState.New && originalState == null)
                    throw new InvalidOperationException("Entity must be in New state if no originalState was supplied.");
                PropertyInfo[] dataMembers = GetDataMembers(entity);
                if (originalState != null)
                {
                    ApplyState(entity, originalState, dataMembers);
                    ((IChangeTracking)entity).AcceptChanges();
                }
                if (modifiedState != null)
                {
                    ApplyState(entity, modifiedState, dataMembers);
                }

            }

            private static void ApplyState(Entity entity, IDictionary<string, object> state, PropertyInfo[] dataMembers)
            {
                foreach (PropertyInfo currentPropertyInfo in dataMembers)
                {
                    object currentProperty;
                    if (state.TryGetValue(currentPropertyInfo.Name, out currentProperty))
                    {
                        currentPropertyInfo.SetValue(entity, currentProperty, null);
                    }
                }
            }

            private static PropertyInfo[] GetDataMembers( Entity entity)
            {
                BindingFlags bindingAttr = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
                var qry = from p in entity.GetType().GetProperties(bindingAttr)
                       where p.GetCustomAttributes(typeof(DataMemberAttribute), true).Length > 0
                       && p.GetSetMethod() != null
                       select p;
                return qry.ToArray();

            }
        }
    }

    Tuesday, August 18, 2009 12:12 PM
  • Hey Colin,

    I'm having an issue which i'd like your input on !

    Imagine that i deep clone a customer with orders and orderdetails. I then change the order details a bit by removing detail lines, modifying some and deleting some.

    When its time to merge back, this scenario is pretty simple, get rid of removed lines, append new lines etc. But at the deep clone level i have no access to the DataContext instance unless its provided to me, and even then i would not want to delete items from the context without some kind of approval.

    If i'm working in a different business and i have, say, i have products that i can buy from different vendors, now a few vendors contact me and tell me they dont support this product anymore, so i go and clone the product and its vendors, get rid of the few vendors in the list and submit the changes. I dont want the vendors deleted from the context.

    I was thinking returning a list of discarded entities for the user to take a decision on. What do you think ?

    Friday, August 21, 2009 7:22 AM
  • If i'm working in a different business and i have, say, i have products that i can buy from different vendors, now a few vendors contact me and tell me they dont support this product anymore, so i go and clone the product and its vendors, get rid of the few vendors in the list and submit the changes. I dont want the vendors deleted from the context.

    Would you really implement it that way? Wouldn't you call a domain service operation like "DiscontinueProductFromSuppliers(....)". I guess it's more than a simple entity changeset being composed on the client and submitted to the server...

    Friday, August 21, 2009 7:51 AM
  • This sounds like you are really talking about a many-to-many relationship. In that case, what you are really deleting is the VendorProduct table, not the vendor. In that case, there shouldn't be a problem. That being said, I haven't looked into many to many in RIA Services lately to see what the DomainContext looks like. I will post again later after I look that up.

    Friday, August 21, 2009 8:01 AM
  • Do you really call your operations 1 by 1 ?

    I like to work with things offline, i like to think that i'm working a customer order in its entirety before i submit anything to the server, and at this point i want full Transaction support. Its an "all or retry" situation with me.

    Nah, j/k .. i just try to make it possible for full client-side, off-context handling of changes.

     To Colin,

    Many to Many is not well supported yet, you have to show the mid table in you EDMX by giving it a identity field. At least, that's the only way i found to be able to have that mid table showing in my EDMX. I might be wrong but arent there situations where removing a related item in a collection not going will have to handle both scenarios (i.e: Full remove and don't remove) at the context level ?

    Friday, August 21, 2009 8:15 AM
  • any to Many is not well supported yet, you have to show the mid table in you EDMX by giving it a identity field. At least, that's the only way i found to be able to have that mid table showing in my EDMX. I might be wrong but arent there situations where removing a related item in a collection not going will have to handle both scenarios (i.e: Full remove and don't remove) at the context level ?

    That is how I have my many-to-many relationships setup, but I haven't tried doing it without the linking table in the current CTP to make sure that hasn't changed. I am not sure what you mean by "a related item in a collection not going".

    Friday, August 21, 2009 8:29 AM
  • Geez, i've been starting to think one way and changed the direction without completely fixin the sentence .. lolllll

    Ok, seems obvious that in Many to Many i'm killing the middle item but how does it get completely killed ?

    Given: Order table and OrderDetail table, and Order.OrderDetails collection (1 to many). If i do Order.OrderDetails.Remove(Order.OrderDetails[0]), don't i need to also remove the item from datacontext.OrderDetails collection for the OrderDetail line to be completely deleted from the database ?

    Friday, August 21, 2009 8:59 AM
  • I would only remove it from datacontext.OrderDetails myself. Hmm, let me give you an update on how I am doing things. Maybe that will give you an idea. My current code is posted at www.riaservicesblog.net and I will be moving into RIA Services Contrib as soon as I finish documentation and add the MergeOption parameter back into my ApplyState. I also want a deep copy exporter option but I haven't fully fleshed out a plan.

    I am mirroring how RIA Services itself functions. RIA Services is not actually hierarchial, it is just a set of EntityLists. The associations are really just views attached to each entity that returns all entities in the other EntityList that have matching foreign keys. So, when I do an export I an exporting a seperate List<EntityStateSet> for each type of Entity, and that is going to be true for deep copy as well. Importing is easy, each List<EntityStateSet> gets imported into the appropriate EntityList. If EntityStateSet.IsDelete == true then I delete the object. I don't need to worry about the various EntityCollections, those are automatically updated as the entities themselves are created.

    Friday, August 21, 2009 9:54 AM
  • Hey Colin, 

    I'm starting to think that i am taking the wrong approach to the original problem, and got directed into something similar but different.

    Not to say that it would be useless, but you're getting me to question myself and my intent here and that's good.

    Hmmm . a mix of what i do with what you do could be interesting thought. Let me analyse this.

    My original intent is to help to edit a copy of an entity because i dont want changes to show in other UI parts until i completely accept those changes. I figured i wanted to work with entities Off-Context and it lead me to cloning. Cloning data members back is pretty easy. Then i started thinking about things like Order Items when i edit an Order so it lead me to deep cloning. All of this to work Off-Context but now i realize that this was partially wrong thinking. I'd probably be better off bringing my cloned entities into a second context that i would work from, that would know the current state of each element. So when i take the Deep Clone, i copy it into the other context where it's marked as Original, then as i work out my elements, they start getting dirty or new or deleted. Once ready to save, just bring back datacontext2 in datacontext1 and Save Changes ... or do Save Changes on datacontext 2 and copy new state over (althought i have a feeling this would be wrong, unless pre-processed). Cloning stuff from context to context would be better off using your technique althought mine isnt bad but doesnt account for states. My original thinking was, i make changes off-context and simply set changed properties when merging back and the state of the object will be set by itself. But now its giving me headaches (almost .. lolll).

    Hope what i'm saying makes sense because i start questioning the sanity of my brain.

    Friday, August 21, 2009 10:44 AM
  • All of that pretty much mirrors my thoughts on everything. For saving changes in Datacontext 2 and then moving the changes back into Datacontext 1, I think that should work fine as long as you make sure that the submit has completed on dc2 before you extract the state.

    I just uploaded the zip file to my blog, I guess I forgot to push the upload button yesterday.

    Friday, August 21, 2009 11:45 AM
  • Hi Colin,

    If I am reading this write this is developed for use on the client. Is there code available to clone enties on the server (RIA). I simply want to send a list of ints to the server and do the cloning there, i.e.

     public void cloneorders(list<int> neworderids, order origorder){}

    Many Thanks

    Monday, November 2, 2009 8:00 AM
  • Is there code available to clone enties on the server (RIA). I simply want to send a list of ints to the server and do the cloning there

    Could you explain a bit more about your need ?

    I'm not sure what you would gain by cloning server side, but of course, i don't currently live your situation.

    Monday, November 2, 2009 8:47 AM
  • Server side you are dealing with the entity objects that are defined by your model. You would need to find out how to clone objects using your specific model (i.e. NHibernate, L2S, Entity Framework, etc.)

    I do my cloning client side as I like keeping RIA Services in the loop for the new objects.

    Monday, November 2, 2009 10:54 AM