2

So I am trying to populate a model, which needs a userId. So right now, my ActionResult Index method just returns a view which prompts for the user to enter their userId. I need to grab this value and then create the viewmodel and then pass it in back to the view so that way I can do stuff like @Model.blah. I was wondering how I can do that, whether it'd be having two different action result methods, or in general how to populate models when the information you need must be queried first before constructing the viewmodel.

Here is my controller:

 public ActionResult Index()
        {
            // Ask for UserID
            return View("~/Views/FingerprintTool/Index.cshtml");
        }


public ActionResult Index(int userId)
        {

            var response = _driver.ListFingerprints(userId);
            var model = new FingerprintToolModel()
            {
                Fingerprints = response.Fingerprints
            };
            return View(model);
        }

And here is my html:

  model Models.Tools.FingerprintToolModel

<head>
    <script type="text/javascript" src="~/Scripts/FingerprintTool.js"></script>

</head>
<body>
    <h1>Fingerprint Tool</h1>
<form id="userIdForm" method="post">
    Type in your UserId: <input name="userId" type="number" id="formUserId"/>
    <input type="submit"/>
</form>

    @if (Model != null)
    {
        <h1>SUCCESS</h1>
    }
</body>

I also have a Javascript file that deals with the submit button be clicked and whatnot.

Here is the Js:

window.onload = function() {
    $("#userIdForm")
        .submit(function(e) {
            e.preventDefault();
            if ($("#formUserId").length == 0) {
                alert("Invalid UserId");
            } else {
                listUserFingerprints();
            }

        });
}

function listUserFingerprints() {
    // what to do here
}
kebabTiger
  • 642
  • 7
  • 15

2 Answers2

9

Form:

First, update your form. You can use a simple HTML form, or the @Html.BeginForm() helper, like so:

@using Html.BeginForm()
{
    @Html.AntiForgeryToken()

    <label for="userId">Enter your User ID:</label>
    <input type="number" name="userId" id="userId" />

    <input type="submit" />
}

By default, your Html.BeginForm creates all the necessary elements and form action etc. If you prefer, you can still use standard HTML. Both do the same job.

Note the AntiForgeryToken. This is so useful when using POST form data (and even get, if you really must).

In your Controller, you can then check against this AntiForgeryToken, to protect against malicious posts or data being injected - see below.

Controller:

Create another method in your Controller, with the same name as your existing one; decorate it with the [HttpPost] attribute.

If you are using an AntiForgeryToken, you need to decorate the method with [ValidateAntiForgeryToken] also.

Like this:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(int userId)
{
    // MVC's DefaultModelBinder is smart enough to map your form
    // post values to objects of the correct type, given the name in the form

    // Get the data from your repository etc.
    var model = GetUser(userId);

    // Then return this model to the view:
    return View(model);
}

Notice the parameter we are looking for in the method signature matches the name attribute of the input in your form.

MVC's DefaultModelBinder is able to make the connection between the two and map the value(s) of any parameters to form values.

You can also check if your model is null (for example, that userId doesn't exist) and then return an error to the page if so.

I like to use validation errors, but you can also use ViewBag or any other kind of method.

You can do a check and add an error, like this:

// Get the data from your repository etc.
var model = GetUser(userId);

if (model == null)
{
    ModelState.AddModelError("", "The user ID you entered cannot be found. Please try again");
}

// Then return this model to the view:
return View(model);

This will add a "generic" model error to the view data, which you can then process in the view. More on that, below.

View:

In order to support your view displaying your model, you need to insert an @model statement at the top of your cshtml file.

Like so:

@model MyNameSpace.Models.User

This tells the view engine what Model type to expect from the Controller. In this case I have used User, but it would be whatever your class is called.

Be sure to use the fully-qualified namespace of your class in order to access it.

Then, in your HTML code, you can access the properties of your model using @Model.YourProperty.

Like this:

...
<div>@Model.Username</div>
<div>@Model.FullName</div>
<ul>
    @foreach (var fingerPrint in Model.FingerPrints){
        <li>@fingerPrint.WhateverProperty</li>
    }
</ul>
...

As you can see, this loops through the FingerPrints (or whatever the property is called on your model object) and prints them out in a <ul>. This is to give you an idea of how to access the data from your model.

It is a good idea to create strongly-typed views like this - as this is the WHOLE idea of MVC in the first place :)

Don't forget to add an if check around the part of the page you're access the @Model data (otherwise you will get a NullReferenceException):

@if (Model != null){
    <div id="modelDataInHere">
        ... // Display the data from your model, nice and pretty-like
    </div>
}

Note the casing difference between the declaration and printing the values to the HTML output. I won't go into detail, but this is correct and necessary. Don't mix the two up.

Over to that "Validation Error" (AddModelError) we added in the Controller.

If you're using the Html.BeginForm() helper - like in the example - then you can add a Summary in the form, somewhere. When ModelState.AddModelError() is called in your controller, this populates the view data and this error can be displayed on the page.

Like so:

@using Html.BeginForm()
{
    @Html.AntiForgeryToken()

    // This puts any "generic" error messages at the top of the form
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })

    // Original form fields
    <label for="userId">Enter your User ID:</label>
    <input type="number" name="userId" id="userId" />

    <input type="submit" />
}

No need for any Ajax or any JavaScript in here. You're simply using MVC for what it was designed in the first place.

You can use Ajax, however.

I would encourage you to read into it. There is also a similar helper: Ajax.BeginForm(), which renders the form as an Ajax one.

A bit of JS set up is required. And your controller action could return a PartialView, rather than a full-blown one.

Have a read up on using Ajax form posts here: http://eliot-jones.com/2014/09/mvc-ajax

The finished article

Time is marching on, and this post is getting longer.

But for the sake of clarity, your view and controller should look something like below. (I've stuck in a made-up class, so you can see the where the properties come from):

View

@model YourNameSpace.Models.User

... all your other code in here...

<div>
    @using Html.BeginForm()
    {
        @Html.AntiForgeryToken()

        @Html.ValidationSummary(true, "", new { @class = "text-danger" })

        <label for="userId">Enter your User ID:</label>
        <input type="number" name="userId" id="userId" />

        <input type="submit" />
    }
</div>

@if (Model != null)
{
<!-- If there is a model present, display the data for it: -->

        <div>
        <div>@Model.Username</div>
        <div>@Model.FullName</div>
        <ul>
            @foreach (var fingerPrint in Model.FingerPrints)
            {
                <li>@fingerPrint.WhateverProperty</li>
            }
        </ul>
    </div>
}

Controller

public ActionResult Index()
{
    // This is your standard "GET" request for "Index"
    return View();
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(int userId)
{
    // MVC's DefaultModelBinder is smart enough to map your form
    // post values to objects of the correct type, given the name in the form

    // Get the data from your repository etc.
    var model = GetUser(userId);

    if (model == null)
    {
        ModelState.AddModelError("", "The user ID you entered cannot be found. Please try again");
    }

    // Then return this model to the view:
    return View(model);
}

Sample model

public class User
{
    public string Username { get; set; }
    public string FullName { get; set; }
    public List<FingerPrint> FingerPrints { get; set; }
}

Sincerely hope this helps you, and I wish you the best in your project.

Any questions, please feel free to ask :)

Geoff James
  • 3,122
  • 1
  • 17
  • 36
  • Wow! Great thoroughness with the response! One issue I have is that I can't seem to compile the @Html stuff. Is there any dependency that i'm missing that i'd need? – kebabTiger Jul 02 '16 at 01:19
  • No problem. I've seen this a few times myself with the `@Html` helper. Usually something really trivial. This link might help: http://stackoverflow.com/questions/12041633/razor-views-not-seeing-system-web-mvc-htmlhelper – Geoff James Jul 02 '16 at 06:11
  • I also should have asked. Are you using the Razor view engine and syntax? – Geoff James Jul 02 '16 at 08:14
  • Yes I am using the RazorView engine and syntax – kebabTiger Jul 03 '16 at 17:25
  • So I just gave this a try, and still nothing happens. Check out my updated code in the question! – kebabTiger Jul 03 '16 at 17:26
  • When i click submit, and make the method type post, will it automatically redirect to the second Index method? – kebabTiger Jul 03 '16 at 17:28
  • 1
    Yes, your submit should perform a POST to your controller and the second ActionResult with `[HttpPost]` will be stepped into. This is usually the default for a 'BeginForm()' helper, but you can set it explicitly. Also, have you taken out the JS code from you question, as this will prevent the default behaviour of the form and instead run whatever JS code you have/had. – Geoff James Jul 03 '16 at 17:31
  • That was it! The JS was preventing it from doing the default behavior! Thanks so much for your response! – kebabTiger Jul 03 '16 at 17:39
  • With this current setup, if I wanted to do anything with javascript, unrelated to this method switching, it would not do the intended task. Is there any way around this? – kebabTiger Jul 03 '16 at 19:05
  • Sorry for the late reply! What is said "*intended task*"? I would opt for asking another question, to avoid changing the scope of this one. Link me in a comment on here, when you've posted it and I'll take a look :) – Geoff James Jul 20 '16 at 11:02
1

Seems like you need to work with json and AJAX method:

 [HttpPost]
 public ActionResult BasePage(int userId)
    {
        // user ID is binded to userId variable, based on the input name
        var model = populate(userId);
        // Remember to return the model to the view
        return Json(model);
    }

The Javascript for calling this would be:

function listUserFingerprints() {
    $.post("/FingerprintTool/BasePage", {userId: $("#formUserId").val() }, function(model) {
        console.log(model); // do whatever you want with model here
   }
}
Huy Hoang Pham
  • 4,107
  • 1
  • 16
  • 28
  • This is similar to what i have. Would the second `ActionResult` method redirect to the same `cshtml` page as before? Or would I have to create a new one? – kebabTiger Jul 01 '16 at 21:24
  • Yes, the second `ActionResult` method would redirect to the same cshtml page, but with the model populated. – Huy Hoang Pham Jul 01 '16 at 21:25
  • When I do that, I get the error: `Object reference not set to an instance of an object`, since on the first instantiation of the view, Model is not populated – kebabTiger Jul 01 '16 at 21:28
  • Oops, sorrry. You should create a new cshtml file call `submitted.cshtml` and redirect to that page then. See my update answer. – Huy Hoang Pham Jul 01 '16 at 21:33
  • how would I call the second ActionResult controller from the javascript, since it isn't a post or get, it's just an actionresult? – kebabTiger Jul 01 '16 at 21:33
  • ok, sounds good. But how would I call the `ActionResult` method from my javascript, since it isn't a .GEt or .Post etc – kebabTiger Jul 01 '16 at 21:35
  • You have to make an ajax call then. http://www.w3schools.com/ajax/ajax_xmlhttprequest_send.asp – Huy Hoang Pham Jul 01 '16 at 21:36
  • When I do this: `var request = new XMLHttpRequest(); request.open("POST", "/FingerprintTool/BasePage", true); request.send();` Nothing happens, where BasePage is the name of my second `ActionResult` method – kebabTiger Jul 01 '16 at 21:46
  • Then will I not be returning the submittedView? – kebabTiger Jul 01 '16 at 22:04
  • There's no error, but nothing happens. With your edited answer, the new view won't be loaded either... so nothing visible will happen... right? – kebabTiger Jul 01 '16 at 22:09
  • Open the web development console to see if it log the model or not – Huy Hoang Pham Jul 01 '16 at 22:09
  • It returns the stock html page – kebabTiger Jul 01 '16 at 22:11
  • No idea. Try debugging both the client and server side. Good luck http://blog.bcdsoftware.com/2323/how-to-use-chrome-developer-tools-to-debug-ajax/ – Huy Hoang Pham Jul 01 '16 at 22:15