1

The basic problem is this. I'm using CKEditor for an interface for a blog post of sorts. CKEditor gets the wordcount, but I have to use some client-side JavaScript to clean it up. I want to pass the wordcount into the database so I know how many words each post has.

I have a viewmodel for the post:

public class NewStoryViewModel
{
    [Required]
    public string Title { get; set; }

    [Required]
    public string Content { get; set; }

    [Required]
    public int Genre { get; set; }
    public IEnumerable<Genre> Genres { get; set; }

    [Required]
    public int StoryType { get; set; }
    public IEnumerable<StoryType> StoryTypes { get; set; }

    public int WordCount { get; set; }

    [Required]
    public int StoryAgeRange { get; set; }
    public IEnumerable<StoryAgeRange> StoryAgeRanges { get; set; }

    [Required]
    public int Visibility { get; set; }
    public IEnumerable<Visibility> Visibilities { get; set; }
}

And the controller for the post:

[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
[ValidateInput(false)]
public ActionResult New (NewStoryViewModel viewModel)
{
    //confirm form data is valid
    if (ModelState.IsValid)
    {
        //create new story object
        var newStory = new Story
        {
            AuthorId = User.Identity.GetUserId(),
            Title = viewModel.Title,
            Content = viewModel.Content,
            GenreId = viewModel.Genre,
            StoryTypeId = viewModel.StoryType,
            StoryAgeRangeId = viewModel.StoryAgeRange,
            VisibilityId = viewModel.Visibility,
            CreatedAt = DateTime.Now,
            WordCount = viewModel.WordCount
        };

        //add new story to db
        dbContext.Stories.Add(newStory);

        //save db
        dbContext.SaveChanges();

        return RedirectToAction("Index", "Story");
    }
    else
    {
        return View(viewModel);
    }
}

On the client-side in the razor view, I have this:

$(document).ready(function () {

    $('#addStoryBtn').on('click', function () {

        //get the content of the div
        var wordcount = $('#cke_wordcount_Content').html();

        //chop off the words before the number in the string
        var slicedWordCount = wordcount.slice(6);

        //remove any excess white space
        var trimmedWordCount = slicedWordCount.trim();

        //capture the index of the slash 
        var indexOfSlash = trimmedWordCount.indexOf("/");

        //split the string at the slash to get the words used out of the total allotted
        var finalWordCount = trimmedWordCount.slice(0, indexOfSlash);

        //$.ajax({
        //  url: "/story/new",
        //  type: 'POST',
        //  data: {
        //      WordCount = finalWordCount
        //  },
        //  success: function (data) {
        //      console.log("Success")
        //  },
        //  error: function (error) {
        //      console.log("error is " + error);
        //  }
        //})
    });
});

I do this because CKEditor prints the word count out of the maximum like this:

Words: 4/5000

so I use a bit of JS to remove everything I don't need and keep the number before the slash.

But the ajax post didn't work (stepping through the controller, it returns 0).

I thought about using a hiddenfield in the view. Something like:

@Html.Hidden(new  { WordCount = finalWordCount })

But the razor view gives me an error that finalWordCount doesn't mean anything in the current context. I surmise it's because finalWordCount is subject to the button click and since the addPost button hasn't been clicked, finalWordCount is undefined.

Any suggestions on how to pass the wordcount to the viewmodel?

Lauren Rutledge
  • 1,195
  • 5
  • 18
  • 27
J.G.Sable
  • 1,258
  • 4
  • 26
  • 62

2 Answers2

0

You've mentioned in the comments that you're experiencing a 500 internal server error, which I'm guessing is after you've tried Shyju's suggestion to fix the invalid JSON. My guess is you're unable to even debug the controller action right now because it's expecting an anti-forgery token to be passed to it, but you're not sending that in the body of the POST request.

To fix that, try this:

var form = // selector for your form
var token = $('input[name="__RequestVerificationToken"]', form).val();

$.ajax({
  url: "/story/new",
  type: 'POST',
  data: {
      __RequestVerificationToken: token,
      WordCount: finalWordCount
  },
  success: function (data) {
      console.log("Success")
  },
  error: function (error) {
      console.log("error is " + error);
  }
});

That should hopefully fix the validation error, allowing you to at least reach the action.

John H
  • 14,422
  • 4
  • 41
  • 74
  • I have a call in about 2 minutes but want to try this. What would the selector be when using @using(Html.BeginForm("New", "Story")). Should I add an anonymous object to that to give the form an id? – J.G.Sable Aug 08 '18 at 17:53
  • @J.G.Sable The simplest way to get it, assuming no other forms are on the page, would be to just use: `$("form")`. If that works, you can specify an actual id for the form with one of the overloads of `Html.BeginForm()` as you've said. – John H Aug 08 '18 at 18:03
  • Finally had a chance to try this. For some reason stack overflow picture uploader isn't working for me. But, now the wordcount is coming through when I hover over the viewmodel in the controller but the rest of the fields are blank, causing my modelstate to be invalid. I suppose I could build the entire post object on the client side and pass it all through that ajax call, but wanted to keep it lighter – J.G.Sable Aug 08 '18 at 19:32
  • @J.G.Sable Nice, one problem solved then. Your problem is because you're not POSTing the remaining elements to the controller. It's simply a case of building up those elements from the form, and POSTing them in the `data` object, just as you have done with `token` and `WordCount`. You're having to POST them all manually because you're using the AJAX call. That said, maybe a better approach would be to modify the word count when the form is actually submitted, without making the AJAX call. That way, you wouldn't have to POST the fields manually. It depends on your requirements though. – John H Aug 08 '18 at 19:36
  • Right. Basically two options from what I'm interpreting. Create the object in the ajax call and post that to the controller, or, don't do any of the javascript cleanup on the word count, post the whole string, and use C# to cut up that string into just the word count that I need. Correct? – J.G.Sable Aug 08 '18 at 19:38
  • @J.G.Sable Exactly. :) – John H Aug 08 '18 at 19:39
  • Thanks for the help. I'll work on it. (I'll probably go with option one since I have more experience with JS ajax/string manipulations than C#) – J.G.Sable Aug 08 '18 at 19:40
  • Can I ask you one more question? – J.G.Sable Aug 08 '18 at 20:19
  • @J.G.Sable You just did ;), but if it's relevant to this, sure. If not, it might be best to post a separate one if other people will more easily benefit from finding it via Google in future. – John H Aug 08 '18 at 20:22
  • no, it's the same thing. I created a new object in the client-side javascript and successfully posted it to the controller. However, if you look at my controller code above, I also create an object in there and forgot to delete it or modify it. so now I have two duplicate stories in my database - one with a word count, and one without, but otherwise the same. I obviously need to change something in the post controller so that this doesn't happen. Do I just need to pass the viewmodel parameter to the dbcontext.stories.add()? – J.G.Sable Aug 08 '18 at 20:24
  • @J.G.Sable There are a couple of things here. Firstly, whenever you call `Add()` on your `DbContext`, it will always insert a new record following a call to `SaveChanges()`. If you want to cater for update behaviour. See https://stackoverflow.com/questions/25894587/how-to-update-record-using-entity-framework-6 for an example. – John H Aug 08 '18 at 20:29
  • @J.G.Sable Secondly, saving view models directly to your database isn't a good idea. What you have in your code is really two things: a view model, and a domain model (`newStory`). As you're still fitting the pieces together of how it all works, I'd stick to what you have, but once you get further into it, start reading up about domain models, as these will solve a lot of problems for you. – John H Aug 08 '18 at 20:32
  • hm. I can see what you're saying and understand that I'm not actually passing a viewmodel to the db but constructing a new instance of the domain model which is modeled after the Story.cs model class. I guess I'm still confused as to how I need to change my controller so that two records aren't saved to the db, because I need to set the authorId of the story too, which I get from User.Identity – J.G.Sable Aug 08 '18 at 20:37
  • @J.G.Sable Then you need to add some kind of duplicate detection (e.g. query for a story to see if a duplicate already exists, if not insert one). – John H Aug 08 '18 at 20:42
  • By the way. I figured it out and I didn't need to check for duplicates. What was happening is that originally I was using razor and grabbing the form via a button submit. But then I added the JS ajax post on button click and never changed the button's type from 'submit' to 'button'. So the single button click was literally sending two sets of data to the server. Changed the button type from 'submit' to button and it works fine and posts to the db once. – J.G.Sable Aug 08 '18 at 22:30
0

The MVC application is probably expecting a json format request body, as that is the default configuration of asp.net MVC. So before posting the data to the server you need to stringify the model to a proper json.

Try it like this

var data = JSON.stringify({WordCount: finalWordCount});
$.ajax({
     url: "/story/new",
     type: 'POST',
      data: data,
      success: function (data) {
          console.log("Success")
      },
      error: function (error) {
          console.log("error is " + error);
      }
    })
Marcus Höglund
  • 16,172
  • 11
  • 47
  • 69