multiply Parts for Record

Topics: General
Mar 16, 2015 at 5:35 PM
Edited Mar 24, 2015 at 2:06 PM
At first I've created module "MyProject.Fruits", which contains front-end (i mean not administration) page with edit form for "Fruit" entity.

/Models/FruitPartRecord.cs
public class FruitPartRecord : ContentPartRecord {
    public virtual string Name { get; set; }
    public virtual int AverageWeight { get; set; }
    public virtual string Description { get; set; }
    public virtual int MinAmount { get; set; }
}
/Models/FruitPart.cs
public class FruitPart : ContentPart<FruitPartRecord> {
    public string Name { 
        get { return Retrieve(x => x.Name); } 
        set { Store(x => x.Name, value); } 
    }
    public int AverageWeight { 
        get { return Retrieve(x => x.AverageWeight); } 
        set { Store(x => x.AverageWeight, value); } 
    }
    public string Description { 
        get { return Retrieve(x => x.Description); } 
        set { Store(x => x.Description, value); } 
    }
    public int MinAmount { 
        get { return Retrieve(x => x.MinAmount); } 
        set { Store(x => x.MinAmount, value); } 
    }
}
/Drivers/FruitPartDriver.cs
public class FruitPartDriver : ContentPartDriver<FruitPart> {
    //GET
    protected override DriverResult Editor(
        FruitPart part, dynamic shapeHelper) {
        
        return ContentShape("Parts_Fruit_Edit",
            () => shapeHelper.EditorTemplate(
                TemplateName: "Parts/Fruit",
                Model: part,
                Prefix: Prefix));
    }

    //POST
    protected override DriverResult Editor(
        FruitPart part, IUpdateModel updater, dynamic shapeHelper) {
        updater.TryUpdateModel(part, Prefix, null, null);
        
        return Editor(part, shapeHelper);
    }
}
/Handlers/FruitPartHandler.cs
public class FruitPartHandler : ContentHandler {
    public FruitPartHandler(IRepository<FruitPartRecord> repository) {
        Filters.Add(new ActivatingFilter<FruitPart>("Fruit"));
        Filters.Add(StorageFilter.For(repository));
    }
}
/Views/EditorTemplates/Parts/Fruit.cshtml
@model FruitPart
<div>@Html.TextBoxFor(t => t.Name)</div>
<div>@Html.TextBoxFor(t => t.AverageWeight)</div>
<div>@Html.TextBoxFor(t => t.Description)</div>
<div>@Html.TextBoxFor(t => t.MinAmount)</div>
/Migrations.cs
public class FruitsDataMigration : DataMigrationImpl {
    public int Create() {
        SchemaBuilder.CreateTable("FruitPartRecord", 
            table => table
                .ContentPartRecord()
                .Column<string>("Name")
                .Column<int>("AverageWeight")
                .Column<string>("Description")
                .Column<int>("MinAmount")
            );

        return 1;
    }
}
HomeController
//..
 public ActionResult Edit(int? id) {
    // check user access
    //...
 
    ContentItem contentItem = null;
    if (id.Hasvalue) {
        contentItem = _contentManager.Get(id.Value);
    }
    if (contentItem == null || !contentItem.ContentType.Equals("Fruit")) {
        contentItem = _contentManager.New("Fruit");
    }
    var model = _contentManager.BuildEditor(contentItem).Content;
    return View(model);
}
// POST: Edit
[HttpPost, ActionName("Edit")]
[Orchard.Mvc.FormValueRequired("submit.Edit")]
public ActionResult EditPost(int id = 0) {
    // check permissions
    //... 
    
    var contentItem = _contentManager.Get(id);
    if (contentItem == null || !contentItem.ContentType.Equals("Fruit"))
        return HttpNotFound();
    var model = _contentManager.UpdateEditor(contentItem, this);

    if (!ModelState.IsValid) {
        _orchardServices.TransactionManager.Cancel();
        return View("Edit", model.Content);
    }
    return RedirectToAction("Index");
}

// POST: Edit group - creating new group
[HttpPost, ActionName("Edit")]
[Orchard.Mvc.FormValueRequired("submit.Create")]
public ActionResult CreatePost() {
    // check permissions
    //... 

    var contentItem = _contentManager.Create("Fruit");
    var model = _contentManager.UpdateEditor(contentItem, this);

    if (!ModelState.IsValid) {
        _orchardServices.TransactionManager.Cancel();
        return View("Edit", model.Content);
    }

    return RedirectToAction("Index");
}
/Views/Home/Edit.cshtml
@model dynamic
<div>
    @using (Html.BeginFormAntiForgeryPost()) {
        <div>
            @Display(Model)
        </div>
        
        //...
        <input type="submit" />
        
        // if create - add
        // @Html.Hidden("submit.Create", true) 
        // else add
        // @Html.Hidden("submit.Edit", true)
    }
</div>
and Placement.info file
<Placement>
  <Place Parts_Fruit_Edit="Content:1"/>
</Placement>
Everything works fine. But then, I've got necessity to add another module. It has similar form, but it should be only 'AverageWeight' and 'Description' fields display. I decided to create the same (like first module) structure, but i need to re-use existing FruitPartRecord.
I tried to do something like:

/Models/FruitManagedPart.cs
public class FruitManagedPart : ContentPart<FruitPartRecord> {
    public virtual string Description { 
        get { return Retrieve(x => x.Description); } 
        set { Store(x => x.Description, value); } 
    }
    public virtual int MinAmount { 
        get { return Retrieve(x => x.MinAmount); } 
        set { Store(x => x.MinAmount, value); } 
    }
}
/Drivers/FruitManagedPartDriver.cs
public class FruitManagedPartDriver : ContentPartDriver<FruitManagedPart> {
    //GET
    protected override DriverResult Editor(
        FruitManagedPart part, dynamic shapeHelper) {
        
        return ContentShape("Parts_FruitManagedPart_Edit",
            () => shapeHelper.EditorTemplate(
                TemplateName: "Parts/FruitManaged",
                Model: part,
                Prefix: Prefix));
    }

    //POST
    protected override DriverResult Editor(
        FruitManagedPart part, IUpdateModel updater, dynamic shapeHelper) {
        updater.TryUpdateModel(part, Prefix, null, null);
        
        return Editor(part, shapeHelper);
    }
}
/Handlers/FruitManagedPartHandler.cs
public class FruitManagedPartHandler : ContentHandler {
    public FruitManagedPartHandler(IRepository<FruitPartRecord> repository) {
        Filters.Add(new ActivatingFilter<FruitManagedPart>("Fruit"));
    }
}
/Views/EditorTemplates/Parts/FruitManaged.cshtml
@model FruitManagedPart
<div>Markup is not same as in first module</div>
<table>
    <tbody>
        <tr>
            <td>
                <div>@Html.TextBoxFor(t => t.MinAmount)</div>
            </td>
        </tr>
        
        <tr>
            <td>
                <div>@Html.TextBoxFor(t => t.Description)</div>
            </td>
        </tr>
    </tbody>
</table>
HomeController
//..
 public ActionResult Edit(int? id) {
    // check user access
    //...
 
    ContentItem contentItem = null;
    if (id.Hasvalue) {
        contentItem = _contentManager.Get(id.Value);
    }
    if (contentItem == null || !contentItem.ContentType.Equals("Fruit")) {
        contentItem = _contentManager.New("Fruit");
    }
    var model = _contentManager.BuildEditor(contentItem).Content;
    return View(model);
}
// POST: Edit
[HttpPost, ActionName("Edit")]
[Orchard.Mvc.FormValueRequired("submit.Edit")]
public ActionResult EditPost(int id = 0) {
    // check permissions
    //... 
    
    var contentItem = _contentManager.Get(id);
    if (contentItem == null || !contentItem.ContentType.Equals("Fruit"))
        return HttpNotFound();
    var model = _contentManager.UpdateEditor(contentItem, this);

    if (!ModelState.IsValid) {
        _orchardServices.TransactionManager.Cancel();
        return View("Edit", model.Content);
    }
    return RedirectToAction("Index");
}

// POST: Edit group - creating new group
[HttpPost, ActionName("Edit")]
[Orchard.Mvc.FormValueRequired("submit.Create")]
public ActionResult CreatePost() {
    // check permissions
    //... 

    var contentItem = _contentManager.Create("Fruit");
    var model = _contentManager.UpdateEditor(contentItem, this);

    if (!ModelState.IsValid) {
        _orchardServices.TransactionManager.Cancel();
        return View("Edit", model.Content);
    }

    return RedirectToAction("Index");
}
/Views/Home/Edit.cshtml
@model dynamic
<div>
    @using (Html.BeginFormAntiForgeryPost()) {
        <div>
            @Display(Model)
        </div>
        
        //...
        <input type="submit" />
        
        // if create - add
        // @Html.Hidden("submit.Create", true) 
        // else add
        // @Html.Hidden("submit.Edit", true)
    }
</div>
and Placement.info file
<Placement>
  <Place Parts_Fruit_Edit="-"/>
  <Place Parts_FruitManaged_Edit="Content:1"/>
</Placement>
Is it possible to re-use existing record within new part/new module? I this example I always getting Record is null. Is there any alternatives to resolve such tasks? Thanks.
Mar 23, 2015 at 11:44 AM
Okay, let me simplify the question...
I need my part (e.g. Part1) to be included into two different forms. I'm using DataAnnotations to validate my part.
What if I need to have different validation rules for Part1 (I mean diffetent between two forms)?
Is there any way to properly re-use unobtrusive validation via attributes?
Mar 24, 2015 at 12:08 AM
Interesting, I don't know if we can do that this way, I will try it

Just began to write your code, I need to use an int for MinAmount in FruitPartRecord. And a FruitPart with the ActivatingFilter declaration, not a FruitPartRecord

Did you try by using a StorageFilter in both handlers?

But, because you use Store() and Retreive() for Infoset storage, maybe you don't need at all to use a FruitPartRecord repository, part data are stored as per content item in the Data column...

Best
Mar 24, 2015 at 2:05 PM
Edited Mar 24, 2015 at 2:05 PM
Hello, thank you for reply!

Sorry, my mistake. There shoul be int MinAmount and Filters.Add(new ActivatingFilter<FruitPart>("Fruit")). I have updated my post.
When I tried to use StorageFilter in both modules I got error:

2015-03-24 15:03:16,794 [7] Orchard.ContentManagement.DefaultContentManager - Default - InvalidOperationException thrown from IContentHandler by Custom.FruitsManaged.Handlers.FruitManagedPartHandler
System.InvalidOperationException: Having more than one storage filter for a given part (Orchard.ContentManagement.ContentPart`1[[Custom.Fruits.Models.FruitPartRecord, Custom.Fruits, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]) is invalid.
   at Orchard.ContentManagement.Handlers.StorageFilter`1.Activated(ActivatedContentContext context, ContentPart`1 instance)
   at Orchard.ContentManagement.Handlers.StorageFilterBase`1.Orchard.ContentManagement.Handlers.IContentStorageFilter.Activated(ActivatedContentContext context)
   at Orchard.ContentManagement.Handlers.ContentHandler.Orchard.ContentManagement.Handlers.IContentHandler.Activated(ActivatedContentContext context)
   at Orchard.ContentManagement.DefaultContentManager.<>c__DisplayClass7.<New>b__4(IContentHandler handler)
   at Orchard.InvokeExtensions.Invoke[TEvents](IEnumerable`1 events, Action`1 dispatch, ILogger logger)
Mar 24, 2015 at 2:25 PM
I've prepared demo, but still can't figure out how to setup Placement.info files...
I need to show Fruit.cshtml on /Custom.Fruit/Home/Edit/ page
and FruitManaged.cshtml on /Custom.FruitManaged/Home/Edit/ page


folders:
https://www.dropbox.com/sh/cgz8zpum975n9gi/AACqHK8ryr46zL6O3T4xKiaCa?dl=0
7z archive:
https://www.dropbox.com/s/gvzlh01ukxmbwr5/test%20orchard%20modules.7z?dl=0

Thanks
Mar 24, 2015 at 2:41 PM
But, because you use Store() and Retreive() for Infoset storage, maybe you don't need at all to use a FruitPartRecord repository, part data are stored as per content item in the Data column...
Did you mean that I shouldn't register StorageFilter at all?
Mar 24, 2015 at 3:53 PM
Tried on a 1.8.x version. I got the same errors as you

Note that you can make a part "Attachable" via the Migration file, but indeed the use of an ActivatingFilter can do that for a given content type on the fly. Also note that you can create a Content type and define its content parts via the Migration file, but, maybe as you, I've used an already created content type via the dashboard...

First, I've used the same parts / records as you but I didn't use any controllers. Maybe you don't need them because a driver is like a controller but for a given part. So, if you have a content type with at least a "Common" part, maybe an "Autoroute" part if you need it for the front end..., and your "Fruit" part, then you can let Orchard manage your content items...

So, in this context, as you, when I use a StorageFilter in both handlers I got this self explanatory error: Having more than one storage filter for a given part. And when I use only one, I got a null reference exception. That said, If I use 2 StorageFilter but I separate the handlers in 2 features and I enable only one at a time, it works. To do that I've used the Orchard attribute: [OrchardFeature("YourFeatureName")]. Then, for a given content item we can retrieve the same data but with not the same fields depending on the selected feature. But maybe you want to use these 2 features in the same time, anyway it's not a good practice to have 2 incompatible features

There are other more complex ways to do that, e.g by using a FruitPartSetting that define options that you can use in a content type. For example, you can have a "Managed" option that you can use afterwards in your driver / views. You can also have differents parts that only store a reference to the same fruit record table by using custom drivers...

But, if you don't need to be able to change the fruit part type in a content type and retreive the same data (already stored) in the related content items. Then, you only need to have separate parts without using any part record tables and Storage Filters. Then your handler is only used for the ActivatingFilter that you can omit if you create and attach your part via the migration or the dashboard

No more time right now, let me know

Best
Mar 24, 2015 at 11:55 PM
Anyway, there are some solutions, even you use a fruit record table or only the infoset storage

If you don't want to implement custom drivers / handlers or don't use a FruitPartSettings and deal with the Content Definition Editor Events (I can show you if you need it, as a starting point see in the Core/Common/OwnerEditor/OwnerEditorSettings.cs file), one simple way is to use a fake "FruitManagedPart". So, in FruitPart.cs (or in another file/module) add this
public class FruitManagedPart : ContentPart {
}
And add this property to the FruitPart class
public bool Managed { get; set; }
In FruitPartHandler.cs, use this
[OrchardFeature("Evolutive.FruitManaged")]
public class FruitManagedPartHandler : ContentHandler {
    public FruitManagedPartHandler() {
        Filters.Add(new ActivatingFilter<FruitManagedPart>("Fruit"));
    }
}
In FruitPartDriver.cs use this
    protected override DriverResult Editor(
        FruitPart part, dynamic shapeHelper) {

            part.Managed = part.ContentItem.Has<FruitManagedPart>();

            return ContentShape("Parts_Fruit_Edit",
                () => shapeHelper.EditorTemplate(
                    TemplateName: "Parts/Fruit",
                    Model: part,
                    Prefix: Prefix));
    }
Then, in the /Views/EditorTemplates/Parts/Fruit.cshtml, use this
@model Evolutive.Automation.Models.FruitPart
@if (!(bool)Model.Managed) {
    <fieldset>
        <div>
            @Html.LabelFor(model => model.Name, T("Name"))
            @Html.TextBoxFor(model => model.Name, new { @class = "text medium" })
        </div>
    </fieldset>
    <fieldset>
        <div>
            @Html.LabelFor(model => model.AverageWeight, T("Average Weight"))
            @Html.TextBoxFor(model => model.AverageWeight, new { @class = "text medium" })
        </div>
    </fieldset>
}
...
Other fields
For the front end, in FruitPartDriver.cs you can use this
protected override DriverResult Display(FruitPart part, string displayType, dynamic shapeHelper) {

    part.Managed = part.ContentItem.Has<FruitManagedPart>();

    return ContentShape("Parts_Fruit",
        () => shapeHelper.Parts_Fruit(
            Managed: part.Managed,
            Name: part.Name,
            AverageWeight: part.AverageWeight,
            Description: part.Description,
            MinAmount: part.MinAmount
        ));
}
And use a /Views/Parts/Fruit.cshtml file like that
@if (!(bool)Model.Managed) {
    <h3>Name: @Model.Name</h3>
    <h3>Average Weight: @Model.AverageWeight</h3>
}

<h3>Description: @Model.Description</h3>
<h3>Min Amount: @Model.MinAmount</h3>

Best
Mar 25, 2015 at 1:27 PM
Thanks for examples, but in fact I have more sophisticated system ('Fruit' - it's just simple demo, which I created to produce my problem) and I'm afraid it will be really difficult to support or modify it with this approach. What if I'll need to add another form, with some other properties from FruitPartRecord, or change markup of some forms.
I definitely need to have separated Views.

I also noticed, that i don't nned to use infoset storage, so changed all my properties like
public virtual string Description { 
        get { return Record.Description; } 
        set { Record.Description = value; } 
    }
Hm.. Why do we need to use infoset storage at all? Is it some kind of approach to abandon migrations (I mean: not using Record and StorageFilter at all, but store FruitPart data within infoset)?
I noticed Orchard.Users module has both data storing approaches... Is that for productivity?

I'll take a look 'custom handlers and drivers logic' and will let you know about case
Developer
Mar 25, 2015 at 2:53 PM
Edited Mar 25, 2015 at 2:54 PM
andmaz wrote:
(...) Hm.. Why do we need to use infoset storage at all? Is it some kind of approach to abandon migrations (I mean: not using Record and StorageFilter at all, but store FruitPart data within infoset)?
I noticed Orchard.Users module has both data storing approaches... Is that for productivity?
Infoset storage was introduced as a mean to simplify storage for parts that do not require querying on and hence do not require a separate table. You're right - it also simplifies a lot of other things (no migrations, no record, no storage filter etc.).

The Orchard.Users mixed approach is necessary to allow SQL queries on that data in first place (you need a record). The added benefit of an infoset storage here is that the underlying record doesn't have to be fetched (lazy-loaded) when reading properties (think of it as a persistent cache, kind of).
Apr 14, 2015 at 12:45 PM
I'm a little bit confused.

First I want to say, that almost solved all of my tasks. But still have no clarity how does it work.
I thought, concept of 'Placement.info' file became deprecated? And It will be totally replaced by some kind of filters like 'TemplateFilterForPart'.

By the way, in my solution I removed all Placement.info files and add TemplateFilterForPart into handlers (to have ability of grouping parts with some alias)
 public class FruitPartHandler : ContentHandler {
        public FruitPartHandler(IRepository<FruitPartRecord> repository) {
            Filters.Add(new ActivatingFilter<FruitPart>("Fruit"));
            Filters.Add(new TemplateFilterForPart<FruitPart>("FruitPart", "Parts/Fruit", "baseFruit"));
            Filters.Add(StorageFilter.For(repository));
        }
    }
public class FruitManagedPartHandler : ContentHandler {
        public FruitManagedPartHandler(IRepository<FruitPartRecord> repository) {
            Filters.Add(new ActivatingFilter<FruitManagedPart>("Fruit"));
            Filters.Add(new TemplateFilterForPart<FruitManagedPart>("FruitManagedPart", "Parts/FruitManaged", "baseFruit"));
            // just for testing (if I'll need to add some other parts on FruitManaged form)
            Filters.Add(new TemplateFilterForPart<FruitManagedPart>("FruitManagedPart", "Parts/FruitManaged", "managedPart"));
            
            OnLoaded<FruitManagedPart>((context, part) => {
                var basePart = part.As<FruitPart>();
                part.MinAmount = basePart.MinAmount;
                part.Description = basePart.Description;
            });
        }
    }
As you see, I'm using single Record for all parts, so I can add any kind of ASP.NET MVC validation and it work great! Also I've updated Driver, Contollers (to specify groupId for editor templates), and made FruitManagedPart inherited from ContentPart.

Maybe this issue (I mean, filters) is still developing, because I didn't find how to set order for part when rendering a group.
Later I found undocumented possibility to add grouping for parts via Placement.info syntax ('@'). So now it's possible to modify order (position), and specify which parts should be rendered.
Now I'm using Placement.info again...

So, is Placement.info obsolete?
How can I programmatically set part's position, but without 'Placement.info' file?
is 'grouping' a good approach? What is the target or 'grouping'?