Render the comments list and comment form

Topics: Customizing Orchard, General
Dec 26, 2014 at 1:02 AM
Hi,

I am struggling to work out how to render the comments list and comments form in an alternate:

Content-Course.Detail.cshtml

I have managed to get all other fields rendering as I have wanted, using shape tracing and a little help along the way, but the comments sections really have me stumped.

Using the shape tracing so far there was a lot of code to include in me Content-Course.Detail alternate, however this seemed to create an object not set error on:
    var settings = commentsPart.TypePartDefinition.Settings.GetModel<CommentsPartSettings>();
This was the code I had relating to the comments section for my content type of "Course":
@using Orchard.Comments;
@using Orchard.Comments.Models
@using Orchard.Comments.Settings
@using Orchard.Utility.Extensions;

@{
    CommentsPart commentsPart = Model.ContentPart;
    bool canStillCommentOn = Model.CanStillComment;
    var settings = commentsPart.TypePartDefinition.Settings.GetModel<CommentsPartSettings>();
    // add 'comments' class on the list container
    Model.List.Classes.Add("comments");
}

<h2 class="comment-count">@T.Plural("No Comments", "1 Comment", "{0} Comments", (int)Model.CommentCount)</h2>
@Display(Model.List)

@* render reply button if threaded comments enabled *@
@if(commentsPart.ThreadedComments) {
    Script.Require("jQuery");
    using (Script.Foot()) {

<script type="text/javascript">
//<![CDATA[
    $(function() {
        $('.comment-reply-button').click(function() {
            var self = $(this);

            var reply = $('#Comments_RepliedOn');
            var currentReply = reply.val();

            @* should we restore the form at its original location ? *@
            if (currentReply && currentReply.length > 0) {
                reply.val('');
                $('#comment-form-beacon').after($('.comment-form'));
            } else {
                @* assign repliedOn id *@
                var id = self.data('id');
                reply.val(id);

                @* inject the form in the replied zone *@
                $('.comment-form').appendTo(self.parents('article').first());
            }

            @* don't execute the link action *@
            return false;
        });


        @if (TempData.ContainsKey("Comments.RepliedOn")) {
            // invalid form while replying

        <text>
        var reply = $('#Comments_RepliedOn');
        reply.val(@TempData["Comments.RepliedOn"]);
        $('.comment-form').appendTo($('#comment-@TempData["Comments.RepliedOn"]'));
        </text>
        }

    });
//]]>
</script>
 }
}
@if (!Model.ContentPart.CommentsActive || !canStillCommentOn) {
    if (Model.ContentPart.Comments.Count > 0) {
    <div id="comments">
        <p class="comment-disabled">@T("Comments have been disabled for this content.")</p>
    </div>
    }
}
else if (settings.MustBeAuthenticated && WorkContext.CurrentUser == null) {
    <div id="comments">
        <p class="comment-disabled">@T("You must be authenticated in order to add a comment.")</p>
    </div>
}
else if (WorkContext.CurrentUser == null && !AuthorizedFor(Permissions.AddComment)) {
    <h2 id="add-comment">@T("Add a Comment")</h2>
    <p class="info message">@T("You must {0} to comment.", Html.ActionLink(T("log on").ToString(), "LogOn",
                            new { Controller = "Account", Area = "Orchard.Users", ReturnUrl = string.Format("{0}#addacomment", Context.Request.RawUrl) }))</p>
}
else {
    @Html.ValidationSummary() 
    <span id="comment-form-beacon"></span>
    using (Html.BeginFormAntiForgeryPost(Url.Action("Create", "Comment", new { Area = "Orchard.Comments", ReturnUrl = Context.Request.ToUrlString() }), FormMethod.Post, new { @class = "comment-form" })) {
        if (TempData.ContainsKey("Comments.InvalidCommentEditorShape")) {
            @Display(TempData["Comments.InvalidCommentEditorShape"]);
        }
        else {
            @Display(Model.EditorShape)
        }
    <button class="primaryAction" type="submit">@T("Submit Comment")</button>
    }

}
Any help would be much appreciated!

Many thanks

Andy
Developer
Dec 26, 2014 at 7:20 PM
The NullReferenceException makes sense because you are accessing properties on the model that don't exist.
Keep in mind that you are working in the "Content-Course.Detail.cshtml" view, whose model is a shape of type "Content", and that this shape contains "child" shapes that you want to render (including the comments shapes).

The recommended way of doing this is by simply rendering a local zone (e.g. "Footer", or "Comments"; I'd go with "Footer" as it seems more generic and can be reused to add more shapes above or below the comments, but it is really up to you), and configure your "Placement.info" file to render the "Parts_ListOfComments" shape in that zone.

Let us know if you have more questions about this.
Dec 26, 2014 at 11:51 PM
Hi sfmskywalker!

Thanks for the reply, I haven't really used the placement.info before, I tender to just render the parts and fields out the models. Its probably why I've found it harder at times, but in the idea of consistency with the rest of my project, can you tell me how I can go about including the comments list and comments form parts in my Content-Course.Detail.cshtml?

This is my code so far, excluding the comments parts that is:
@using Orchard.Utility.Extensions;
@using Orchard.Taxonomies;
@using Orchard.Taxonomies.Models;
@using Orchard.Taxonomies.Services;
@using Orchard.Comments.Models;
@{
    if (Model.Title != null) {
        Layout.Title = Model.Title;
    }

    Model.Classes.Add("content-item");
    
    var contentTypeClassName = ((string)Model.ContentItem.ContentType).HtmlClassify();
    Model.Classes.Add(contentTypeClassName);

    IEnumerable<TermPart> prerequisites = Model.ContentItem.Course.Prerequisites.Terms;
    IEnumerable<TermPart> audience = Model.ContentItem.Course.Audience.Terms;

    // cast in order to have improved accessors
    dynamic contentItem = Model.ContentItem;

    // access each part by its name
    string body = contentItem.BodyPart.Text;

    var tag = Tag(Model, "article");
}

@tag.StartElement
    <header>
        @Display(Model.Header)
        @if (Model.Meta != null) {
        <div class="metadata">
            @Display(Model.Meta)
        </div>
        }
    </header>
    @if(audience != null)
    {
    <div class="course-audience">
        <div class="form-group">
            <label class="col-sm-12">Who Should Attend</label>
        </div>
        <div class="form-group">
            <div class="col-sm-12">
                <ul id="Course_Audience">
                    @foreach(var a in audience)
                    {
                    <li>@a.Name</li>
                    }
                </ul>
            </div>
        </div>
    </div>
    }
    @if(prerequisites != null)
    {
    <div class="course-prerequisites">
        <div class="form-group">
            <label class="col-sm-12">Previous Experience</label>
        </div>
        <div class="form-group">
            <div class="col-sm-12">
                <ul id="Course_Prerequisites">
                    @foreach(var p in prerequisites)
                    {
                    <li>@p.Name</li>
                    }
                </ul>
            </div>
        </div>
    </div>
    }
    <div class="course-summary">
        <div class="form-group">
            <label class="col-sm-12">Summary</label>
        </div>
        <div class="form-group">
            <div class="col-sm-12">
                <p>@Model.ContentItem.Course.Summary.Value</p>
            </div>
        </div>
    </div>
    @if((Model.ContentItem.Course.Duration.Value != null && Model.ContentItem.Course.Duration.Value > 0) || (Model.ContentItem.Course.Price.Value != null && Model.ContentItem.Course.Price.Value > 0))
    {
        <div class="row">
            @if(Model.ContentItem.Course.Duration.Value != null && Model.ContentItem.Course.Duration.Value > 0)
            {
                <div class="course-duration">
                    <label class="col-xs-4 col-sm-2 lbl-back">Duration</label>
                    <div class="col-xs-8 col-sm-4 val-back">
                        @Model.ContentItem.Course.Duration.Value Days
                    </div>
                </div>
            }
            @if(Model.ContentItem.Course.Price.Value != null && Model.ContentItem.Course.Price.Value > 0)
            {
                <!-- Add the extra clearfix for only the required viewport -->
                <div class="clearfix visible-xs"></div>
                <div class="course-price label-back">
                    <label class="col-xs-4 col-sm-2 lbl-back">Price</label>
                    <div class="col-xs-8 col-sm-4 val-back">
                        £@Model.ContentItem.Course.Price.Value
                    </div>
                </div>
            }
            <br />
        </div>
    }
    @if(body != null)
    {
    <div class="course-body">
        <div class="form-group">
            <label class="col-sm-12">Course Details</label>
        </div>
        <div class="form-group">
            <div class="col-sm-12">
                <p>@T(Model.ContentItem.BodyPart.Text)</p>
            </div>
        </div>
    </div>
    <br />
    }
    
    @if(Model.Footer != null) {
    <footer>
        @Display(Model.Footer)
    </footer>
}
@tag.EndElement
Thanks again for all the help!

Andy
Dec 27, 2014 at 10:56 AM
Hi,

I had a go with the placement idea you suggested and think I have done it correctly, at first I was having issue where by the styling of the footer was duplicated in the body, in my case this meant that the body text had a green background when it should be white.

I played a little more and got more and more in to a mess, until I removed the footer tags from around where I was rendering the Model.Footer in my template and also added before to the placement.

This is where I am a little unsure as originally I had footer:0 and footer:1 for the placement of the comment list and form respectively, now I just have footer:before for both as below. Please can you confirm which is correct as this is where I was a little unsure of the purpose?
    <Match ContentType="Course">
        <Match DisplayType="Detail">
            <Place Parts_ListOfComments="Footer:before"/>
            <Place Parts_CommentForm="Footer:before"/>
        </Match>
    </Match>
This is rendered in my alternate with:
    @if(Model.Footer != null) {
            @Display(Model.Footer)
    }
I would still be interested in knowing how to do this without placement.info, but thanks again for the help :)

Many thanks

Andy
Dec 29, 2014 at 1:38 AM
Tested in a Content.Detail.cshtml alternate. Maybe you need some null checking (e.g if you remove the comments part), but a test like that (Model.ContentItem.CommentsPart != null) throw an exception. So here I use a strongly typed contentItem to be able to use the .As<>() extension

The comments form doesn't really belong to the ContentItem shape, we have to go through the Content shape list. But here, I think you have better to use an alternate of Parts.CommentForm-YourType.cshtml

Note: With shape tracing, to see where is the property you want, you need to start with the Model that correspond to the view you are overriding. So, select first the right part on the left panel, and go through the Model tree to see if you can retrieve the property (you have seen in a sub model). So, if you want to copy / paste what you see in the template section, you need to override the same view
@using Orchard.Comments.Models
@using Orchard.Comments.Settings
@using Orchard.ContentManagement;
@{
    var contentItem = (ContentItem)Model.ContentItem;
    var commentsPart = contentItem.As<CommentsPart>();
    //var commentsPart = Model.ContentItem.CommentsPart; // fails if no CommentsPart

    bool? canStillCommentOn = null;
    CommentsPartSettings settings = null;
    if (commentsPart != null) {
        //settings = commentsPart.TypePartDefinition.Settings.GetModel<CommentsPartSettings>(); // works
        settings = commentsPart.Settings.GetModel<CommentsPartSettings>(); // this works also, see in the Model

        foreach (var item in Model.Content.Items) { // go through the Content shape list
            if (item.CanStillComment != null) {
                canStillCommentOn = item.CanStillComment;
                break;
            }
        }
    }
}
Regards