How to add list of Parts to a ContentType in code?

Topics: Core
Feb 5, 2013 at 9:57 PM
Let's say I have a CustomerPart and AddressPart and I want to create a CustomerType with the list of AddressParts + template to edit this list od Addresses(using knockout).

I dont know how to setup migrations to let CustomerType contains list of AddressParts.
Developer
Feb 7, 2013 at 5:55 PM
This is not a scenario Orchard supports as it's something conceptually different from how content types work: content part can be attached to a content item only once.

This means that if you want to have a 1-n relation like this the n side of that relation should be something that's not limited in such a way. You could store addresses in plain records and load them in the part with an IRepository<TRecord> for example. This would make a content part -> records relation and is quite simple to implement too. Actually the Training Demo module has an example like this: a part having records attached (no editor for records on the part though). Look for PersonListPart.
Coordinator
Feb 8, 2013 at 2:32 AM
What do you mean it's not a scenario Orchard supports? It wouldn't be an address part, but an address record, but otherwise this is perfectly supported: http://docs.orchardproject.net/Documentation/Creating-1-n-and-n-n-relations
Developer
Feb 8, 2013 at 12:10 PM
That's exactly what I've written.
Coordinator
Feb 10, 2013 at 6:43 AM
I'm confused: what is "not a scenario Orchard supports" then?
Developer
Feb 10, 2013 at 10:30 AM
Skorunka's idea was to use multiple AddressParts. I reflected to this, that this is not supported since parts can be attached only once to a type.
Coordinator
Feb 10, 2013 at 8:54 PM
Sorry I got confused.
Developer
Feb 10, 2013 at 9:53 PM
No problem!
Feb 11, 2013 at 8:05 AM
So if I would like to use Parts i should create AddressesPart with AddressesRecord where AddressesRecord hols the list of AddressRecord?
Thank you.
Coordinator
Feb 11, 2013 at 9:18 AM
No: parts don't live on their own. They have to belong to a content item. So either you establish a relationship with naked records, or you use full content items, but you don't make a list of parts without content items.
Feb 13, 2013 at 12:51 PM
Well, probably I still missing something...
Let's say I have AddressPartRecord and AddressPart and I would like to be able to attach list of AddressParts to any Content type. Is is possible?
Coordinator
Feb 13, 2013 at 10:43 PM
No. Parts don't live on their own, they need a content item to be attached to. You can have a relationship between your item and a list of items that have an address part, or you can have plain address records that are not part records, and make your relationship with that. The latter is simpler, but if you need addresses to be full content items, go for the former.
Feb 14, 2013 at 7:41 AM
After playing with the "Training Demo Module" from Piedone's post, I have finally find out the differences between Records, ContentTypes and ContentItems. Now I understand what did You try to say to me :). So here is my current solution:
    public class AddressRecord
    {
        public virtual int Id { get; set; }

        public virtual int ParentContentTypeId { get; set; }

        public virtual string ParentContentTypeName { get; set; }

        public virtual AddressType Type { get; set; }

        public virtual bool IsDefault { get; set; }

        public virtual string Address1 { get; set; }

        public virtual string Address2 { get; set; }

        public virtual string CityName { get; set; }

        public virtual string StateName { get; set; }

        public virtual string CountryName { get; set; }

        public virtual string Zip { get; set; }

        public virtual decimal? Longitude { get; set; }

        public virtual decimal? Latitude { get; set; }

        public virtual string Notes { get; set; }
    }

    public class AddressListPart : ContentPart
    {
        private readonly LazyField<IEnumerable<AddressRecord>> _addresses = new LazyField<IEnumerable<AddressRecord>>();

        public LazyField<IEnumerable<AddressRecord>> AddressesField
        {
            get { return _addresses; }
        }

        public IEnumerable<AddressRecord> Addresses
        {
            get { return _addresses.Value; }
        }
    }

Migrations:
...
            SchemaBuilder.CreateTable(typeof(AddressRecord).Name, table => table
                    .Column<int>("Id", c => c.PrimaryKey().Identity())
                    .Column<int>("ParentContentTypeId", c => c.NotNull())
                    .Column<string>("ParentContentTypeName", c => c.NotNull())
                    .Column<string>("Type", c => c.NotNull().WithLength(50))
                    .Column<bool>("IsDefault", c => c.NotNull())
                    .Column<string>("Address1", c => c.WithLength(100).NotNull())
                    .Column<string>("Address2", c => c.WithLength(100))
                    .Column<string>("CityName", c => c.WithLength(100).NotNull())
                    .Column<string>("StateName", c => c.WithLength(100))
                    .Column<string>("CountryName", c => c.WithLength(100).NotNull())
                    .Column<string>("Zip", c => c.WithLength(20).NotNull())
                    .Column<decimal>("Longitude", c => c.Nullable())
                    .Column<decimal>("Latitude", c => c.Nullable())
                    .Column<string>("Notes", c => c.Unlimited())
                );

            ContentDefinitionManager.AlterPartDefinition(typeof(AddressListPart).Name, part => part.Attachable(false));
...
Customer ContetType:
            ContentDefinitionManager.AlterTypeDefinition(Config.ContentTypes.Customer, type => type
                .WithPart(typeof(CustomerPart).Name)
                .WithPart(typeof(AddressListPart).Name)
                );

    public class AddressListPartHandler : ContentHandler
    {
        public AddressListPartHandler(Work<IAddressService> addressService)
        {
            this.OnActivated<AddressListPart>((context, part) => part.AddressesField.Loader(() => addressService.Value.GetAddresses(part.ContentItem.ContentType)));
        }
    }

    [OrchardFeature(ModuleConfig.Features.Core.Id)]
    public class CustomerPartHandler : ContentHandler
    {
        public CustomerPartHandler(IRepository<CustomerPartRecord> repository)
        {
            Filters.Add(StorageFilter.For(repository));
            Filters.Add(new ActivatingFilter<CustomerPart>(Config.ContentTypes.Customer));
        }
    }

    public class AddressListPartDriver : ContentPartDriver<AddressListPart>
    {
        private const string ContentTypeName = Config.ContentTypes.AddressList;

        protected override string Prefix
        {
            get { return string.Format("{0}.{1}", ModuleConfig.Features.Core.Id, ContentTypeName); }
        }

        // GET
        protected override DriverResult Editor(AddressListPart part, dynamic shapeHelper)
        {
            return this.ContentShape(
                string.Format("Parts_{0}_Edit", ContentTypeName),
                () => shapeHelper.EditorTemplate(TemplateName: "Parts/" + part.PartDefinition.Name, Model: part, Prefix: this.Prefix));
        }

        // POST
        protected override DriverResult Editor(AddressListPart part, IUpdateModel updater, dynamic shapeHelper)
        {
            updater.TryUpdateModel(part, this.Prefix, null, null);

            return Editor(part, shapeHelper);
        }
So I can attach "AddresssList" ContentType to any other ContentType to provide addresses for it. Do you see any issues? TY.
Developer
Feb 14, 2013 at 11:48 AM
You can't attach one ContentType to another ContentTYpe, but you can attach a ContentPart to any ContentType. So In your case you can attach the "AddressListPart" to any ContentType.
Feb 14, 2013 at 12:46 PM
Taht's exactly what I tried to say. Thank you.
Feb 15, 2013 at 7:46 AM
Ok, now I need some hints with saving ContentItems.
I hope I understand the basic concept of saving Content Items and why it is not easily possible to use ViewModel as a action parameter and rely on ModelBinders.

This is my POST action code for updating model:
            var customer = id.HasValue
                               ? Services.ContentManager.Get<CustomerPart>(id.Value)
                               : Services.ContentManager.New<CustomerPart>(Config.ContentTypes.Customer);

            if (customer == null)
                return new HttpNotFoundResult();

            if (!id.HasValue)
            {
                Services.ContentManager.Create(customer);
            }

            var editor = Services.ContentManager.UpdateEditor(customer, this);
This works pretty well, but what if I need to do some ContentPart specific logic after model was loaded? The editing ContentType "Customer" contains two parts "CustomerPart" and "AddressListPart". The "CustomerPart" has two properties "Password" and "PasswordConfirm" which must be only validated if the "UserId" property is not specified. What I would do normally is: in action check if User.IdHasValue, if not : ModelState.Remove("Password") and ModelState.Remove("ConfirmPassword") to make model ModelState.IsValid = true. I somehow feels that I cant do this when saving ContentItems because I dont kno model prefix and the CustomerPart should deal with it by its self. I think I should put it in CustomerPartDriver, but there I cant access ModelState.
How this should be done? Should I implement "IValidatable"or "IValidatableObject" on "CustomerPart"?

Thn aside question: Is it possible to use ViewModels for saving ContentItems?




Where should i put this validation? In "AddressListPartDriver"
Coordinator
Feb 15, 2013 at 8:55 AM
It is possible to use a view model as an action parameter, and it will rely on model binding. But it doesn't seem like you should use an action at all in this case, but rather rely on part driver.
Feb 15, 2013 at 1:10 PM
Here is my current code, and it seems to worki well:
CustomerAdminController:
        public virtual ActionResult Save(int? id)
        {
            if (!this.Services.Authorizer.Authorize(CustomerPermissions.SaveCustomers, T("Not authorized to create Customers.")))
                return new HttpUnauthorizedResult();

            var customer = id.HasValue
                               ? Services.ContentManager.Get<CustomerPart>(id.Value)
                               : Services.ContentManager.New<CustomerPart>(Config.ContentTypes.Customer);

            if (customer == null)
                return new HttpNotFoundResult();

            if (!id.HasValue)
                Services.ContentManager.Create(customer);

            Services.ContentManager.UpdateEditor(customer, this);

            if (ModelState.IsValid)
            {
                ......
                Services.Notifier.Information(id.HasValue ? T("Customer was successfully saved.") : T("Customer was successfully created."));

                return RedirectToAction("Index");
            }

            Services.TransactionManager.Cancel();

            return id.HasValue ? RedirectToAction("Edit", id) : RedirectToAction("Create");
        }

        bool IUpdateModel.TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties)
        {
            this.TryUpdateModel(model, prefix, includeProperties, excludeProperties);

            if (model is CustomerPartSaveViewModel)
            {
                var customerPartSaveViewModel = model as CustomerPartSaveViewModel;
                if (customerPartSaveViewModel.UserId.HasValue)
                {
                    this.RemoveFromModelState(Helper.GetName((CustomerPartSaveViewModel a) => a.User.Email), prefix);
                    this.RemoveFromModelState(Helper.GetName((CustomerPartSaveViewModel a) => a.User.Password), prefix);
                    this.RemoveFromModelState(Helper.GetName((CustomerPartSaveViewModel a) => a.User.ConfirmPassword), prefix);
                }
            }

            return ModelState.IsValid;
        }


CustomerPartDriver:
        protected override DriverResult Editor(CustomerPart part, IUpdateModel updater, dynamic shapeHelper)
        {
            var model = new CustomerPartSaveViewModel();
            
            updater.TryUpdateModel(model, this.Prefix, null, null);

            part = Mapper.Map(model, part);

            return Editor(part, shapeHelper);
        }
What I need to do now, Is to do some business logice before the Cusatomer ContentType is saved.
I need to create a UserPart(data are in CustomerPartSaveViewModel) and connect it to the CustomerPart:
    public class CustomerPart : ContentPart<CustomerPartRecord>
    {
        public UserPartRecord User
        {
            get { return Record.User; }
            set { Record.User = value; }
        }

        public string Title
        {
            get { return Record.Title; }
            set { Record.Title = value; }
        }

        public string FirstName
        {
            get { return Record.FirstName; }
            set { Record.FirstName = value; }
        }

        public string LastName
        {
            get { return Record.LastName; }
            set { Record.LastName = value; }
        }

        public string Phone
        {
            get { return Record.Phone; }
            set { Record.Phone = value; }
        }

        public string Notes
        {
            get { return Record.Notes; }
            set { Record.Notes = value; }
        }

        public DateTime CreatedUtc
        {
            get { return Record.CreatedUtc; }
            set { Record.CreatedUtc = value; }
        }

        public IEnumerable<AddressRecord> Addresses
        {
            get { return this.ContentItem.Get<AddressListPart>().Addresses; }
        }
    }
Shoudl I use CustomerPartHandler adn override Creating method?
Developer
Feb 15, 2013 at 5:56 PM
If you do that then you don't have access to the view model you are referring to. Unless of course you set that in a global state dictionary such as HttpContext.Items, but that is generally not a good design.

So instead you could implement a custom event, say CustomerEventHandler (derived from IEventHandler) and trigger it from within your controller. The custom event would pass a context variable with all interesting information to listeners. That way you can implement custom business logic in a decoupled fashion.