Internet Engineer
Internet Engineer

Reputation: 2534

MVC Ajax with Dynamic Partial View Creation

How can I create dynamic ajax.actionlinks that will call dynamic partial views.

For example:

What have I done so far:

What is the issue

How can I accomplish this dynamically?

Existing Code:

My ajax.actionlink inside my razor view

 @Html.Raw(Ajax.ActionLink("[replacetext]", "VoteUp",
                new { UserPostID = @Model.Id },
                        new AjaxOptions { HttpMethod = "POST", InsertionMode = InsertionMode.Replace, UpdateTargetId = "CountVote" }).ToHtmlString().Replace("[replacetext]",
                        "<img src=\"/Images/up_32x32.png\" />"))

My div inside the same razor view to display the returning results from the partial view.

<div id="CountVote" class="postvotes"></div>

My controller

    public PartialViewResult VoteUp(int UserPostID)
    {
        try
        {
            UserVotes vote = new UserVotes();
            vote.SubmitedVote = 1;
            vote.UserId = Convert.ToInt32(Session["id"]);
            vote.UserPostID = UserPostID;
            ViewBag.SumVotes = postRepository.InsertUserPostVote(vote);

        }
         catch (Exception e)
        {
            xxx.xxx.xxxx().Raise(e);
        }
        return PartialView("_TotalVotes");
    }

And finally my partial view (_TotalVotes.cshtml)

@ViewBag.SumVotes

Now my main view for Viewpost shows the comments in a loop using the viewbag.

foreach (var item in (List<UserComment>)ViewData["Comments"])
            {
                CommentVote = "cv" + i.ToString();
    <div class="postlinewrapper">
        <div class="postvotesframe">
            <div class="postvotes">
                @Html.Raw(Ajax.ActionLink("[replacetext]", "VoteUp",
                        new AjaxOptions { HttpMethod = "POST", InsertionMode = InsertionMode.Replace, UpdateTargetId = "CountVote" }).ToHtmlString().Replace("[replacetext]",
                        "<img src=\"/Images/up_32x32.png\" />"))
            </div>

            <div id="@CommentVote" class="@CommentVote">0</div>
            <div class="postvotes">
                @Html.Raw(Ajax.ActionLink("[replacetext]", "VoteDown",
                        new AjaxOptions { HttpMethod = "POST", InsertionMode = InsertionMode.Replace, UpdateTargetId = "CountVote" }).ToHtmlString().Replace("[replacetext]",
                        "<img src=\"/Images/down_32x32.png\" />"))
            </div>
        </div>
        <div class="postleftbar">
            @Html.Raw(item.Comment)
        </div>
        <div class="postrightbar">
            <div>
                <div class="post_spec">
                    <div class="post_spec_title">Call Sign:  </div>
                    <div class="post_spec_detail">@item.CallSign</div>
                </div>
                <div class="post_spec">
                    <div class="post_spec_title">When:  </div>
                    <div class="post_spec_detail">@item.CommentDate.ToString("dd/MM/yyyy")</div>
                </div>
            </div>
            <br />
            <br />
        </div>
    </div>
                i += 1;
            }

I have implemented the login to increase or decrease votes up and down:

 public PartialViewResult VoteUp(int userPostId)
        {
            try
            {
                UserVotes vote = new UserVotes();
                vote.SubmitedVote = 1;
                vote.UserId = Convert.ToInt32(Session["id"]);
                vote.UserPostID = userPostId;
                ViewBag.SumVotes = postRepository.InsertUserPostVote(vote);

            }
             catch (Exception e)
            {
                xxxx.xxxx.xxxx().Raise(e);
            }
            return PartialView("_TotalVotes");
        }

        public PartialViewResult VoteDown(int userPostId)
        {
            try
            {
                UserVotes vote = new UserVotes();
                vote.SubmitedVote = -1;
                vote.UserId = Convert.ToInt32(Session["id"]);
                vote.UserPostID = userPostId;
                ViewBag.SumVotes = postRepository.InsertUserPostVote(vote);

            }
            catch (Exception e)
            {
                xxx.xxxx.xxxx().Raise(e);
            }
            return PartialView("_TotalVotes");
        }

Now all this code works for 1 ajax call just fine, but what I need to is to display separate ajax calls for separate divs dynamically.

Upvotes: 15

Views: 2941

Answers (2)

Jasen
Jasen

Reputation: 14250

Try it this way.

Main view

I'm supposing you have a model with a collection property Comments of Comment items

@model MyNamespace.CommentAndOtherStuff

<ul>
    @foreach(item in Model.Comments)
    {
      <li>
          <a href="@Url.Action("VoteUp", "VoteControllerName", new { UserPostId = item.Id })" 
             class="vote-link"
             data-id="@item.Id">@item.Votes</a><img src="vote.jpg" />
      </li>
    }
</ul>

And your controller just returns a class called VoteResult as JSON.

[HttpPost]
public ActionResult VoteUp(int UserPostID)
{
    ...
    var model = new VoteResult
    {
        UserPostID = UserPostID,
        Votes = service.tallyVote(UserPostID)
    };

    return Json(model);
}

Now hook all of those up with a jQuery event handler and setup an AJAX call

$(document).ready(function() {

    $("a.vote-link").on("click", function(event) {
        event.preventDefault();
        var link = $(this);  // the link instance that was clicked
        var id = link.attr("data-id");
        var url = link.attr("href");

        $.ajax({
            url: url,
            type: "post"
        })
        .done(function(result) {
            // JSON result: { UserPostID: 1, Votes: 5 }

            // replace link text
            link.html(result.Votes);
        });
    });

});

But I want a partial view html fagment.

[HttpPost]
public ActionResult VoteUp(int UserPostID)
{
    ...
    var model = new VoteResult
    {
        UserPostID = UserPostID,
        Votes = service.tallyVote(UserPostID)
    };

    return PartialView("_TotalVotes", model);
}

_TotalVotes partial

@model MyNamespace.VoteResult

@if (Model.Votes < 0)
{
    <span class="unpopular">@Model.Votes</span>
}
else
{
    <span class="awesome">@Model.Votes</span>
}

And adjust the AJAX callback

.done(function(result) {
    link.html(result);
});

Now you could write a helper for the link fragment but it obfuscates things in my opinion (it's a judgement call). All you really need here is the class name and the data-id which your javascript will bind.

Upvotes: 5

user3559349
user3559349

Reputation:

Using the Ajax helpers here seems an unnecessary overhead and I suggest you just use jquery methods to update the DOM. Your current code suggests you might be missing some logic to make a comment voting system work, including indicating what action the user may have already performed. For example (and assuming you want it to work similar to SO), if a user has previously up-voted, then clicking on the up-vote link should decrement the vote count by 1, but clicking on the down-vote link should decrement the vote count by 2 (the previous up-vote plus the new down-vote).

Refer to this fiddle for how this might be styled and behave when clicking the vote elements

Your view model for a comment might look like

public enum Vote { "None", "Up", "Down" }
public class CommentVM
{
  public int ID { get; set; }
  public string Text { get; set; }
  public Vote CurrentVote { get; set; }
  public int TotalVotes { get; set; }
}

and assuming you have a model that contains a collection of comments

public class PostVM
{
  public int ID { get; set; }
  public string Text { get; set; }
  public IEnumerable<CommentVM> Comments { get; set; }
}

and the associated DisplayTemplate

/Views/Shared/DisplayTemplates/CommentVM.cshtml

@model CommentVM
<div class="comment" data-id="@Model.ID" data-currentvote="@Model.CurrentVote">
  <div class="vote">
    <div class="voteup" class="@(Model.CurrentVote == Vote.Up ? "current" : null)"></div>
    <div class="votecount">@Model.TotalVotes</div>
    <div class="votedown" class="@(Model.CurrentVote == Vote.Down ? "current" : null)"></div>
  </div>
  <div class="commenttext">@Html.DisplayFor(m => m.Text)</div>
</div>

Then in the main view

@model PostVM
.... // display some properties of Post?
@Html.DisplayFor(m => m.Comments)

<script>
  var voteUpUrl = '@Url.Action("VoteUp")';
  var voteDownUrl = '@Url.Action("VoteDown")';
  $('.voteup').click(function() {
    var container = $(this).closest('.comment');
    var id = container.data('id');
    var voteCount = new Number(container.find('.votecount').text());
    $.post(voteUpUrl, { id: id }, function(response) {
      if (!response) {
        // oops, something went wrong - display error message?
        return;
      }
      container.find('.votecount').text(response.voteCount); // update vote count
      if (response.voteCount < voteCount) {
        // the user previously upvoted and has now removed it
        container.find('.voteup').removeClass('current');
      } else if (response.voteCount == voteCount + 1) {
        // the user had not previously voted on this comment
        container.find('.voteup').addClass('current');
      } else if (response.voteCount == voteCount + 2) {
        // the user previoulsy down voted
        container.find('.votedown').removeClass('current');
        container.find('.voteup').addClass('current');
      }
    });
  });
  $('.votedown').click(function() {
    ... // similar to above (modify logic in if/elseif blocks)
  });

</script>

and the controller method

public JsonResult VoteUp(int id)
{
  int voteCount = // your logic to calculate the new total based on the users current vote (if any) for the comment
  return Json(new { voteCount = voteCount });
}

Upvotes: 4

Related Questions