orchardcmsorchardcms-1.9

What is the proper way to add a Field to a custom Part in code?


There are several similar questions that sort of deal with this issue like this one or this one offering a pretty hacky solution. None of the ones out there have a clear satisfactory answer, or an answer at all, or are asking quite the same thing to begin with.

Record

public class MyPartRecord : ContentPartRecord
{
    public virtual Boolean Property1 { get; set; }
    public virtual string Property2 { get; set; }
}

Part

public class MyPart : ContentPart<MyPartRecord>
{
    public Boolean Property1
    {
        get { return Record.Property1; }
        set { Record.Property1 = value; }
    }

    public string...
}

Migration (generated by codegen)

SchemaBuilder.CreateTable("MyPartRecord", table => table
            .ContentPartRecord()
            .Column("Property1", DbType.Boolean)
            .Column("Property2", DbType.String)
);

ContentDefinitionManager.AlterPartDefinition("MyPart", part => part
    .Attachable()
);

Editor template

@model Project.Module.Models.MyPart

<fieldset>
<legend>MyPart</legend>

<!-- Property1 -->
@Html.EditorFor(m => m.Property1)
@Html.LabelFor(m => m.Property1)

...

</fieldset>

This is all taken from the official documentation on writing Content Parts and works fine. However, I want my custom Part to also have a MediaLibraryPickerField. Adding one through a migration is easy enough:

ContentDefinitionManager.AlterPartDefinition("MyPart", part => part
    .WithField("Image", field => field
        .OfType("MediaLibraryPickerField")
        .WithDisplayName("Image")
        .WithSetting("MediaLibraryFieldSettings.Required", "False")
    )
);

But there are several problems I bump into using this approach.

1) I can't render the field in my template, only use placement to have it show up somewhere above or below the rest of the template, so I can't group it with the properties that it belongs to.

2) Since it's attached to MyPart and not the ContentPart of the Type that MyPart gets attached to, admins can't adjust its settings through the GUI, only remove it (is this a bug or a feature that has yet to be implemented?).

3) I'm unsure how to access the MediaLibraryField in code, since ContentItem.MyPart returns a MyPart object now, so ContentItem.MyPart.Image.FirstMediaUrl no longer works.

How do I get around these issues? Am I missing something obvious here? Is there perhaps a way to add Media as a property to my model instead of using a Field and still have Orchard persist it? I would really like to avoid modifying my HTML and copying code from the official implementation to my custom views.


Solution

  • 1) Use placement.info and alternates to customize where you want to render the field

    2) You should be able to adjust the settings in Dashboard -> Content Definition -> YourContentType -> Parts -> under the MyPart settings.

    You could also attach the field to the type instead (note: it isn't really attached to the type, but to the part with the same name as the type):

    Migrations.cs:

    ContentDefinitionManager.AlterPartDefinition("MyType", part => part
        .WithField("Image", field => field
            .OfType("MediaLibraryPickerField")
            .WithDisplayName("Image")
            .WithSetting("MediaLibraryFieldSettings.Required", "False")
        )
    );
    
    ContentDefinitionManager.AlterTypeDefinition("MyType", type => type
        .WithPart("MyType");
    

    3) You can either use the dynamic notation, or search the field:

    // cast to dynamic
    var url = ((dynamic)ContentItem).MyPart.Image.FirstMediaUrl
    
    // or search for the field
    var field = ContentItem.MyPart.Fields.OfType<MediaLibraryPickerField>().Single(f => f.Name == "Image");
    // or ContentItem.MyPart.Fields.Single(f => f.Name == "Image") as MediaLibraryPickerField;
    var url = field.FirstMediaUrl;