[RESOLVED]Layout Needs a Model

Hi All,

I am working on a project for which the UI was created by someone else.  The project has a Partial View called _ApplicationLayout.cshtml that renders on every view page in the project.   It also hosts several partial views, one of which is the "_Comments.cshtml"
view in which I need to display all of the comments from the database that are associated with the application in the right bar of the layout.  I cannot use a View Model in the _Comments.cshtml partial, because it is rendered in the _ApplicationLayout.cshtml
and of course the View Model clashes with all of the other View Models throughout the project.  Can anyone tell me just how can solve this problem?  The comments do need to show on every page (view) in the project.

You can do it via two different ways,

  1. Call @Html.RenderAction  rather than partial to render the result of an action method, that will give you chance to separate out the code with strongly typed model
  2. Create a base controller and initialize the data and assign it to ViewBag which is required for comments view, now all controllers will inherit from base controller and they will have the data available.

I would prefer the first approach as it is a MVC way of doing things and avoid any accidental miss of controller inheritance.

Either move the comments-partial to the actual View, or replace the Partial helper with an Action helper and pass it an ID/value to process data and return a partial view

Example–

Instead of:

@Html.Partial("_Comments")

do:

@Html.Action("GetComments", new { id = X })

where X could be a ViewBag.Value or a URL parameter, example:

@Html.Action("GetComments", new { id = ViewBag.BlogID })

public ActionResult GetComments(int id)
{
  var comments = db.Comments.Where(c => c.BlogID == id).ToList();

  return PartialView("_Comments", comments);
}

The problem is that there  will be a list of comments, and there is a "textarea" for adding more comments on the fly.  Will the solution you suggested still work?  I don’t have the option of moving the Partial out of the _ApplicationLayout.cshtml.  Also,
how can I specify the controller where the ActionResult is in the code:

@Html.Action("GetComments"new { id = ViewBag.loanApplicationId})
?

You can specify the Controller as another parameter of the Action helper:

@Html.Action("GetComments", "Home", new { id = ViewBag.loanApplicationId })

As for your text-area, just give this input a name that matches the post-action param:

@using Html.BeginForm("AddNewComment"))
{
@Html.Hidden("ApplicationId", ViewBag.loanApplicationId)
<div>
Add Comment: @Html.TextArea("NewComment")
</div>

<input type="submit" value="Submit" />
}

Action:

[HttpPost]
public ActionResult AddNewComment(int ApplicationId, string NewComment)
{
  // use ApplicationId to fetch the record
// add NewComment to the record

return redirectToAction("Index"); }

That’s a fairly simple solution.  If you have questions, ask away

Okay, I think I understand it now.  The only other question I have is if they should add a comment (remember they could be on any View page in the project), Once the comments pane is updated the user is has to return to the same view.  From what I have read
online, this should be some sort of "child action only".  Is that correct?

By the way, you are really awesome to have taken the time to help me!  I wish I could send you a gift!

So what you want to do is use an Ajax form, process the comment, then return a fresh instance of your Partial view back to the main View.  Let’s go through the whole thing, but with the ajax:

1) In your _Layout, you’ve got the Html.Action(…) helper.  We should wrap this in a <div> tag because we’re going to update this after our ajax-post

<div id="commentsDiv">
  @Html.Action("GetComments", "Home", new { id = ViewBag.loanApplicationId })
</div>

2) In your partial view, you’ve got your form that allows the user to add a comment.  We’re going to change this to an Ajax form

@using (Ajax.BeginForm("AddNewComment", new AjaxOptions { HttpMethod = "POST", UpdateTargetId = "commentsDiv" }))
{
  @Html.Hidden("AppId", ViewBag.loanApplicationId)
  <div>
    Add Comment: @Html.TextArea("NewComment")
  </div>

  <input type="submit" value="Submit" />
}

3) In the post-action, we process the new comment, then return our *UPDATED* _Comments partial view back to the view

[HttpPost]
public ActionResult AddNewComment(int AppId, string NewComment)
{
  // use ApplicationId to fetch the record
// add NewComment to the record

var comments = db.Comments.Where(c => c.ApplicationId == AppId).ToList();

return PartialView("_Comments", comments); }

And that’s it!  Now it’s important to note that you MUST have the unobtrusive-ajax scripts included in your View.  If you are unsure whether you have these, check your Scripts folder (download them from the NuGet manager if needed).

I received the following error when I tried to run this:

"Additional information: Error executing child request for handler ‘System.Web.Mvc.HttpHandlerUtil"

I tried to run in without the Id in the parameters and just hardcoded the ID in the method just for testing.  Here is what is in my _ApplicationLayout:

  <div id="PageClassDiv" class="@(ViewBag.PageClass) APPLICATION"><!-- page content container-->
        
        @Html.Partial("~/Views/Shared/_NavigationView.cshtml") <!-- Navigation Partial View -->
        @Html.Partial("~/Views/Shared/_SearchAccordionView.cshtml") <!-- Search Accordion Partial View -->

        <!-- All applications should have comments and sidebar ============================================================================================-->
        @Html.Partial("~/Views/Shared/_SideBarView.cshtml");
        @*@Html.Partial("~/Views/Shared/_CommentsView.cshtml");*@
        @Html.Action("GetComments", "LoanApplication")
        
        <!-- content ============================================================================================-->
        @if (angularEnabled)
        {
            <div class="container" ng-app="@angularAppName">@RenderBody()</div>
        }
        else
        {
            <div class="container">@RenderBody()</div>
        }
    </div><!-- end page content container-->

This is the CommentsView:

<span class="comments">
    <a id="right-menu" href="#right-menu" class="icon-comments"></a>
</span>
<span class="clear"></span>

<div id="sidr-right" class="sidr right">
    <header>
        <h4> Application Notes </h4>
        <a href="#" class="icon-print"></a>
        @using (@Html.BeginForm("Comment", "LoanApplication"))
        {
           @Html.TextArea("Comment")
        @*<textarea cols="100" rows="2" name="Comment" placeholder="Leave Comment ..."></textarea>*@
            <input value="Add Comment" type="submit" />
        }
    </header>

And this is what is in my controller:

  [HttpGet]
        [Route("GetComments")]
        public async Task<ActionResult> GetComments()
        {
            var loanApplicationServiceProxy = base.ServiceProvider.LoanApplicationServiceProxy;

            var comments = await loanApplicationServiceProxy.GetLoanApplicationCommentsByLoanApplicationIdAsync(loanApplicationId) ?? new List<LoanApplicationComment>();
            
            return PartialView("_CommentsView", comments);
        }


 [HttpPost]
        [Route("Comment")]
        public async Task<ActionResult> Comment(FormCollection form)
        {

            var loanApplicationServiceProxy = base.ServiceProvider.LoanApplicationServiceProxy;
            var userId = this.User.Identity.GetUserId();
            string comment = Request.Form["Comment"];
            var applicationComment = new LoanApplicationComment
            {
                Comment = comment,
                CreatedDate = DateTime.Now,
                LoanApplicationId = loanApplicationId,
                Id = Guid.NewGuid(),
                CreatedBy = Guid.Parse(userId)
            };
            await loanApplicationServiceProxy.PutLoanApplicationCommentAsync(applicationComment);

            var comments = loanApplicationServiceProxy.GetLoanApplicationCommentsByLoanApplicationIdAsync(loanApplicationId);

            return View(comments);
         
        }

The code in the Controller "GetComments" never gets hit.  The application throws an error as soon as I try to navigate to a View page that has the comments pane.

BeeDev

The code in the Controller "GetComments" never gets hit.

take off the [HttpGet] and [Route("GetComments")] annotations, and see if that works.

I was able to get it to hit the controller, but I can’t figure out how to actually display the comments.  I’m used to using @model.Comment, or something of the sort, but there is not model, so how do I specify the fields that I need to display?  I must display
the user name, date and time of comment, along with the comment.

This is going beyond the original scope of the question.  If I understand everything correctly, your _Comments partial view displays rows of comments that include user name, date & time of comment, and the comment.

The model for Comment may look something like this (I’m just guessing since you have not posted the actual model):

public class Comment
{
public int CommentId { get; set; } public string Comment { get; set; }
public string UserName { get; set; } public string CDate { get; set; }
public int App_ID { get; set; } }

The model includes a primary index key (CommentId), the other required fields, and also a foreign key reference (App_ID)

The _Comment partial view may then look like this:

@model IEnumerable<AppName.Models.Comment>

<h2>Submit Comment</h2>

@using (Ajax.BeginForm("AddNewComment", "LoanApplication", new AjaxOptions { HttpMethod = "POST", UpdateTargetId = "commentsDiv" }))
{
  @Html.Hidden("AppID", ViewBag.loanApplicationId)
  <div>
    Comment: @Html.TextArea("NewComment")
  </div>

  <input type="submit" value="Submit" />
}

<h2>Comments</h2>

@foreach (var item in Model)
{
  <div>
    <dl class="dl-horizontal">
      <dt>Username</dt>
      <dd>@Html.DisplayFor(modelItem => item.UserName)</dd>

      <dt>Date</dt>
      <dd>@Html.DisplayFor(modelItem => item.CDate)</dd>

      <dt>Comment</dt>
      <dd>@Html.DisplayFor(modelItem => item.Comment)</dd>
    </dl>
  </div>
}

To explain, our @model is a LIST of comments that will be passed to the partial view.  At the top I’ve included the AJAX form, which allows a user to submit their comment.  Below the form is the Comments section, which loops through each comment, displaying
the User, the Date, and the Comment (I used DL formatting but you can use TABLE or DIV, etc).  Now when the form posts and finishes processing, we’ll be returning a fresh _Comments partial view, which will replace the old one.  We use "UpdateTargetId" to specify
where to update.

Your AJAX post-action in your "LoanApplication" controller:

[HttpPost]
public ActionResult AddNewComment(int AppID, string NewComment)
{
  // create new comment object
  var comment = new Comment();
  comment.Comment = NewComment;
  comment.UserName = User.Identity.Name;
  comment.CDate = DateTime.Now;
  comment.App_ID = AppID;

  // add comment to db
  db.Comments.Add(comment);
  db.SaveChanges();

// query new comments list and return to partial view var comments = db.Comments.Where(c => c.App_ID == AppID).ToList(); return PartialView("_Comments", comments); }

I’ve commented this for explanation.

Your _Layout view now only needs to have the initial Action helper inside a named DIV

<div id="commentsDiv">
  @Html.Action("GetComments", "LoanApplication", new { id = ViewBag.loanApplicationId })
</div>

As said earlier, once the AJAX form posts, a new version of _Comments partial view will replace the contents of "commentsDiv".

Lastly, the "GetComments" action (in the LoanApplication controller):

public ActionResult GetComments(int id)
{
  var comments = db.Comments.Where(App_ID == id).ToList();

  return PartialView("_Comments", comments);
}

And that’s it.  There’s the entire solution…  If all of this works for you, please take a moment to mark all my responses as answers, as they all answer each of your subsequent questions.  Thanks.

Okay, I figured out that I can put a model in the _CommentsView.cshtml since it is being rendered with an @Html.Action.  Thanks for ALL of your help!  You are awesome!!!

Leave a Reply