From Silverlight to MVC CslaModelBinding child collections

From Silverlight to MVC CslaModelBinding child collections

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


DanielFowler posted on Thursday, October 13, 2011

Hi,

Background:

A while ago we developed a business application for silverlight using the csla framework. Now we wish to create an MVC alternative for people who are unable to use Silverlight for one reason or another.

 

The problem:

Now the problem is that we wish to re-use the  existing business library for the MVC application. We wish to save a parent and all its children as a batch but the cslamodelbinder does not bind the child objects. Is there a way of binding these child collections?

If not, and we have to save each child seperate as though it were a parent what would be the easiest way? (Bearing in mind all the child objects only have Child data portal methods)

 

I've tried looking round the forums and have only managed to find a very small handfull of posts regarding this problem and these were a little old, was wondering if anyone has found a good solution since then?

 

Thanks a bunch!

 

 

RockfordLhotka replied on Thursday, October 13, 2011

The Using CSLA 4: ASP.NET MVC ebook shows how to use the CslaModelBinder with child collections.

Also, in CSLA 4 version 4.2 the CslaModelBinder is enhanced so it will request the child collection (or root collection) from the controller (if it implements IModelCreator) automatically. This doesn't really change the way things work, but it allows for better organization of your code.

DanielFowler replied on Thursday, October 13, 2011

Thanks Rockford but I had a quick look at the mvc book previously and from what I can gather the CslaBinder only maps the values to existing children but doesn't create new children. My object looks like this:

Client [Parent]   >   Contacts [ChildList]   > Contact [Child]

 

I came up with a way around the problem but its rather hackish i think:

 

        [HttpPost]
        public ActionResult SaveClient(Company client, FormCollection collection)
        {
            this.GetChildren("Contacts""Id", collection, () => CompanyContact.NewCourseDelegate())
                .ForEach(c => client.Contacts.Add(c as CompanyContact));
 
            //This line will map all the values from the collection to the client so all child properties and parent properties will be updated
                TryUpdateModel(client, collection);
 
            //Save the client and pass in a condition that will determine whether to update or insert
                client = client.Save(client.Id > 0);         
 
            return Redirect(string.Format("{0}/{1}", Url.Action("EditClient""Client"), client.Id));
        }

        public List<Csla.Core.BusinessBase> GetChildren(string childPropertyName, string uniqueIdentifier, FormCollection collection, Func<Csla.Core.BusinessBase> createNewChild)
        {
            List<Csla.Core.BusinessBase> Result = new List<Csla.Core.BusinessBase>();
 
            //Find all the child objects posted via the form using the uniqueIdentifier
                var children = collection.AllKeys.Where(k => k.StartsWith(childPropertyName) && k.EndsWith(uniqueIdentifier)).ToList();
 
            //Foreach one we want to create a brand new child object
                foreach (var child in children)
                {
                    var NewChild = createNewChild.Invoke();
 
                    //If the child we're iterating over has an a unique identifier greater than 0
                    //it needs to be marked as old so it Updates instead of inserting.
                        int id = 0;
                        int.TryParse(collection[child], out id);
                        if (id > 0)
                            manager.MarkAsOld(NewChild);
                    
                    Result.Add(NewChild);
                }
 
            return Result;
        }

 

RockfordLhotka replied on Thursday, October 13, 2011

I see, that is a good point.

We should enhance CslaModelBinder so if it doesn't find a matching child in the collection, then it adds a new child.

Your technique might work for you, because you know the nature of your Id property behavior. In the general case that's not practical of course.

But CslaModelBinder should be able to handle this pretty easily as an extension of its existing implementation.

Another thing for the wish list :)

DanielFowler replied on Thursday, October 13, 2011

Thanks Rockford, glad to hear I wasn't just missing something silly! :)

RockfordLhotka replied on Thursday, October 13, 2011

Prior to this point there wasn't a standard way for binding to add new items to a collection, so the functionality wasn't necessary. Obviously it is something we now need to support.

DanielFowler replied on Friday, October 14, 2011

Ok so I've modified the CslaModelBinder to create, delete and modify children based on two values i send from my view "IsNew" and "IsDelete". The only remaining issue is that the children are not being marked as dirty as i thought they would be. Here is the code :


 private object BindCslaCollection(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var collection = (IList)bindingContext.Model;
        var collectionItemType = collection.AsQueryable().ElementType;
        
        var form = controllerContext.RequestContext.HttpContext.Request.Form;
        string firstPropertyName = collectionItemType.GetProperties()[0].Name;
        int collectionCount = form.AllKeys.Count(k => k.StartsWith(bindingContext.ModelName) && k.EndsWith(firstPropertyName));
 
        for (int currIdx = 0; currIdx < collectionCount; currIdx++)
        {
            string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currIdx);
            
            if (!bindingContext.ValueProvider.ContainsPrefix(subIndexKey))
                continue;      //no value to update skip
 
            object elementModel = Activator.CreateInstance(collectionItemType);
            ObjectManager objectManager = new ObjectManager();
            objectManager.MarkAsChild(elementModel);
 
            var suppress = elementModel as Csla.Core.ICheckRules;
            if (suppress != null)
                suppress.SuppressRuleChecking();
 
            var elementContext = new ModelBindingContext()
            {
                ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => elementModel, elementModel.GetType()),
                ModelName = subIndexKey,
                ModelState = bindingContext.ModelState,
                PropertyFilter = bindingContext.PropertyFilter,
                ValueProvider = bindingContext.ValueProvider
            };
 
            if (OnModelUpdating(controllerContext, elementContext))
            {
                //update element's properties
                foreach (PropertyDescriptor property in GetFilteredModelProperties(controllerContext, elementContext))
                {
                    BindProperty(controllerContext, elementContext, property);
                }
                OnModelUpdated(controllerContext, elementContext);
            }
 
            collection.Add(elementModel);
 
            var Element = elementModel as Csla.Core.BusinessBase;
            bool IsDelete, IsNew = false;
            bool.TryParse(form[subIndexKey+".IsDeleted"], out IsDelete);
            bool.TryParse(form[subIndexKey + ".IsNew"], out IsNew);
 
            if (IsDelete)
                Element.Parent.RemoveChild(Element);
            if (!IsNew)
                objectManager.MarkAsOld(Element); 

        }

 

 

gyob00 replied on Friday, November 04, 2011

I have the same requirement. We have been asked to make a site using MVC where one requirement is that one page (with some jquery tabs along with other screen partitioning instruments) performs the following

 

Modify/add a new contact along with modify/add:

 

0-infinite relatives

0-infinite employment histories (which each have their own available variable amount of paycodes)

 

etc..(there are more instances like this in our object graph). We cannot break these actions up onto different pages, it would be a deal breaker.

 

An elegant solution for MVC is needed. I will try some of the ideas on this post, for now.

 

Daniel, does your accepted answer solution work? did you have any other challenges you could mention?

gyob00 replied on Thursday, November 10, 2011

the following modification to MVC  CslaModelBiner.cs will allow you to use a programming style where you can add new items to children and remove items, too (all at once, with one post) You must include the IsNew, IsDeleted, objectIDs with your post data (I dont think anything else is required unless in a situation where validating, like doing a child update) and respect Microsoft MVC form element naming convention CSLAObjectName[index] and you must start at index 0 and you cannot skip index ordinals (which is the standard-normal way of doing things with Ms MVC). Most of the code is original, I've added my hacks to add this functionality. I will continue to hone this until it works or give up. Its working for me now, so I don't think I'll be giving up right now.

 

 *update  dec 9 2011* fixed some more bugs, and it seems to work well (again)...doing more testing and code cleanup

 

 

    private class ObjectManager : Csla.Server.ObjectFactory

    {

      public new void MarkAsChild(object obj)

      {

        base.MarkAsChild(obj);

      }

      public new void MarkOld(object obj)

      {

          base.MarkOld(obj);

      }

    }

 

    /// <summary>

    /// Bind CSLA Collection object using specified controller context and binding context

    /// </summary>

    /// <param name="controllerContext">Controller Context</param>

    /// <param name="bindingContext">Binding Context</param>

    /// <returns>Bound CSLA collection object</returns>

    private object BindCslaCollection(ControllerContext controllerContext, ModelBindingContext bindingContext)

    {

 

 

        var collection = (IList)bindingContext.Model;

 

 

 

        var collectionItemType = collection.AsQueryable().ElementType;

 

        var form = controllerContext.RequestContext.HttpContext.Request.Form;

        string firstPropertyName = collectionItemType.GetProperties()[0].Name;

        int collectionCount = form.AllKeys.Count(k => k.StartsWith(bindingContext.ModelName) && k.EndsWith(firstPropertyName));

 

 

 

            for (int currIdx = 0; currIdx < collection.Count; currIdx++)

            {

                string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currIdx);

                if (!bindingContext.ValueProvider.ContainsPrefix(subIndexKey))

                    continue;      //no value to update skip

                var elementModel = collection[currIdx];

                var suppress = elementModel as Csla.Core.ICheckRules;

                if (suppress != null)

                    suppress.SuppressRuleChecking();

                var elementContext = new ModelBindingContext()

                {

                    ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => elementModel, elementModel.GetType()),

                    ModelName = subIndexKey,

                    ModelState = bindingContext.ModelState,

                    PropertyFilter = bindingContext.PropertyFilter,

                    ValueProvider = bindingContext.ValueProvider

                };

 

                if (OnModelUpdating(controllerContext, elementContext))

                {

                    //update element's properties

                    foreach (PropertyDescriptor property in GetFilteredModelProperties(controllerContext, elementContext))

                    {

                        BindProperty(controllerContext, elementContext, property);

                    }

                    OnModelUpdated(controllerContext, elementContext);

                }

 

            }

 

 

            // The next portion of the code adds any new and deletes any deleted items I use an HttpContext item to ensure it only executes on the final pass (this function is executing twice) not sure if I like this :-\

 

            int IsDeleteCount;

            int IsNewFrmCount;

            bool IsDeleteBool;

            bool IsNewBool;

 

 

            IsDeleteCount = 0;

            IsNewFrmCount = 0;

 

            for (int currIdx = 0; currIdx < collectionCount; currIdx++)

            {

                string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currIdx);

 

                bool.TryParse(form[subIndexKey + ".IsDeleted"], out IsDeleteBool);

                bool.TryParse(form[subIndexKey + ".IsNew"], out IsNewBool);

 

 

                if (IsDeleteBool && !IsNewBool)

                    IsDeleteCount++;

 

 

                if (!IsDeleteBool && IsNewBool)

                    IsNewFrmCount++;

 

            }

 

            if (controllerContext.HttpContext.Items.Contains(bindingContext.ModelName.ToString()))

            {

                controllerContext.HttpContext.Items.Remove(bindingContext.ModelName.ToString());

            }

            else

            {

                controllerContext.HttpContext.Items.Add(bindingContext.ModelName.ToString(), "a");

                IsDeleteCount = 0;

                IsNewFrmCount = 0;

            }

 

 

 

 

 

 

        // have to delete before we add and need to delete in reverse so our indexes dont get out of sync

            if (IsDeleteCount > 0)

            {

                int colCount1 = collection.Count;

 

                for (int currIdx = (colCount1-1); currIdx > -1; currIdx--)

                {

                    string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currIdx);

                    if (!bindingContext.ValueProvider.ContainsPrefix(subIndexKey))

                        continue;      //no value to update skip

                    var elementModel = collection[currIdx];

 

 

                    var Element = elementModel as Csla.Core.BusinessBase;

                    bool IsDelete1;

                    bool.TryParse(form[subIndexKey + ".IsDeleted"], out IsDelete1);

 

                    if (IsDelete1 && (IsDeleteCount > 0))

                    {

 

                            Element.Parent.RemoveChild(Element);

 

                    }

 

 

 

 

                }

 

            }

 

 

 

            //Add missing items

            if (IsNewFrmCount > 0)

            {

                // not sure exactly how many times to loop yet (still testing) so I add some extra loops, the continue in the loop will catch my fudge for now

                int colCount1 = collection.Count + IsNewFrmCount + 1;

                for (int currIdx = 0; currIdx < colCount1; currIdx++)

                {

                    string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currIdx);

 

                    bool IsDelete, IsNew = false;

                    bool.TryParse(form[subIndexKey + ".IsDeleted"], out IsDelete);

                    bool.TryParse(form[subIndexKey + ".IsNew"], out IsNew);

 

                    if (IsNew && !IsDelete)

                    {

                        if (!bindingContext.ValueProvider.ContainsPrefix(subIndexKey))

                            continue;      //no value to update skip

 

                        object elementModel = Activator.CreateInstance(collectionItemType);

 

 

                        ObjectManager objectManager = new ObjectManager();

 

                        objectManager.MarkAsChild(elementModel);

 

                        var suppress = elementModel as Csla.Core.ICheckRules;

                        if (suppress != null)

                            suppress.SuppressRuleChecking();

 

                        var elementContext = new ModelBindingContext()

                        {

                            ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => elementModel, elementModel.GetType()),

                            ModelName = subIndexKey,

                            ModelState = bindingContext.ModelState,

                            PropertyFilter = bindingContext.PropertyFilter,

                            ValueProvider = bindingContext.ValueProvider

                        };

 

                        if (OnModelUpdating(controllerContext, elementContext))

                        {

                            //update element's properties

                            foreach (PropertyDescriptor property in GetFilteredModelProperties(controllerContext, elementContext))

                            {

                                BindProperty(controllerContext, elementContext, property);

                            }

                            OnModelUpdated(controllerContext, elementContext);

                        }

 

                        var Element = elementModel as Csla.Core.BusinessBase;

 

                        collection.Add(elementModel);

 

                    }

                }

            }

 

 

 

        return bindingContext.Model;

    }

 

 

gyob00 replied on Thursday, November 10, 2011

P.s. I made the last chunk of code, because When I tried Fowler's CslaModelBinder code, the existing values were being re-added and therefore were duplicated in the object graph. were you experiencing this side-effect? I may be doing something wrong on my end

DanielFowler replied on Thursday, November 10, 2011

Hi, gyob00

I've not been working on the project since I last posted so I havn't had a chance to get it fully working yet. However briefly after the last post i think i modified it slightly and i had the binding working fine except for objects not being marked as dirty.

I'm not at work atm, ill check it tomorrow and let you know if i changed anything :]

Sorry for the late reply! And yeah i thought my modifications were a bit frankensteinish too! But deadlines are deadlines unfortunatley lol :[

gyob00 replied on Thursday, November 10, 2011

Hey!

I like your modifications - when I said Frankenstein I was referring to MY hack job.

 

Thank you for your original post. I would have been in real trouble without your examples!

:)

DanielFowler replied on Friday, November 11, 2011

Well I've just had a look and it seems to work fine, no duplicate values. However I think i may be using the binder differently from you.

I'm using it implicity, as the defaultbinder for paramaters like :  

 

        [HttpPost]
        public ActionResult SaveClient(Company client)
        {
            //save client
 
            return Redirect(string.Format("{0}/{1}", Url.Action("EditClient""Client"), //clientid));
        }

I think your pulling your existing object from the database then binding via UpdateModel() method. 

Im guessing that there could be new records coming in through the POST, you fetch the object from the DB by its id, 
call UpdateModel() method to trigger the binding. 

In the binder the collection.Count != collection returns true so you jump into the loop that actually creates a child for each record
from the POST, it doesnt check wether it already exists or not in the collection so that may be why you're getting duplicate values.




Hope that helps! :)

 

gyob00 replied on Friday, November 11, 2011

yes, it helps a lot.

 

at the very end I remove the non-new items to prevent duplication. provided that the post contains all the new AND existing items, it will work correctly.

Edi replied on Saturday, November 09, 2013

Has anyone found any good solutions of this problem? I saw it on the git issues as an enhancement but it wasn't finished too. Any sample or some guidelines would be appreciated too.

 

Thank you in advance,

Edi 

gyob00 replied on Monday, November 18, 2013

Yes, I have come to the conclusion that the demonstrative implementation of CSLA is not suited to a ASP.NET MVC environment. Its best to factor a CSLA implementation to only have the web server responsible for Log in security and load balance. The database layer should shred the data graph and deliver XML to the front end. The web server should not touch the XML normally. The front end should normally deal with XML unless loop performance becomes an issue. As Rocky states, the demonstrative implementation is really only an example. If you try to use that in an ASP.NET MVC stack, you are just screwing yourself out of performance and time and the real benefits of CSLA.

gyob00 replied on Monday, November 18, 2013

I should be very clear here... ASP.NET MVC MSSQL environment. Any changes will certainly cause one to refactor...

gyob00 replied on Monday, November 18, 2013

so basically XPATH MSSQL queries can work manage data against multiple tables faster than C# can. On average, based on my business needs, its about 20x faster. This is odd because for some reason PHP/DB architectures are horribly fast and dont seem to incur noticable perfomance overhead when dealing with multiple tables.....test, refactor, test, etc. In any event the CSLA philosophy is sound, as long as you apply it flexibly.

gyob00 replied on Monday, November 18, 2013

I've read that CLR on the DB Server can potentially match or beat the performance I've achieved with my XPATH solution. It could even allow for a more bullet proof CSLA implementation without sacrificing performance to multiple MSSQL db calls (which previously affected my particular situation). I'm just too comfortable right now to test this kind of refactor The legacy systems I deal with here, and the entrenched priests of the temple of syrinx, have caused a serenity prayer of design on my part, which has proven to scale well against dealing with a variable amount of tables...

Copyright (c) Marimer LLC