Admin - Create Custom Content Type - with 'BodyPart'

Topics: Writing modules
Mar 5, 2012 at 12:55 PM
Edited Mar 5, 2012 at 1:12 PM

I'm trying to write a module that allows a content type instance to be created and edited via the admin menu. Currently I'm just trying to create:

MyContentType

 - MyContentPart (1 date field - date is set in code during the save and not visible to the user)

 - BodyPart

However I can't work out how get the body part text to save.

MyContentPart : Date gets saved, and a record in the DB for the BodyPart gets created, but the text doesn't get saved. I've obviously wired something up wrong, but if anyone has a sample module showing how to do all this from code I'd very much appreciate it! I've been trying to follow the blog/blog post code and simplify it, with not much success :(

Mar 5, 2012 at 1:25 PM

Here's my migration:

public int Create()
{
    SchemaBuilder.CreateTable("MyObjectPartRecord",
        table => table
            .ContentPartRecord()
            .Column<DateTime>("CreatedUtc", c => c.NotNull())
    );
     ContentDefinitionManager.AlterTypeDefinition("MyObject",
        cfg => cfg
            .WithPart("BodyPart")
            .WithPart("MyObjectPart")
    );
    return 1;
}

Mar 5, 2012 at 1:27 PM

Here's my part:

public class MyObjectPart : ContentPart<MyObjectPartRecord>
{
    public string BodyText
    {
        get { return this.As<BodyPart>().Text; }
        set { this.As<BodyPart>().Text = value; }
    }

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

Mar 5, 2012 at 1:30 PM

Here's my PartRecord

public class MyObjectPartRecord : ContentPartRecord
{
    public virtual string BodyText { get; set; }
    public virtual DateTime CreatedUtc { get; set; }
}
(I've tried this with and without the 'BodyText' property as I'm not sure how the BodyPart.Text gets populated and saved.)

Mar 5, 2012 at 1:39 PM

Here's the SAVE controller method:

[HttpPost, ActionName("Edit")]
[FormValueRequired("submit.Save")]
public ActionResult Save(int myObjId) {
    var myObj = _myObjectService.Get(myObjId);
    if (myObj == null)
        return HttpNotFound();

    myObj.BodyText = "TEST"; // TEST
    myObj.CreatedUtc = DateTime.UtcNow;

    dynamic model = Services.ContentManager.UpdateEditor(myObj, this);
    if (!ModelState.IsValid)
    {
        Services.TransactionManager.Cancel();
        // Casting to avoid invalid (under medium trust) reflection over the protected View method and force a static invocation.
        return View((object)model);
    }

    Services.Notifier.Information(T("Information updated successfully."));
    return Redirect(Url.MyObjectForAdmin());
}

Mar 5, 2012 at 1:43 PM
Edited Mar 5, 2012 at 1:43 PM

You don't need to worry about saving the body part text in your custom part. 

Your Content Type contains your custom part as well as body part. Your custom part is responsible for saving its own properties, and the BodyPart is responsible for saving it's own properties. Did you look in the database table for the BodyPart? After you edit your custom type in the dashboard, can you go back into the "Content" list, click on the new instance of your type, and then see the text you entered into the body part in the WYSIWYG editor? If so, then your body part is saving correctly. 

The body part should be saving tothe database table: <your orchard db prefix>_Common_BodyPartRecord. 

Mar 5, 2012 at 1:50 PM

Ah, i see you posted a bunch of stuff right as I was replying. You are trying to create the stuff on your own through a custom controller. Is there any reason for that? I couldn't give any guidance on that as I haven't tried it before. 

One option that might work (not sure what you are trying to accomplish with the custom controller so it depends), would be to create a custom admin menu with a custom "new" link for editing your content type. Just point the link to /Admin/Contents/Create/<YourCustomTypeName>. Here's an example: 

http://contentslider.codeplex.com/SourceControl/changeset/view/26df93b8664c#src%2fAdminMenu.cs 

Mar 5, 2012 at 1:58 PM

Yes, I'm trying to do it all through code so that my module exposes it's own admin menu where I can show a list of, create and edit instances of MyObject Type.

I've mimicked the actions for the Blog. (Combined with a bit of BlogPost) So I get the same cycle of Create -> List -> Edit actions.

I get a new row in the DB Common_BodyPartRecord table, but it has NULL in the test field.

Mar 5, 2012 at 2:22 PM

Your controller method is quite a bit different from the analagous BlogPostAdminController one (for example, you aren't calling IContentManager.Create() anywhere in the code you posted). Try stepping through both to see at which point they differ. 

Mar 5, 2012 at 3:42 PM
Edited Mar 5, 2012 at 3:43 PM

There are 2 'Create' controller methods, here's the first one that's called (both follow the sequence of the BlogAdminController) when you create your first 'MyObject'

public ActionResult Create()
{
    var myObj = _contentManager.New<MyObjectPart>("MyObject");
    if (myObj == null)
         return HttpNotFound();

    dynamic model = _contentManager.BuildEditor(myObj);

    // Casting to avoid invalid (under medium trust) reflection over the protected View method and force a static invocation.
    return View((object)model);
}
Mar 5, 2012 at 3:48 PM
Edited Mar 5, 2012 at 3:48 PM

Again, i'm not sure I can give you specific guidance -- maybe someone else can chime in. If you want to solve it yourself you should investigate the differences between your controller methods and the BlogPostAdminController methods. 

Developer
Mar 5, 2012 at 8:43 PM
Edited Mar 5, 2012 at 8:43 PM

I think you should simply remove the BodyText property from your part and try again. Where ever you reference the BodyText property, replace it with .As<BodyPart>.Text for now. If it works, you could create methods instead of a property. I ran into a similar situation where I also created a wrapper property on my custom part, and it also caused the attached part not to update its content (in my case, I was working with TitlePart).

Mar 8, 2012 at 5:29 AM

Mmm, no luck yet. I've removed the BodyText property, and do still get the body text editor come up during the creation but still the text doesn't save even though the record gets created.

Back to the drawing board again I think. But if anyone does have the time to write a demo module...

Now if only there was shape tracing on the admin screens too.

Mar 8, 2012 at 9:22 AM
Edited Mar 8, 2012 at 9:24 AM

Well...

If I go in through URL <mysite>/Admin/Contents/Edit/<MyObjectId> (rather than the <mysite>/Admin/MyObject/Edit/<MyObjectId> route I set up) then it all works. So obviously my 'Edit'/'Save' in the controller isn't working i.e. is not working with the Content Type instance as a whole, so I'm still stuck. 

My 'List' method in the controller works. But how do I edit the template for the list? e.g. to edit the "View | Unpublish | Edit | Delete" menu to add or remove items or change the URL's?

It's a good learning experience, but a long one!

Mar 8, 2012 at 12:13 PM
Edited Mar 9, 2012 at 9:05 AM

Fixed it!! In my controller I was getting an instance of 'MyObjectPart' so that I could update a property (CreatedUtc) that the user cannot see using:

var myObj = _contentManager.New("MyObject");
myObj.CreatedUtc = DateTime.UtcNow;

I then proceeded to save 'MyObject', but I was actually only saving 'MyObjectPart'. What I should have done was to create the full Content Type, edit the Part, then save the ContentType. Here's what I should have done:

        [HttpPost, ActionName("Create")]
        public ActionResult CreatePOST()
        {
            // Create as ContentItem:
            var myObj = _contentManager.New("MyObject");
            // Edit our part:
            myObj.Get<MyObjectPart>().CreatedUtc = DateTime.UtcNow;
            // Populate the other parts from the submitted values:
            dynamic model = _contentManager.UpdateEditor(myObj, this);
            if (!ModelState.IsValid)
            {
                _transactionManager.Cancel();
                // Casting to avoid invalid (under medium trust) reflection over the protected View method and force a static invocation.
                return View((object)model);
            }
            // Now save the full ContentType:
            _contentManager.Create(myObj);
            Services.Notifier.Information(T("New MyObject created successfully."));
            // Return to List View
            return Redirect(Url.ListForAdmin());
        }

 

Success at last! Now how do I edit the parts in the list? i.e. to edit the "View | Unpublish | Edit | Delete" menu so that I can add or remove items or change the URL's?

Answer: Need to use my own copy of 'Content.SummaryAdmin.cshtml, but only for my admin list. The original is found in: 

Orchard.Web\Core\Contents\Views\Content.SummaryAdmin.cshtml

Or maybe I can alter the 'Model.Actions' for my content type?

Nov 8, 2012 at 1:27 PM
Edited Nov 8, 2012 at 1:30 PM

Hey, I met the same problem. But your fix does not fix mine. My code would be like this:

 

[HttpPost, ActionName("Create")]
public ActionResult CreatePOST()
{
     // Create as ContentItem:
    var myObj = _contentManager.New("MyObject");

    _contentManager.Create(myObj);
    dynamic model = _contentManager.UpdateEditor(myObj, this);

    // Return to List View
    return Redirect(Url.ListForAdmin());
}

 

I almost start to believe it might not be this piece of code causing the issue. Me too, the body record is created, but the text is not saved. Why other parts can be saved like CommonPart, TitlePart, ContainerPart, but not BodyPart. I am so frustrated.

Coordinator
Nov 8, 2012 at 8:18 PM

Anything in app_data\logs?

Nov 9, 2012 at 1:07 AM
Edited Nov 9, 2012 at 1:10 AM

Thank you bertrandleroy for your reply. Go with your suggestion, I do find some logs in the app_data\logs.

2012-11-09 09:53:07,308 [4] Orchard.ContentManagement.Drivers.Coordinators.ContentPartDriverCoordinator - HttpRequestValidationException thrown from IContentPartDriver by Orchard.Core.Common.Drivers.BodyPartDriver
System.Web.HttpRequestValidationException (0x80004005): A potentially dangerous Request.Form value was detected from the client (Body.Text="<p>abc</p>").

Server stack trace: 
   at System.Web.HttpRequest.ValidateString(String value, String collectionKey, RequestValidationSource requestCollection)
   at Microsoft.Web.Infrastructure.DynamicValidationHelper.ValidationUtility.CollectionReplacer.<>c__DisplayClass12.<ReplaceCollection>b__d(String value, String key)
   at Microsoft.Web.Infrastructure.DynamicValidationHelper.LazilyEvaluatedNameObjectEntry.ValidateObject()
   at Microsoft.Web.Infrastructure.DynamicValidationHelper.LazilyValidatingHashtable.get_Item(Object key)
   at System.Collections.Specialized.NameObjectCollectionBase.FindEntry(String key)
   at System.Collections.Specialized.NameValueCollection.GetValues(String name)
   at System.Web.Mvc.NameValueCollectionValueProvider.ValueProviderResultPlaceholder.GetResultFromCollection(String key, NameValueCollection collection, CultureInfo culture)
   at System.Web.Mvc.NameValueCollectionValueProvider.ValueProviderResultPlaceholder.<>c__DisplayClass4.<.ctor>b__0()
   at System.Lazy`1.CreateValue()

Exception rethrown at [0]: 
   at System.Web.HttpRequest.ValidateString(String value, String collectionKey, RequestValidationSource requestCollection)
   at Microsoft.Web.Infrastructure.DynamicValidationHelper.ValidationUtility.CollectionReplacer.<>c__DisplayClass12.<ReplaceCollection>b__d(String value, String key)
   at Microsoft.Web.Infrastructure.DynamicValidationHelper.LazilyEvaluatedNameObjectEntry.ValidateObject()
   at Microsoft.Web.Infrastructure.DynamicValidationHelper.LazilyValidatingHashtable.get_Item(Object key)
   at System.Collections.Specialized.NameObjectCollectionBase.FindEntry(String key)
   at System.Collections.Specialized.NameValueCollection.GetValues(String name)
   at System.Web.Mvc.NameValueCollectionValueProvider.ValueProviderResultPlaceholder.GetResultFromCollection(String key, NameValueCollection collection, CultureInfo culture)
   at System.Web.Mvc.NameValueCollectionValueProvider.ValueProviderResultPlaceholder.<>c__DisplayClass4.<.ctor>b__0()
   at System.Lazy`1.CreateValue()
   at System.Lazy`1.LazyInitValue()
   at System.Lazy`1.get_Value()
   at System.Web.Mvc.NameValueCollectionValueProvider.GetValue(String key, Boolean skipValidation)
   at System.Web.Mvc.ValueProviderCollection.GetValueFromProvider(IValueProvider provider, String key, Boolean skipValidation)
   at System.Web.Mvc.ValueProviderCollection.<>c__DisplayClass9.<GetValue>b__4(IValueProvider provider)
   at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
   at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
   at System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source)
   at System.Web.Mvc.ValueProviderCollection.GetValue(String key, Boolean skipValidation)
   at System.Web.Mvc.DefaultModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   at System.Web.Mvc.DefaultModelBinder.GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
   at System.Web.Mvc.DefaultModelBinder.BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
   at System.Web.Mvc.DefaultModelBinder.BindProperties(ControllerContext controllerContext, ModelBindingContext bindingContext)
   at System.Web.Mvc.DefaultModelBinder.BindComplexElementalModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Object model)
   at System.Web.Mvc.DefaultModelBinder.BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   at System.Web.Mvc.DefaultModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   at System.Web.Mvc.Controller.TryUpdateModel[TModel](TModel model, String prefix, String[] includeProperties, String[] excludeProperties, IValueProvider valueProvider)
   at System.Web.Mvc.Controller.TryUpdateModel[TModel](TModel model, String prefix, String[] includeProperties, String[] excludeProperties)
   at InnoCellence.iSuggest.Controllers.ChallengeController.Orchard.ContentManagement.IUpdateModel.TryUpdateModel[TModel](TModel model, String prefix, String[] includeProperties, String[] excludeProperties)
   at Orchard.Core.Common.Drivers.BodyPartDriver.Editor(BodyPart part, IUpdateModel updater, Object shapeHelper) in E:\Orchard1.5.1\src\Orchard.Web\Core\Common\Drivers\BodyPartDriver.cs:line 59
   at System.Dynamic.UpdateDelegates.UpdateAndExecute4[T0,T1,T2,T3,TRet](CallSite site, T0 arg0, T1 arg1, T2 arg2, T3 arg3)
   at Orchard.ContentManagement.Drivers.ContentPartDriver`1.Orchard.ContentManagement.Drivers.IContentPartDriver.UpdateEditor(UpdateEditorContext context) in E:\Orchard1.5.1\src\Orchard\ContentManagement\Drivers\ContentPartDriver.cs:line 79
   at Orchard.ContentManagement.Drivers.Coordinators.ContentPartDriverCoordinator.<>c__DisplayClass10.<UpdateEditor>b__f(IContentPartDriver driver) in E:\Orchard1.5.1\src\Orchard\ContentManagement\Drivers\Coordinators\ContentPartDriverCoordinator.cs:line 63
   at Orchard.InvokeExtensions.Invoke[TEvents](IEnumerable`1 events, Action`1 dispatch, ILogger logger) in E:\Orchard1.5.1\src\Orchard\InvokeExtensions.cs:line 17

Seems issue with validation error. Is that for script injection? Hope anyone has advice on how to fix.

Nov 9, 2012 at 1:22 AM

Hey bertrandleroy, I just added [ValidateInput(false)] before the controller class definition. And it works! Thank you for your great help.